From f17af7c75de54f0e61a9ee0c5e4e15fb3631943e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 25 Mar 2026 20:09:46 +0100 Subject: [PATCH] Handle password change when the user never had one --- .github/workflows/auth-hurl.yml | 2 +- auth/models/users.go | 7 +++--- auth/oidc.go | 6 ++--- auth/users.go | 24 ++++++++++++------- front/src/models/user.ts | 4 ++-- front/src/primitives/button.tsx | 2 +- front/src/primitives/popup.tsx | 15 +++++++++--- front/src/ui/admin/videos-modal/headers.tsx | 1 + .../src/ui/admin/videos-modal/movie-modal.tsx | 1 + front/src/ui/login/register.tsx | 2 +- front/src/ui/settings/account.tsx | 8 ++++--- front/src/ui/settings/base.tsx | 6 ++--- front/src/ui/settings/oidc.tsx | 16 ++++++++++--- 13 files changed, 62 insertions(+), 32 deletions(-) diff --git a/.github/workflows/auth-hurl.yml b/.github/workflows/auth-hurl.yml index d4db2c7f..619fdd2b 100644 --- a/.github/workflows/auth-hurl.yml +++ b/.github/workflows/auth-hurl.yml @@ -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 diff --git a/auth/models/users.go b/auth/models/users.go index 7e6c04ca..de704348 100644 --- a/auth/models/users.go +++ b/auth/models/users.go @@ -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"` } - diff --git a/auth/oidc.go b/auth/oidc.go index bd8420ae..a6b6aa17 100644 --- a/auth/oidc.go +++ b/auth/oidc.go @@ -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, diff --git a/auth/users.go b/auth/users.go index 461600fb..5a0ff5f2 100644 --- a/auth/users.go +++ b/auth/users.go @@ -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) diff --git a/front/src/models/user.ts b/front/src/models/user.ts index 22f90b7e..495f127d 100644 --- a/front/src/models/user.ts +++ b/front/src/models/user.ts @@ -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(), }), ) diff --git a/front/src/primitives/button.tsx b/front/src/primitives/button.tsx index 6d1df9e1..1800a2f4 100644 --- a/front/src/primitives/button.tsx +++ b/front/src/primitives/button.tsx @@ -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"; diff --git a/front/src/primitives/popup.tsx b/front/src/primitives/popup.tsx index 16fba948..75a8ed0f 100644 --- a/front/src/primitives/popup.tsx +++ b/front/src/primitives/popup.tsx @@ -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 ( } {scroll ? ( - {children} + + {children} + ) : ( - {children} + + {children} + )} @@ -54,15 +61,17 @@ export const Popup = ({ close, children, scroll, + ...props }: { icon?: IconType; title: string; close?: () => void; children: ReactNode; scroll?: boolean; + className?: string; }) => { return ( - + {children} ); diff --git a/front/src/ui/admin/videos-modal/headers.tsx b/front/src/ui/admin/videos-modal/headers.tsx index 87a25147..ec246ada 100644 --- a/front/src/ui/admin/videos-modal/headers.tsx +++ b/front/src/ui/admin/videos-modal/headers.tsx @@ -101,6 +101,7 @@ export const AddVideoFooter = ({ return ( ( + // @ts-expect-error prop mismatch due to generic