mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-03-28 04:17:50 -04:00
Handle password change when the user never had one
This commit is contained in:
parent
b5ace8d6ed
commit
f17af7c75d
2
.github/workflows/auth-hurl.yml
vendored
2
.github/workflows/auth-hurl.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '^1.22.5'
|
||||
go-version: '^1.26.0'
|
||||
cache-dependency-path: ./auth/go.sum
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@ -16,6 +16,8 @@ type User struct {
|
||||
Username string `json:"username" example:"zoriya"`
|
||||
// Email of the user. Can be used as a login.
|
||||
Email string `json:"email" format:"email" example:"kyoo@zoriya.dev"`
|
||||
// False if the user has never setup a password and only used oidc.
|
||||
HasPassword bool `json:"hasPassword"`
|
||||
// When was this account created?
|
||||
CreatedDate time.Time `json:"createdDate" example:"2025-03-29T18:20:05.267Z"`
|
||||
// When was the last time this account made any authorized request?
|
||||
@ -52,7 +54,6 @@ type EditUserDto struct {
|
||||
}
|
||||
|
||||
type EditPasswordDto struct {
|
||||
OldPassword string `json:"oldPassword" validate:"required" example:"password1234"`
|
||||
NewPassword string `json:"newPassword" validate:"required" example:"password1234"`
|
||||
OldPassword *string `json:"oldPassword" example:"password1234"`
|
||||
NewPassword string `json:"newPassword" validate:"required" example:"password1234"`
|
||||
}
|
||||
|
||||
|
||||
@ -468,14 +468,14 @@ func (h *Handler) OidcUnlink(c *echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
user, err := h.db.GetUser(ctx, dbc.GetUserParams{UseId: true, Id: uid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err == pgx.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "No user found")
|
||||
} else if err != nil {
|
||||
return nil
|
||||
}
|
||||
if user.User.Password == nil {
|
||||
return echo.NewHTTPError(http.StatusUnprocessableEntity, "You must configure a password before unlinking your OIDC provider")
|
||||
}
|
||||
|
||||
err = h.db.DeleteOidcHandle(ctx, dbc.DeleteOidcHandleParams{
|
||||
UserPk: user.User.Pk,
|
||||
|
||||
@ -23,6 +23,7 @@ func MapDbUser(user *dbc.User) User {
|
||||
Id: user.Id,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
HasPassword: user.Password != nil,
|
||||
CreatedDate: user.CreatedDate,
|
||||
LastSeen: user.LastSeen,
|
||||
Claims: user.Claims,
|
||||
@ -474,15 +475,20 @@ func (h *Handler) ChangePassword(c *echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
match, err := argon2id.ComparePasswordAndHash(
|
||||
req.OldPassword,
|
||||
*user.User.Password,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !match {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Invalid password")
|
||||
if user.User.Password != nil {
|
||||
if req.OldPassword == nil {
|
||||
return echo.NewHTTPError(http.StatusUnprocessableEntity, "Missing old password")
|
||||
}
|
||||
match, err := argon2id.ComparePasswordAndHash(
|
||||
*req.OldPassword,
|
||||
*user.User.Password,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !match {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Invalid password")
|
||||
}
|
||||
}
|
||||
|
||||
pass, err := argon2id.CreateHash(req.NewPassword, argon2id.DefaultParams)
|
||||
|
||||
@ -5,9 +5,9 @@ export const User = z
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
email: z.string(),
|
||||
hasPassword: z.boolean().default(true),
|
||||
claims: z.object({
|
||||
permissions: z.array(z.string()),
|
||||
// hasPassword: z.boolean().default(true),
|
||||
settings: z
|
||||
.object({
|
||||
downloadQuality: z
|
||||
@ -37,7 +37,7 @@ export const User = z
|
||||
z.string(),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
username: z.string().nullable().default(""),
|
||||
username: z.string(),
|
||||
profileUrl: z.string().nullable(),
|
||||
}),
|
||||
)
|
||||
|
||||
@ -5,7 +5,7 @@ import type {
|
||||
ReactNode,
|
||||
Ref,
|
||||
} from "react";
|
||||
import { type Falsy, type PressableProps, View } from "react-native";
|
||||
import type { Falsy, PressableProps, View } from "react-native";
|
||||
import { cn } from "~/utils";
|
||||
import { Icon } from "./icons";
|
||||
import { PressableFeedback } from "./links";
|
||||
|
||||
@ -12,12 +12,15 @@ export const Overlay = ({
|
||||
close,
|
||||
children,
|
||||
scroll = true,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
icon?: IconType;
|
||||
title: string;
|
||||
close?: () => void;
|
||||
children: ReactNode;
|
||||
scroll?: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Pressable
|
||||
@ -39,9 +42,13 @@ export const Overlay = ({
|
||||
{close && <IconButton icon={Close} onPress={close} />}
|
||||
</View>
|
||||
{scroll ? (
|
||||
<ScrollView className="p-6">{children}</ScrollView>
|
||||
<ScrollView className={cn("p-6", className)} {...props}>
|
||||
{children}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View className="flex-1">{children}</View>
|
||||
<View className={cn("flex-1", className)} {...props}>
|
||||
{children}
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
@ -54,15 +61,17 @@ export const Popup = ({
|
||||
close,
|
||||
children,
|
||||
scroll,
|
||||
...props
|
||||
}: {
|
||||
icon?: IconType;
|
||||
title: string;
|
||||
close?: () => void;
|
||||
children: ReactNode;
|
||||
scroll?: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Overlay icon={icon} title={title} close={close} scroll={scroll}>
|
||||
<Overlay icon={icon} title={title} close={close} scroll={scroll} {...props}>
|
||||
{children}
|
||||
</Overlay>
|
||||
);
|
||||
|
||||
@ -101,6 +101,7 @@ export const AddVideoFooter = ({
|
||||
return (
|
||||
<ComboBox
|
||||
Trigger={(props) => (
|
||||
// @ts-expect-error prop mismatch due to generic
|
||||
<Button
|
||||
icon={LibraryAdd}
|
||||
text={t("videos-map.add")}
|
||||
|
||||
@ -76,6 +76,7 @@ const AddMovieVideoFooter = ({ slug }: { slug: string }) => {
|
||||
return (
|
||||
<ComboBox
|
||||
Trigger={(props) => (
|
||||
// @ts-expect-error prop mismatch due to generic
|
||||
<Button
|
||||
icon={LibraryAdd}
|
||||
text={t("videos-map.add")}
|
||||
|
||||
@ -7,9 +7,9 @@ import { defaultApiUrl } from "~/providers/account-provider";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { FormPage } from "./form";
|
||||
import { login } from "./logic";
|
||||
import { OidcLogin } from "./oidc";
|
||||
import { PasswordInput } from "./password-input";
|
||||
import { ServerUrlPage } from "./server-url";
|
||||
import { OidcLogin } from "./oidc";
|
||||
|
||||
export const RegisterPage = () => {
|
||||
const [apiUrl] = useQueryState("apiUrl", defaultApiUrl);
|
||||
|
||||
@ -59,7 +59,7 @@ export const AccountSettings = () => {
|
||||
compute: (body: { oldPassword: string; newPassword: string }) => ({
|
||||
body,
|
||||
}),
|
||||
invalidate: null,
|
||||
invalidate: ["auth", "users", "me"],
|
||||
});
|
||||
|
||||
return (
|
||||
@ -189,7 +189,7 @@ export const AccountSettings = () => {
|
||||
<ChangePasswordPopup
|
||||
icon={Password}
|
||||
label={t("settings.account.password.label")}
|
||||
hasPassword={true}
|
||||
hasPassword={account.hasPassword}
|
||||
apply={async (op, np) =>
|
||||
await editPassword({ oldPassword: op, newPassword: np })
|
||||
}
|
||||
@ -273,6 +273,7 @@ const ChangePasswordPopup = ({
|
||||
value={oldValue}
|
||||
onChangeText={(v) => setOldValue(v)}
|
||||
placeholder={t("settings.account.password.oldPassword")}
|
||||
containerClassName="my-1"
|
||||
/>
|
||||
)}
|
||||
<PasswordInput
|
||||
@ -280,9 +281,10 @@ const ChangePasswordPopup = ({
|
||||
value={newValue}
|
||||
onChangeText={(v) => setNewValue(v)}
|
||||
placeholder={t("settings.account.password.newPassword")}
|
||||
containerClassName="my-1"
|
||||
/>
|
||||
{error && <P className="text-red-500">{error}</P>}
|
||||
<View className="flex-row gap-2 self-end">
|
||||
<View className="my-1 flex-row gap-2 self-end">
|
||||
<Button
|
||||
text={t("misc.cancel")}
|
||||
onPress={() => close()}
|
||||
|
||||
@ -45,10 +45,10 @@ export const SettingsContainer = ({
|
||||
extraTop,
|
||||
...props
|
||||
}: {
|
||||
children: ReactElement | (ReactElement | Falsy)[] | Falsy;
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
extra?: ReactElement;
|
||||
extraTop?: ReactElement;
|
||||
extra?: ReactNode;
|
||||
extraTop?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Container {...props}>
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import Badge from "@material-symbols/svg-400/outlined/badge.svg";
|
||||
import Remove from "@material-symbols/svg-400/outlined/close.svg";
|
||||
import OpenProfile from "@material-symbols/svg-400/outlined/open_in_new.svg";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "react-native";
|
||||
import { type KyooError, User } from "~/models";
|
||||
import { Button, IconButton, Link, P, Skeleton, tooltip } from "~/primitives";
|
||||
import { type QueryIdentifier, useFetch, useMutation } from "~/query";
|
||||
import { Preference, SettingsContainer } from "./base";
|
||||
import { OidcLogin } from "../login/oidc";
|
||||
import { User } from "~/models";
|
||||
import { Preference, SettingsContainer } from "./base";
|
||||
|
||||
export const OidcSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const [unlinkError, setUnlinkError] = useState<string | null>(null);
|
||||
const { data } = useFetch(OidcLogin.query());
|
||||
const { data: user } = useFetch(OidcSettings.query());
|
||||
const { mutateAsync: unlinkAccount } = useMutation({
|
||||
@ -23,6 +25,7 @@ export const OidcSettings = () => {
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.oidc.label")}>
|
||||
{unlinkError && <P className="text-red-500">{unlinkError}</P>}
|
||||
{data && user
|
||||
? Object.entries(data.oidc).map(([id, x]) => {
|
||||
const acc = user.oidc[id];
|
||||
@ -60,7 +63,14 @@ export const OidcSettings = () => {
|
||||
)}
|
||||
<IconButton
|
||||
icon={Remove}
|
||||
onPress={() => unlinkAccount(id)}
|
||||
onPress={async () => {
|
||||
setUnlinkError(null);
|
||||
try {
|
||||
await unlinkAccount(id);
|
||||
} catch (e) {
|
||||
setUnlinkError((e as KyooError).message);
|
||||
}
|
||||
}}
|
||||
{...tooltip(
|
||||
t("settings.oidc.delete", { provider: x.name }),
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user