feat(auth): password change (self + admin reset) with rate limiting; dedupe watchlist to user menu
Build & Push Docker Image / build (push) Successful in 2m19s

This commit is contained in:
opencode
2026-08-03 07:43:24 +02:00
parent 63f0bdbab6
commit 68a81ec775
20 changed files with 825 additions and 16 deletions
@@ -52,6 +52,14 @@ export const UserTier = {
enterprise: 'enterprise',
} as const;
export type UserAuthProvider = typeof UserAuthProvider[keyof typeof UserAuthProvider];
export const UserAuthProvider = {
local: 'local',
oidc: 'oidc',
} as const;
export interface User {
id: number;
username: string;
@@ -59,6 +67,7 @@ export interface User {
email?: string | null;
role: UserRole;
tier?: UserTier;
authProvider?: UserAuthProvider;
createdAt: string;
}
@@ -111,6 +120,23 @@ export interface UserRoleUpdate {
tier?: UserRoleUpdateTier;
}
export interface ChangePasswordInput {
/** @minLength 1 */
currentPassword: string;
/** @minLength 8 */
newPassword: string;
}
export interface SetPasswordInput {
/** @minLength 8 */
password: string;
}
export interface PasswordRedirect {
/** @nullable */
url: string | null;
}
export interface AuditLog {
id: number;
entityType: string;
+223
View File
@@ -25,6 +25,7 @@ import type {
AuthMode,
AuthUser,
CategoryStats,
ChangePasswordInput,
EmptyTrash200,
ErrorResponse,
GetRatingDistributionParams,
@@ -35,11 +36,13 @@ import type {
ListToolsParams,
ListTrashedToolsParams,
LocalLoginInput,
PasswordRedirect,
Rating,
RatingDistribution,
RatingHistoryItem,
RatingInput,
RestoreTools200,
SetPasswordInput,
Tool,
ToolInput,
ToolUpdate,
@@ -2051,6 +2054,154 @@ export function useGetMe<TData = Awaited<ReturnType<typeof getMe>>, TError = Err
export const getChangeMyPasswordUrl = () => {
return `/api/auth/me/password`
}
/**
* @summary Change own password (local users only)
*/
export const changeMyPassword = async (changePasswordInput: ChangePasswordInput, options?: RequestInit): Promise<void> => {
return customFetch<void>(getChangeMyPasswordUrl(),
{
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(
changePasswordInput,)
}
);}
export const getChangeMyPasswordMutationOptions = <TError = ErrorType<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof changeMyPassword>>, TError,{data: BodyType<ChangePasswordInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
): UseMutationOptions<Awaited<ReturnType<typeof changeMyPassword>>, TError,{data: BodyType<ChangePasswordInput>}, TContext> => {
const mutationKey = ['changeMyPassword'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof changeMyPassword>>, {data: BodyType<ChangePasswordInput>}> = (props) => {
const {data} = props ?? {};
return changeMyPassword(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type ChangeMyPasswordMutationResult = NonNullable<Awaited<ReturnType<typeof changeMyPassword>>>
export type ChangeMyPasswordMutationBody = BodyType<ChangePasswordInput>
export type ChangeMyPasswordMutationError = ErrorType<ErrorResponse>
/**
* @summary Change own password (local users only)
*/
export const useChangeMyPassword = <TError = ErrorType<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof changeMyPassword>>, TError,{data: BodyType<ChangePasswordInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
): UseMutationResult<
Awaited<ReturnType<typeof changeMyPassword>>,
TError,
{data: BodyType<ChangePasswordInput>},
TContext
> => {
return useMutation(getChangeMyPasswordMutationOptions(options));
}
export const getGetPasswordRedirectUrl = () => {
return `/api/auth/password-redirect`
}
/**
* @summary Get redirect URL for managing credentials in the identity provider
*/
export const getPasswordRedirect = async ( options?: RequestInit): Promise<PasswordRedirect> => {
return customFetch<PasswordRedirect>(getGetPasswordRedirectUrl(),
{
...options,
method: 'GET'
}
);}
export const getGetPasswordRedirectQueryKey = () => {
return [
`/api/auth/password-redirect`
] as const;
}
export const getGetPasswordRedirectQueryOptions = <TData = Awaited<ReturnType<typeof getPasswordRedirect>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getPasswordRedirect>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetPasswordRedirectQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getPasswordRedirect>>> = ({ signal }) => getPasswordRedirect({ signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getPasswordRedirect>>, TError, TData> & { queryKey: QueryKey }
}
export type GetPasswordRedirectQueryResult = NonNullable<Awaited<ReturnType<typeof getPasswordRedirect>>>
export type GetPasswordRedirectQueryError = ErrorType<unknown>
/**
* @summary Get redirect URL for managing credentials in the identity provider
*/
export function useGetPasswordRedirect<TData = Awaited<ReturnType<typeof getPasswordRedirect>>, TError = ErrorType<unknown>>(
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getPasswordRedirect>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPasswordRedirectQueryOptions(options)
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
return { ...query, queryKey: queryOptions.queryKey };
}
export const getGetMePreferencesUrl = () => {
@@ -2566,6 +2717,78 @@ export const useDeleteUser = <TError = ErrorType<unknown>,
return useMutation(getDeleteUserMutationOptions(options));
}
export const getSetUserPasswordUrl = (id: number,) => {
return `/api/users/${id}/password`
}
/**
* @summary Set/reset a user's password (admin only, local users only)
*/
export const setUserPassword = async (id: number,
setPasswordInput: SetPasswordInput, options?: RequestInit): Promise<void> => {
return customFetch<void>(getSetUserPasswordUrl(id),
{
...options,
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(
setPasswordInput,)
}
);}
export const getSetUserPasswordMutationOptions = <TError = ErrorType<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof setUserPassword>>, TError,{id: number;data: BodyType<SetPasswordInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
): UseMutationOptions<Awaited<ReturnType<typeof setUserPassword>>, TError,{id: number;data: BodyType<SetPasswordInput>}, TContext> => {
const mutationKey = ['setUserPassword'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof setUserPassword>>, {id: number;data: BodyType<SetPasswordInput>}> = (props) => {
const {id,data} = props ?? {};
return setUserPassword(id,data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type SetUserPasswordMutationResult = NonNullable<Awaited<ReturnType<typeof setUserPassword>>>
export type SetUserPasswordMutationBody = BodyType<SetPasswordInput>
export type SetUserPasswordMutationError = ErrorType<ErrorResponse>
/**
* @summary Set/reset a user's password (admin only, local users only)
*/
export const useSetUserPassword = <TError = ErrorType<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof setUserPassword>>, TError,{id: number;data: BodyType<SetPasswordInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
): UseMutationResult<
Awaited<ReturnType<typeof setUserPassword>>,
TError,
{id: number;data: BodyType<SetPasswordInput>},
TContext
> => {
return useMutation(getSetUserPasswordMutationOptions(options));
}
export const getListAuditLogsUrl = (params?: ListAuditLogsParams,) => {
const normalizedParams = new URLSearchParams();