Add local user authentication and admin capabilities
Implement local user authentication with password hashing, add admin roles for user management and audit log viewing, and introduce audit logging for critical actions. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 776963d0-f75d-42e2-a57b-cc36bdff8495 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 832a44ff-12ae-4096-8a0d-666ec083d536 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0683fb79-a27c-485c-9333-5f4b288c4567/776963d0-f75d-42e2-a57b-cc36bdff8495/1p7jhzu Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -9,6 +9,82 @@ export interface HealthStatus {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export type AuthModeMode = typeof AuthModeMode[keyof typeof AuthModeMode];
|
||||
|
||||
|
||||
export const AuthModeMode = {
|
||||
oidc: 'oidc',
|
||||
local: 'local',
|
||||
} as const;
|
||||
|
||||
export interface AuthMode {
|
||||
mode: AuthModeMode;
|
||||
}
|
||||
|
||||
export interface LocalLoginInput {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export type UserRole = typeof UserRole[keyof typeof UserRole];
|
||||
|
||||
|
||||
export const UserRole = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
} as const;
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
/** @nullable */
|
||||
email?: string | null;
|
||||
role: UserRole;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type UserCreateInputRole = typeof UserCreateInputRole[keyof typeof UserCreateInputRole];
|
||||
|
||||
|
||||
export const UserCreateInputRole = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
} as const;
|
||||
|
||||
export interface UserCreateInput {
|
||||
/** @minLength 2 */
|
||||
username: string;
|
||||
/** @minLength 6 */
|
||||
password: string;
|
||||
email?: string;
|
||||
role?: UserCreateInputRole;
|
||||
}
|
||||
|
||||
export type UserRoleUpdateRole = typeof UserRoleUpdateRole[keyof typeof UserRoleUpdateRole];
|
||||
|
||||
|
||||
export const UserRoleUpdateRole = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
} as const;
|
||||
|
||||
export interface UserRoleUpdate {
|
||||
role: UserRoleUpdateRole;
|
||||
}
|
||||
|
||||
export interface AuditLog {
|
||||
id: number;
|
||||
entityType: string;
|
||||
/** @nullable */
|
||||
entityId?: number | null;
|
||||
action: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
/** @nullable */
|
||||
changes?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -18,6 +94,8 @@ export interface Tool {
|
||||
websiteUrl?: string | null;
|
||||
/** @nullable */
|
||||
iconUrl?: string | null;
|
||||
/** @nullable */
|
||||
createdBy?: string | null;
|
||||
features?: string[];
|
||||
tags?: string[];
|
||||
createdAt: string;
|
||||
@@ -33,6 +111,8 @@ export interface ToolWithStats {
|
||||
websiteUrl?: string | null;
|
||||
/** @nullable */
|
||||
iconUrl?: string | null;
|
||||
/** @nullable */
|
||||
createdBy?: string | null;
|
||||
features?: string[];
|
||||
tags?: string[];
|
||||
createdAt: string;
|
||||
@@ -144,6 +224,14 @@ export interface RatingDistribution {
|
||||
usability: ScoreBucket[];
|
||||
}
|
||||
|
||||
export type AuthUserRole = typeof AuthUserRole[keyof typeof AuthUserRole];
|
||||
|
||||
|
||||
export const AuthUserRole = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
} as const;
|
||||
|
||||
export interface AuthUser {
|
||||
sub: string;
|
||||
/** @nullable */
|
||||
@@ -152,6 +240,8 @@ export interface AuthUser {
|
||||
name?: string | null;
|
||||
/** @nullable */
|
||||
preferredUsername?: string | null;
|
||||
role?: AuthUserRole;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
@@ -191,3 +281,9 @@ export type GetRatingDistributionParams = {
|
||||
toolId?: number;
|
||||
};
|
||||
|
||||
export type ListAuditLogsParams = {
|
||||
entityType?: string;
|
||||
entityId?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -21,13 +21,17 @@ import type {
|
||||
|
||||
import type {
|
||||
AnalyticsSummary,
|
||||
AuditLog,
|
||||
AuthMode,
|
||||
AuthUser,
|
||||
CategoryStats,
|
||||
ErrorResponse,
|
||||
GetRatingDistributionParams,
|
||||
GetTopToolsParams,
|
||||
HealthStatus,
|
||||
ListAuditLogsParams,
|
||||
ListToolsParams,
|
||||
LocalLoginInput,
|
||||
Rating,
|
||||
RatingDistribution,
|
||||
RatingInput,
|
||||
@@ -35,7 +39,10 @@ import type {
|
||||
ToolInput,
|
||||
ToolUpdate,
|
||||
ToolWithStats,
|
||||
TopToolEntry
|
||||
TopToolEntry,
|
||||
User,
|
||||
UserCreateInput,
|
||||
UserRoleUpdate
|
||||
} from './api.schemas';
|
||||
|
||||
import { customFetch } from '../custom-fetch';
|
||||
@@ -1127,6 +1134,154 @@ export function useListAllFeatures<TData = Awaited<ReturnType<typeof listAllFeat
|
||||
|
||||
|
||||
|
||||
export const getGetAuthModeUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/auth/mode`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get authentication mode (oidc or local)
|
||||
*/
|
||||
export const getAuthMode = async ( options?: RequestInit): Promise<AuthMode> => {
|
||||
|
||||
return customFetch<AuthMode>(getGetAuthModeUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetAuthModeQueryKey = () => {
|
||||
return [
|
||||
`/api/auth/mode`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetAuthModeQueryOptions = <TData = Awaited<ReturnType<typeof getAuthMode>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getAuthMode>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, request: requestOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetAuthModeQueryKey();
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAuthMode>>> = ({ signal }) => getAuthMode({ signal, ...requestOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getAuthMode>>, TError, TData> & { queryKey: QueryKey }
|
||||
}
|
||||
|
||||
export type GetAuthModeQueryResult = NonNullable<Awaited<ReturnType<typeof getAuthMode>>>
|
||||
export type GetAuthModeQueryError = ErrorType<unknown>
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get authentication mode (oidc or local)
|
||||
*/
|
||||
|
||||
export function useGetAuthMode<TData = Awaited<ReturnType<typeof getAuthMode>>, TError = ErrorType<unknown>>(
|
||||
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getAuthMode>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
|
||||
const queryOptions = getGetAuthModeQueryOptions(options)
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getLocalLoginUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/auth/login`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Local username/password login
|
||||
*/
|
||||
export const localLogin = async (localLoginInput: LocalLoginInput, options?: RequestInit): Promise<AuthUser> => {
|
||||
|
||||
return customFetch<AuthUser>(getLocalLoginUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
localLoginInput,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getLocalLoginMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof localLogin>>, TError,{data: BodyType<LocalLoginInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof localLogin>>, TError,{data: BodyType<LocalLoginInput>}, TContext> => {
|
||||
|
||||
const mutationKey = ['localLogin'];
|
||||
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 localLogin>>, {data: BodyType<LocalLoginInput>}> = (props) => {
|
||||
const {data} = props ?? {};
|
||||
|
||||
return localLogin(data,requestOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type LocalLoginMutationResult = NonNullable<Awaited<ReturnType<typeof localLogin>>>
|
||||
export type LocalLoginMutationBody = BodyType<LocalLoginInput>
|
||||
export type LocalLoginMutationError = ErrorType<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Local username/password login
|
||||
*/
|
||||
export const useLocalLogin = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof localLogin>>, TError,{data: BodyType<LocalLoginInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof localLogin>>,
|
||||
TError,
|
||||
{data: BodyType<LocalLoginInput>},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getLocalLoginMutationOptions(options));
|
||||
}
|
||||
|
||||
export const getGetMeUrl = () => {
|
||||
|
||||
|
||||
@@ -1204,3 +1359,377 @@ export function useGetMe<TData = Awaited<ReturnType<typeof getMe>>, TError = Err
|
||||
|
||||
|
||||
|
||||
export const getListUsersUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/users`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List all local users (admin only)
|
||||
*/
|
||||
export const listUsers = async ( options?: RequestInit): Promise<User[]> => {
|
||||
|
||||
return customFetch<User[]>(getListUsersUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getListUsersQueryKey = () => {
|
||||
return [
|
||||
`/api/users`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getListUsersQueryOptions = <TData = Awaited<ReturnType<typeof listUsers>>, TError = ErrorType<ErrorResponse>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listUsers>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, request: requestOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListUsersQueryKey();
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listUsers>>> = ({ signal }) => listUsers({ signal, ...requestOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listUsers>>, TError, TData> & { queryKey: QueryKey }
|
||||
}
|
||||
|
||||
export type ListUsersQueryResult = NonNullable<Awaited<ReturnType<typeof listUsers>>>
|
||||
export type ListUsersQueryError = ErrorType<ErrorResponse>
|
||||
|
||||
|
||||
/**
|
||||
* @summary List all local users (admin only)
|
||||
*/
|
||||
|
||||
export function useListUsers<TData = Awaited<ReturnType<typeof listUsers>>, TError = ErrorType<ErrorResponse>>(
|
||||
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listUsers>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
|
||||
const queryOptions = getListUsersQueryOptions(options)
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getCreateUserUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/users`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create a new local user (admin only)
|
||||
*/
|
||||
export const createUser = async (userCreateInput: UserCreateInput, options?: RequestInit): Promise<User> => {
|
||||
|
||||
return customFetch<User>(getCreateUserUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
userCreateInput,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getCreateUserMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createUser>>, TError,{data: BodyType<UserCreateInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof createUser>>, TError,{data: BodyType<UserCreateInput>}, TContext> => {
|
||||
|
||||
const mutationKey = ['createUser'];
|
||||
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 createUser>>, {data: BodyType<UserCreateInput>}> = (props) => {
|
||||
const {data} = props ?? {};
|
||||
|
||||
return createUser(data,requestOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type CreateUserMutationResult = NonNullable<Awaited<ReturnType<typeof createUser>>>
|
||||
export type CreateUserMutationBody = BodyType<UserCreateInput>
|
||||
export type CreateUserMutationError = ErrorType<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Create a new local user (admin only)
|
||||
*/
|
||||
export const useCreateUser = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createUser>>, TError,{data: BodyType<UserCreateInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createUser>>,
|
||||
TError,
|
||||
{data: BodyType<UserCreateInput>},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateUserMutationOptions(options));
|
||||
}
|
||||
|
||||
export const getUpdateUserUrl = (id: number,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/users/${id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Update user role (admin only)
|
||||
*/
|
||||
export const updateUser = async (id: number,
|
||||
userRoleUpdate: UserRoleUpdate, options?: RequestInit): Promise<User> => {
|
||||
|
||||
return customFetch<User>(getUpdateUserUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
userRoleUpdate,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getUpdateUserMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{id: number;data: BodyType<UserRoleUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{id: number;data: BodyType<UserRoleUpdate>}, TContext> => {
|
||||
|
||||
const mutationKey = ['updateUser'];
|
||||
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 updateUser>>, {id: number;data: BodyType<UserRoleUpdate>}> = (props) => {
|
||||
const {id,data} = props ?? {};
|
||||
|
||||
return updateUser(id,data,requestOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type UpdateUserMutationResult = NonNullable<Awaited<ReturnType<typeof updateUser>>>
|
||||
export type UpdateUserMutationBody = BodyType<UserRoleUpdate>
|
||||
export type UpdateUserMutationError = ErrorType<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Update user role (admin only)
|
||||
*/
|
||||
export const useUpdateUser = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{id: number;data: BodyType<UserRoleUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateUser>>,
|
||||
TError,
|
||||
{id: number;data: BodyType<UserRoleUpdate>},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateUserMutationOptions(options));
|
||||
}
|
||||
|
||||
export const getDeleteUserUrl = (id: number,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/users/${id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Delete a user (admin only)
|
||||
*/
|
||||
export const deleteUser = async (id: number, options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getDeleteUserUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'DELETE'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getDeleteUserMutationOptions = <TError = ErrorType<unknown>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteUser>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof deleteUser>>, TError,{id: number}, TContext> => {
|
||||
|
||||
const mutationKey = ['deleteUser'];
|
||||
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 deleteUser>>, {id: number}> = (props) => {
|
||||
const {id} = props ?? {};
|
||||
|
||||
return deleteUser(id,requestOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type DeleteUserMutationResult = NonNullable<Awaited<ReturnType<typeof deleteUser>>>
|
||||
|
||||
export type DeleteUserMutationError = ErrorType<unknown>
|
||||
|
||||
/**
|
||||
* @summary Delete a user (admin only)
|
||||
*/
|
||||
export const useDeleteUser = <TError = ErrorType<unknown>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteUser>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteUser>>,
|
||||
TError,
|
||||
{id: number},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteUserMutationOptions(options));
|
||||
}
|
||||
|
||||
export const getListAuditLogsUrl = (params?: ListAuditLogsParams,) => {
|
||||
const normalizedParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params || {}).forEach(([key, value]) => {
|
||||
|
||||
if (value !== undefined) {
|
||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
||||
}
|
||||
});
|
||||
|
||||
const stringifiedParams = normalizedParams.toString();
|
||||
|
||||
return stringifiedParams.length > 0 ? `/api/audit-logs?${stringifiedParams}` : `/api/audit-logs`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List audit log entries (admin only)
|
||||
*/
|
||||
export const listAuditLogs = async (params?: ListAuditLogsParams, options?: RequestInit): Promise<AuditLog[]> => {
|
||||
|
||||
return customFetch<AuditLog[]>(getListAuditLogsUrl(params),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getListAuditLogsQueryKey = (params?: ListAuditLogsParams,) => {
|
||||
return [
|
||||
`/api/audit-logs`, ...(params ? [params] : [])
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getListAuditLogsQueryOptions = <TData = Awaited<ReturnType<typeof listAuditLogs>>, TError = ErrorType<unknown>>(params?: ListAuditLogsParams, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listAuditLogs>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, request: requestOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListAuditLogsQueryKey(params);
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listAuditLogs>>> = ({ signal }) => listAuditLogs(params, { signal, ...requestOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listAuditLogs>>, TError, TData> & { queryKey: QueryKey }
|
||||
}
|
||||
|
||||
export type ListAuditLogsQueryResult = NonNullable<Awaited<ReturnType<typeof listAuditLogs>>>
|
||||
export type ListAuditLogsQueryError = ErrorType<unknown>
|
||||
|
||||
|
||||
/**
|
||||
* @summary List audit log entries (admin only)
|
||||
*/
|
||||
|
||||
export function useListAuditLogs<TData = Awaited<ReturnType<typeof listAuditLogs>>, TError = ErrorType<unknown>>(
|
||||
params?: ListAuditLogsParams, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listAuditLogs>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
|
||||
const queryOptions = getListAuditLogsQueryOptions(params,options)
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@ tags:
|
||||
description: Tool ratings
|
||||
- name: analytics
|
||||
description: Analytics and aggregated statistics
|
||||
- name: auth
|
||||
description: Authentication
|
||||
- name: users
|
||||
description: User management (admin only)
|
||||
- name: audit
|
||||
description: Audit log
|
||||
paths:
|
||||
/healthz:
|
||||
get:
|
||||
@@ -319,6 +325,44 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
|
||||
/auth/mode:
|
||||
get:
|
||||
operationId: getAuthMode
|
||||
tags: [auth]
|
||||
summary: Get authentication mode (oidc or local)
|
||||
responses:
|
||||
"200":
|
||||
description: Auth mode
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AuthMode"
|
||||
|
||||
/auth/login:
|
||||
post:
|
||||
operationId: localLogin
|
||||
tags: [auth]
|
||||
summary: Local username/password login
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LocalLoginInput"
|
||||
responses:
|
||||
"200":
|
||||
description: Logged in successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AuthUser"
|
||||
"401":
|
||||
description: Invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/auth/me:
|
||||
get:
|
||||
operationId: getMe
|
||||
@@ -338,6 +382,137 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/users:
|
||||
get:
|
||||
operationId: listUsers
|
||||
tags: [users]
|
||||
summary: List all local users (admin only)
|
||||
responses:
|
||||
"200":
|
||||
description: User list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/User"
|
||||
"401":
|
||||
description: Not authenticated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"403":
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
post:
|
||||
operationId: createUser
|
||||
tags: [users]
|
||||
summary: Create a new local user (admin only)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserCreateInput"
|
||||
responses:
|
||||
"201":
|
||||
description: Created user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/User"
|
||||
"400":
|
||||
description: Validation error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"409":
|
||||
description: Username already exists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/users/{id}:
|
||||
patch:
|
||||
operationId: updateUser
|
||||
tags: [users]
|
||||
summary: Update user role (admin only)
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserRoleUpdate"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/User"
|
||||
"404":
|
||||
description: User not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
delete:
|
||||
operationId: deleteUser
|
||||
tags: [users]
|
||||
summary: Delete a user (admin only)
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"204":
|
||||
description: Deleted
|
||||
|
||||
/audit-logs:
|
||||
get:
|
||||
operationId: listAuditLogs
|
||||
tags: [audit]
|
||||
summary: List audit log entries (admin only)
|
||||
parameters:
|
||||
- name: entityType
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: entityId
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
description: Audit log entries
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AuditLog"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
HealthStatus:
|
||||
@@ -348,6 +523,86 @@ components:
|
||||
required:
|
||||
- status
|
||||
|
||||
AuthMode:
|
||||
type: object
|
||||
required: [mode]
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
enum: [oidc, local]
|
||||
|
||||
LocalLoginInput:
|
||||
type: object
|
||||
required: [username, password]
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
|
||||
User:
|
||||
type: object
|
||||
required: [id, username, role, createdAt]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
email:
|
||||
type: ["string", "null"]
|
||||
role:
|
||||
type: string
|
||||
enum: [admin, user]
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
UserCreateInput:
|
||||
type: object
|
||||
required: [username, password]
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
minLength: 2
|
||||
password:
|
||||
type: string
|
||||
minLength: 6
|
||||
email:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
enum: [admin, user]
|
||||
|
||||
UserRoleUpdate:
|
||||
type: object
|
||||
required: [role]
|
||||
properties:
|
||||
role:
|
||||
type: string
|
||||
enum: [admin, user]
|
||||
|
||||
AuditLog:
|
||||
type: object
|
||||
required: [id, entityType, action, userId, username, createdAt]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
entityType:
|
||||
type: string
|
||||
entityId:
|
||||
type: ["integer", "null"]
|
||||
action:
|
||||
type: string
|
||||
userId:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
changes:
|
||||
type: ["string", "null"]
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
Tool:
|
||||
type: object
|
||||
required: [id, name, description, category, createdAt, updatedAt]
|
||||
@@ -364,6 +619,8 @@ components:
|
||||
type: ["string", "null"]
|
||||
iconUrl:
|
||||
type: ["string", "null"]
|
||||
createdBy:
|
||||
type: ["string", "null"]
|
||||
features:
|
||||
type: array
|
||||
items:
|
||||
@@ -395,6 +652,8 @@ components:
|
||||
type: ["string", "null"]
|
||||
iconUrl:
|
||||
type: ["string", "null"]
|
||||
createdBy:
|
||||
type: ["string", "null"]
|
||||
features:
|
||||
type: array
|
||||
items:
|
||||
@@ -587,6 +846,11 @@ components:
|
||||
type: ["string", "null"]
|
||||
preferredUsername:
|
||||
type: ["string", "null"]
|
||||
role:
|
||||
type: string
|
||||
enum: [admin, user]
|
||||
isLocal:
|
||||
type: boolean
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
|
||||
@@ -33,6 +33,7 @@ export const ListToolsResponseItem = zod.object({
|
||||
"category": zod.string(),
|
||||
"websiteUrl": zod.string().nullish(),
|
||||
"iconUrl": zod.string().nullish(),
|
||||
"createdBy": zod.string().nullish(),
|
||||
"features": zod.array(zod.string()).optional(),
|
||||
"tags": zod.array(zod.string()).optional(),
|
||||
"createdAt": zod.coerce.date(),
|
||||
@@ -78,6 +79,7 @@ export const GetToolResponse = zod.object({
|
||||
"category": zod.string(),
|
||||
"websiteUrl": zod.string().nullish(),
|
||||
"iconUrl": zod.string().nullish(),
|
||||
"createdBy": zod.string().nullish(),
|
||||
"features": zod.array(zod.string()).optional(),
|
||||
"tags": zod.array(zod.string()).optional(),
|
||||
"createdAt": zod.coerce.date(),
|
||||
@@ -116,6 +118,7 @@ export const UpdateToolResponse = zod.object({
|
||||
"category": zod.string(),
|
||||
"websiteUrl": zod.string().nullish(),
|
||||
"iconUrl": zod.string().nullish(),
|
||||
"createdBy": zod.string().nullish(),
|
||||
"features": zod.array(zod.string()).optional(),
|
||||
"tags": zod.array(zod.string()).optional(),
|
||||
"createdAt": zod.coerce.date(),
|
||||
@@ -194,6 +197,7 @@ export const GetAnalyticsSummaryResponse = zod.object({
|
||||
"category": zod.string(),
|
||||
"websiteUrl": zod.string().nullish(),
|
||||
"iconUrl": zod.string().nullish(),
|
||||
"createdBy": zod.string().nullish(),
|
||||
"features": zod.array(zod.string()).optional(),
|
||||
"tags": zod.array(zod.string()).optional(),
|
||||
"createdAt": zod.coerce.date(),
|
||||
@@ -222,6 +226,7 @@ export const GetTopToolsResponseItem = zod.object({
|
||||
"category": zod.string(),
|
||||
"websiteUrl": zod.string().nullish(),
|
||||
"iconUrl": zod.string().nullish(),
|
||||
"createdBy": zod.string().nullish(),
|
||||
"features": zod.array(zod.string()).optional(),
|
||||
"tags": zod.array(zod.string()).optional(),
|
||||
"createdAt": zod.coerce.date(),
|
||||
@@ -283,6 +288,32 @@ export const ListAllFeaturesResponseItem = zod.string()
|
||||
export const ListAllFeaturesResponse = zod.array(ListAllFeaturesResponseItem)
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get authentication mode (oidc or local)
|
||||
*/
|
||||
export const GetAuthModeResponse = zod.object({
|
||||
"mode": zod.enum(['oidc', 'local'])
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary Local username/password login
|
||||
*/
|
||||
export const LocalLoginBody = zod.object({
|
||||
"username": zod.string(),
|
||||
"password": zod.string()
|
||||
})
|
||||
|
||||
export const LocalLoginResponse = zod.object({
|
||||
"sub": zod.string(),
|
||||
"email": zod.string().nullish(),
|
||||
"name": zod.string().nullish(),
|
||||
"preferredUsername": zod.string().nullish(),
|
||||
"role": zod.enum(['admin', 'user']).optional(),
|
||||
"isLocal": zod.boolean().optional()
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get current authenticated user
|
||||
*/
|
||||
@@ -290,7 +321,89 @@ export const GetMeResponse = zod.object({
|
||||
"sub": zod.string(),
|
||||
"email": zod.string().nullish(),
|
||||
"name": zod.string().nullish(),
|
||||
"preferredUsername": zod.string().nullish()
|
||||
"preferredUsername": zod.string().nullish(),
|
||||
"role": zod.enum(['admin', 'user']).optional(),
|
||||
"isLocal": zod.boolean().optional()
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary List all local users (admin only)
|
||||
*/
|
||||
export const ListUsersResponseItem = zod.object({
|
||||
"id": zod.number(),
|
||||
"username": zod.string(),
|
||||
"email": zod.string().nullish(),
|
||||
"role": zod.enum(['admin', 'user']),
|
||||
"createdAt": zod.coerce.date()
|
||||
})
|
||||
export const ListUsersResponse = zod.array(ListUsersResponseItem)
|
||||
|
||||
|
||||
/**
|
||||
* @summary Create a new local user (admin only)
|
||||
*/
|
||||
export const createUserBodyUsernameMin = 2;
|
||||
|
||||
export const createUserBodyPasswordMin = 6;
|
||||
|
||||
|
||||
|
||||
export const CreateUserBody = zod.object({
|
||||
"username": zod.string().min(createUserBodyUsernameMin),
|
||||
"password": zod.string().min(createUserBodyPasswordMin),
|
||||
"email": zod.string().optional(),
|
||||
"role": zod.enum(['admin', 'user']).optional()
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary Update user role (admin only)
|
||||
*/
|
||||
export const UpdateUserParams = zod.object({
|
||||
"id": zod.coerce.number()
|
||||
})
|
||||
|
||||
export const UpdateUserBody = zod.object({
|
||||
"role": zod.enum(['admin', 'user'])
|
||||
})
|
||||
|
||||
export const UpdateUserResponse = zod.object({
|
||||
"id": zod.number(),
|
||||
"username": zod.string(),
|
||||
"email": zod.string().nullish(),
|
||||
"role": zod.enum(['admin', 'user']),
|
||||
"createdAt": zod.coerce.date()
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary Delete a user (admin only)
|
||||
*/
|
||||
export const DeleteUserParams = zod.object({
|
||||
"id": zod.coerce.number()
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary List audit log entries (admin only)
|
||||
*/
|
||||
export const ListAuditLogsQueryParams = zod.object({
|
||||
"entityType": zod.coerce.string().optional(),
|
||||
"entityId": zod.coerce.number().optional(),
|
||||
"limit": zod.coerce.number().optional()
|
||||
})
|
||||
|
||||
export const ListAuditLogsResponseItem = zod.object({
|
||||
"id": zod.number(),
|
||||
"entityType": zod.string(),
|
||||
"entityId": zod.number().nullish(),
|
||||
"action": zod.string(),
|
||||
"userId": zod.string(),
|
||||
"username": zod.string(),
|
||||
"changes": zod.string().nullish(),
|
||||
"createdAt": zod.coerce.date()
|
||||
})
|
||||
export const ListAuditLogsResponse = zod.array(ListAuditLogsResponseItem)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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 AuditLog {
|
||||
id: number;
|
||||
entityType: string;
|
||||
/** @nullable */
|
||||
entityId?: number | null;
|
||||
action: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
/** @nullable */
|
||||
changes?: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
import type { AuthModeMode } from './authModeMode';
|
||||
|
||||
export interface AuthMode {
|
||||
mode: AuthModeMode;
|
||||
}
|
||||
@@ -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 AuthModeMode = typeof AuthModeMode[keyof typeof AuthModeMode];
|
||||
|
||||
|
||||
export const AuthModeMode = {
|
||||
oidc: 'oidc',
|
||||
local: 'local',
|
||||
} as const;
|
||||
@@ -5,6 +5,7 @@
|
||||
* ToolRate API — Tool listing and rating platform
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import type { AuthUserRole } from './authUserRole';
|
||||
|
||||
export interface AuthUser {
|
||||
sub: string;
|
||||
@@ -14,4 +15,6 @@ export interface AuthUser {
|
||||
name?: string | null;
|
||||
/** @nullable */
|
||||
preferredUsername?: string | null;
|
||||
role?: AuthUserRole;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
|
||||
@@ -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 AuthUserRole = typeof AuthUserRole[keyof typeof AuthUserRole];
|
||||
|
||||
|
||||
export const AuthUserRole = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
} as const;
|
||||
@@ -7,15 +7,21 @@
|
||||
*/
|
||||
|
||||
export * from './analyticsSummary';
|
||||
export * from './auditLog';
|
||||
export * from './authMode';
|
||||
export * from './authModeMode';
|
||||
export * from './authUser';
|
||||
export * from './authUserRole';
|
||||
export * from './categoryStats';
|
||||
export * from './errorResponse';
|
||||
export * from './getRatingDistributionParams';
|
||||
export * from './getTopToolsMetric';
|
||||
export * from './getTopToolsParams';
|
||||
export * from './healthStatus';
|
||||
export * from './listAuditLogsParams';
|
||||
export * from './listToolsParams';
|
||||
export * from './listToolsSort';
|
||||
export * from './localLoginInput';
|
||||
export * from './rating';
|
||||
export * from './ratingDistribution';
|
||||
export * from './ratingInput';
|
||||
@@ -25,3 +31,9 @@ export * from './toolInput';
|
||||
export * from './toolUpdate';
|
||||
export * from './toolWithStats';
|
||||
export * from './topToolEntry';
|
||||
export * from './user';
|
||||
export * from './userCreateInput';
|
||||
export * from './userCreateInputRole';
|
||||
export * from './userRole';
|
||||
export * from './userRoleUpdate';
|
||||
export * from './userRoleUpdateRole';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 ListAuditLogsParams = {
|
||||
entityType?: string;
|
||||
entityId?: number;
|
||||
limit?: number;
|
||||
};
|
||||
@@ -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 LocalLoginInput {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
@@ -15,6 +15,8 @@ export interface Tool {
|
||||
websiteUrl?: string | null;
|
||||
/** @nullable */
|
||||
iconUrl?: string | null;
|
||||
/** @nullable */
|
||||
createdBy?: string | null;
|
||||
features?: string[];
|
||||
tags?: string[];
|
||||
createdAt: Date;
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface ToolWithStats {
|
||||
websiteUrl?: string | null;
|
||||
/** @nullable */
|
||||
iconUrl?: string | null;
|
||||
/** @nullable */
|
||||
createdBy?: string | null;
|
||||
features?: string[];
|
||||
tags?: string[];
|
||||
createdAt: Date;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* ToolRate API — Tool listing and rating platform
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import type { UserRole } from './userRole';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
/** @nullable */
|
||||
email?: string | null;
|
||||
role: UserRole;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* ToolRate API — Tool listing and rating platform
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import type { UserCreateInputRole } from './userCreateInputRole';
|
||||
|
||||
export interface UserCreateInput {
|
||||
/** @minLength 2 */
|
||||
username: string;
|
||||
/** @minLength 6 */
|
||||
password: string;
|
||||
email?: string;
|
||||
role?: UserCreateInputRole;
|
||||
}
|
||||
@@ -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 UserCreateInputRole = typeof UserCreateInputRole[keyof typeof UserCreateInputRole];
|
||||
|
||||
|
||||
export const UserCreateInputRole = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
} as const;
|
||||
@@ -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 UserRole = typeof UserRole[keyof typeof UserRole];
|
||||
|
||||
|
||||
export const UserRole = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
} as const;
|
||||
@@ -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
|
||||
*/
|
||||
import type { UserRoleUpdateRole } from './userRoleUpdateRole';
|
||||
|
||||
export interface UserRoleUpdate {
|
||||
role: UserRoleUpdateRole;
|
||||
}
|
||||
@@ -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 UserRoleUpdateRole = typeof UserRoleUpdateRole[keyof typeof UserRoleUpdateRole];
|
||||
|
||||
|
||||
export const UserRoleUpdateRole = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
} as const;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { pgTable, serial, timestamp, text, integer } from "drizzle-orm/pg-core";
|
||||
|
||||
export const auditLogsTable = pgTable("audit_logs", {
|
||||
id: serial("id").primaryKey(),
|
||||
entityType: text("entity_type").notNull(),
|
||||
entityId: integer("entity_id"),
|
||||
action: text("action").notNull(),
|
||||
userId: text("user_id").notNull(),
|
||||
username: text("username").notNull(),
|
||||
changes: text("changes"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type AuditLog = typeof auditLogsTable.$inferSelect;
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./tools";
|
||||
export * from "./ratings";
|
||||
export * from "./users";
|
||||
export * from "./audit-logs";
|
||||
|
||||
@@ -9,6 +9,7 @@ export const toolsTable = pgTable("tools", {
|
||||
category: text("category").notNull(),
|
||||
websiteUrl: text("website_url"),
|
||||
iconUrl: text("icon_url"),
|
||||
createdBy: text("created_by"),
|
||||
features: text("features").array().notNull().default([]),
|
||||
tags: text("tags").array().notNull().default([]),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { pgTable, text, serial, timestamp } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const usersTable = pgTable("users", {
|
||||
id: serial("id").primaryKey(),
|
||||
username: text("username").notNull().unique(),
|
||||
passwordHash: text("password_hash").notNull(),
|
||||
email: text("email"),
|
||||
role: text("role").notNull().default("user"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const insertUserSchema = createInsertSchema(usersTable).omit({ id: true, createdAt: true });
|
||||
export type InsertUser = z.infer<typeof insertUserSchema>;
|
||||
export type LocalUser = typeof usersTable.$inferSelect;
|
||||
Reference in New Issue
Block a user