feat(auth): password change (self + admin reset) with rate limiting; dedupe watchlist to user menu
Build & Push Docker Image / build (push) Successful in 2m19s
Build & Push Docker Image / build (push) Successful in 2m19s
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -614,6 +614,58 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/auth/me/password:
|
||||
post:
|
||||
operationId: changeMyPassword
|
||||
tags: [auth]
|
||||
summary: Change own password (local users only)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChangePasswordInput"
|
||||
responses:
|
||||
"204":
|
||||
description: Password changed
|
||||
"400":
|
||||
description: Invalid input or wrong current password
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"401":
|
||||
description: Not authenticated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"422":
|
||||
description: OIDC user - password is managed by the identity provider
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"429":
|
||||
description: Too many attempts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/auth/password-redirect:
|
||||
get:
|
||||
operationId: getPasswordRedirect
|
||||
tags: [auth]
|
||||
summary: Get redirect URL for managing credentials in the identity provider
|
||||
responses:
|
||||
"200":
|
||||
description: Redirect URL (null in local mode)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PasswordRedirect"
|
||||
|
||||
/auth/me/preferences:
|
||||
get:
|
||||
operationId: getMePreferences
|
||||
@@ -783,6 +835,51 @@ paths:
|
||||
"204":
|
||||
description: Deleted
|
||||
|
||||
/users/{id}/password:
|
||||
patch:
|
||||
operationId: setUserPassword
|
||||
tags: [users]
|
||||
summary: Set/reset a user's password (admin only, local users only)
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SetPasswordInput"
|
||||
responses:
|
||||
"204":
|
||||
description: Password updated
|
||||
"400":
|
||||
description: Validation error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: User not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"422":
|
||||
description: OIDC user - password is managed by the identity provider
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"429":
|
||||
description: Too many attempts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/audit-logs:
|
||||
get:
|
||||
operationId: listAuditLogs
|
||||
@@ -870,6 +967,10 @@ components:
|
||||
tier:
|
||||
type: string
|
||||
enum: [free, premium, enterprise]
|
||||
authProvider:
|
||||
type: string
|
||||
enum: [local, oidc]
|
||||
default: local
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -903,6 +1004,32 @@ components:
|
||||
type: string
|
||||
enum: [free, premium, enterprise]
|
||||
|
||||
ChangePasswordInput:
|
||||
type: object
|
||||
required: [currentPassword, newPassword]
|
||||
properties:
|
||||
currentPassword:
|
||||
type: string
|
||||
minLength: 1
|
||||
newPassword:
|
||||
type: string
|
||||
minLength: 8
|
||||
|
||||
SetPasswordInput:
|
||||
type: object
|
||||
required: [password]
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
minLength: 8
|
||||
|
||||
PasswordRedirect:
|
||||
type: object
|
||||
required: [url]
|
||||
properties:
|
||||
url:
|
||||
type: ["string", "null"]
|
||||
|
||||
AuditLog:
|
||||
type: object
|
||||
required: [id, entityType, action, userId, username, createdAt]
|
||||
|
||||
@@ -480,6 +480,28 @@ export const GetMeResponse = zod.object({
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary Change own password (local users only)
|
||||
*/
|
||||
|
||||
export const changeMyPasswordBodyNewPasswordMin = 8;
|
||||
|
||||
|
||||
|
||||
export const ChangeMyPasswordBody = zod.object({
|
||||
"currentPassword": zod.string().min(1),
|
||||
"newPassword": zod.string().min(changeMyPasswordBodyNewPasswordMin)
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get redirect URL for managing credentials in the identity provider
|
||||
*/
|
||||
export const GetPasswordRedirectResponse = zod.object({
|
||||
"url": zod.string().nullable()
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get current user's browse preferences
|
||||
*/
|
||||
@@ -532,12 +554,15 @@ export const GetMeWatchlistResponse = zod.array(GetMeWatchlistResponseItem)
|
||||
/**
|
||||
* @summary List all local users (admin only)
|
||||
*/
|
||||
export const listUsersResponseAuthProviderDefault = `local`;
|
||||
|
||||
export const ListUsersResponseItem = zod.object({
|
||||
"id": zod.number(),
|
||||
"username": zod.string(),
|
||||
"email": zod.string().nullish(),
|
||||
"role": zod.enum(['admin', 'user']),
|
||||
"tier": zod.enum(['free', 'premium', 'enterprise']).optional(),
|
||||
"authProvider": zod.enum(['local', 'oidc']).default(listUsersResponseAuthProviderDefault),
|
||||
"createdAt": zod.coerce.date()
|
||||
})
|
||||
export const ListUsersResponse = zod.array(ListUsersResponseItem)
|
||||
@@ -573,12 +598,15 @@ export const UpdateUserBody = zod.object({
|
||||
"tier": zod.enum(['free', 'premium', 'enterprise']).optional()
|
||||
})
|
||||
|
||||
export const updateUserResponseAuthProviderDefault = `local`;
|
||||
|
||||
export const UpdateUserResponse = zod.object({
|
||||
"id": zod.number(),
|
||||
"username": zod.string(),
|
||||
"email": zod.string().nullish(),
|
||||
"role": zod.enum(['admin', 'user']),
|
||||
"tier": zod.enum(['free', 'premium', 'enterprise']).optional(),
|
||||
"authProvider": zod.enum(['local', 'oidc']).default(updateUserResponseAuthProviderDefault),
|
||||
"createdAt": zod.coerce.date()
|
||||
})
|
||||
|
||||
@@ -591,6 +619,22 @@ export const DeleteUserParams = zod.object({
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary Set/reset a user's password (admin only, local users only)
|
||||
*/
|
||||
export const SetUserPasswordParams = zod.object({
|
||||
"id": zod.coerce.number()
|
||||
})
|
||||
|
||||
export const setUserPasswordBodyPasswordMin = 8;
|
||||
|
||||
|
||||
|
||||
export const SetUserPasswordBody = zod.object({
|
||||
"password": zod.string().min(setUserPasswordBodyPasswordMin)
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary List audit log entries (admin only)
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* ToolRate API — Tool listing and rating platform
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface ChangePasswordInput {
|
||||
/** @minLength 1 */
|
||||
currentPassword: string;
|
||||
/** @minLength 8 */
|
||||
newPassword: string;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export * from './authUser';
|
||||
export * from './authUserRole';
|
||||
export * from './authUserTier';
|
||||
export * from './categoryStats';
|
||||
export * from './changePasswordInput';
|
||||
export * from './emptyTrash200';
|
||||
export * from './errorResponse';
|
||||
export * from './getRatingDistributionParams';
|
||||
@@ -26,12 +27,14 @@ export * from './listToolsParams';
|
||||
export * from './listToolsSort';
|
||||
export * from './listTrashedToolsParams';
|
||||
export * from './localLoginInput';
|
||||
export * from './passwordRedirect';
|
||||
export * from './rating';
|
||||
export * from './ratingDistribution';
|
||||
export * from './ratingHistoryItem';
|
||||
export * from './ratingInput';
|
||||
export * from './restoreTools200';
|
||||
export * from './scoreBucket';
|
||||
export * from './setPasswordInput';
|
||||
export * from './tool';
|
||||
export * from './toolInput';
|
||||
export * from './toolUpdate';
|
||||
@@ -40,6 +43,7 @@ export * from './topToolEntry';
|
||||
export * from './trashTools200';
|
||||
export * from './trashToolsInput';
|
||||
export * from './user';
|
||||
export * from './userAuthProvider';
|
||||
export * from './userCreateInput';
|
||||
export * from './userCreateInputRole';
|
||||
export * from './userCreateInputTier';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* ToolRate API — Tool listing and rating platform
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface PasswordRedirect {
|
||||
/** @nullable */
|
||||
url: string | null;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* ToolRate API — Tool listing and rating platform
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface SetPasswordInput {
|
||||
/** @minLength 8 */
|
||||
password: string;
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* ToolRate API — Tool listing and rating platform
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import type { UserAuthProvider } from './userAuthProvider';
|
||||
import type { UserRole } from './userRole';
|
||||
import type { UserTier } from './userTier';
|
||||
|
||||
@@ -15,5 +16,6 @@ export interface User {
|
||||
email?: string | null;
|
||||
role: UserRole;
|
||||
tier?: UserTier;
|
||||
authProvider?: UserAuthProvider;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* ToolRate API — Tool listing and rating platform
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export type UserAuthProvider = typeof UserAuthProvider[keyof typeof UserAuthProvider];
|
||||
|
||||
|
||||
export const UserAuthProvider = {
|
||||
local: 'local',
|
||||
oidc: 'oidc',
|
||||
} as const;
|
||||
Reference in New Issue
Block a user