feat(ADM-018): completed feature
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import type { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import type { Authenticate } from '../../../shared/auth.js';
|
||||
import { parseJson } from '../../../shared/http-input.js';
|
||||
import { AppError } from '../../../shared/errors.js';
|
||||
import type pg from 'pg';
|
||||
@@ -35,6 +36,8 @@ export interface IdentityRoutesDeps {
|
||||
/** Test seams; production uses defaults. */
|
||||
hasher?: PasswordHasher;
|
||||
rateLimiter?: LoginRateLimiter;
|
||||
/** Session authenticator. Created by createSessionAuthenticator in build-app.ts. */
|
||||
authenticate?: Authenticate;
|
||||
}
|
||||
|
||||
const credentialsSchema = z.object({
|
||||
@@ -104,6 +107,18 @@ export async function registerIdentityRoutes(
|
||||
clearSessionCookie(reply, cookieSecure);
|
||||
return reply.code(204).send();
|
||||
});
|
||||
|
||||
app.get('/auth/me', async (request, reply) => {
|
||||
try {
|
||||
const user = await deps.authenticate!(request);
|
||||
return reply.send({ id: user.id, email: user.email, role: user.role });
|
||||
} catch (error) {
|
||||
if (error instanceof AppError && error.statusCode === 401) {
|
||||
return reply.send({ user: null });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setSessionCookie(reply: FastifyReply, token: string, secure: boolean): void {
|
||||
|
||||
@@ -12,6 +12,10 @@ export interface PasswordHasher {
|
||||
export interface UserRepository {
|
||||
create(user: NewUser): Promise<User>;
|
||||
findByEmail(email: string): Promise<(User & { passwordHash: string }) | undefined>;
|
||||
findById(id: string): Promise<User | undefined>;
|
||||
listUsers(params?: { limit?: number; offset?: number; role?: string; q?: string }): Promise<{ items: User[]; total: number }>;
|
||||
updateUser(id: string, patch: { role?: string; passwordHash?: string }): Promise<User>;
|
||||
deleteUser(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface SessionRepository {
|
||||
|
||||
@@ -61,6 +61,66 @@ export class PgUserRepository implements UserRepository {
|
||||
passwordHash: row.password_hash,
|
||||
};
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<User | undefined> {
|
||||
const result = await this.pool.query<UserRow>(
|
||||
`SELECT id, email, role, created_at
|
||||
FROM identity_users WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
const row = result.rows[0];
|
||||
if (!row) return undefined;
|
||||
return { id: row.id, email: row.email, role: row.role, createdAt: row.created_at };
|
||||
}
|
||||
|
||||
async listUsers(params?: { limit?: number; offset?: number; role?: string; q?: string }): Promise<{ items: User[]; total: number }> {
|
||||
const limit = params?.limit ?? 20;
|
||||
const offset = params?.offset ?? 0;
|
||||
const conditions: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
let i = 1;
|
||||
if (params?.role) { conditions.push(`role = $${i++}`); values.push(params.role); }
|
||||
if (params?.q) { conditions.push(`(email ILIKE $${i++} OR role ILIKE $${i++})`); values.push(`%${params.q}%`); values.push(`%${params.q}%`); }
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const countResult = await this.pool.query<{ count: string }>(
|
||||
`SELECT COUNT(*) FROM identity_users ${where}`,
|
||||
values,
|
||||
);
|
||||
const total = parseInt(countResult.rows[0]?.count ?? '0', 10);
|
||||
const rows = await this.pool.query<UserRow>(
|
||||
`SELECT id, email, role, created_at FROM identity_users ${where} ORDER BY created_at DESC LIMIT $${i++} OFFSET $${i}`,
|
||||
[...values, limit, offset],
|
||||
);
|
||||
return {
|
||||
items: rows.rows.map((r) => ({ id: r.id, email: r.email, role: r.role, createdAt: r.created_at })),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
async updateUser(id: string, patch: { role?: string; passwordHash?: string }): Promise<User> {
|
||||
const sets: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
let i = 1;
|
||||
if (patch.role) { sets.push(`role = $${i++}`); values.push(patch.role); }
|
||||
if (patch.passwordHash) { sets.push(`password_hash = $${i++}`); values.push(patch.passwordHash); }
|
||||
if (!sets.length) {
|
||||
const existing = await this.findById(id);
|
||||
if (!existing) throw new Error('User not found');
|
||||
return existing;
|
||||
}
|
||||
values.push(id);
|
||||
const result = await this.pool.query<UserRow>(
|
||||
`UPDATE identity_users SET ${sets.join(', ')} WHERE id = $${i} RETURNING id, email, role, created_at`,
|
||||
values,
|
||||
);
|
||||
const row = result.rows[0];
|
||||
if (!row) throw new Error('User not found');
|
||||
return { id: row.id, email: row.email, role: row.role, createdAt: row.created_at };
|
||||
}
|
||||
|
||||
async deleteUser(id: string): Promise<void> {
|
||||
await this.pool.query(`DELETE FROM identity_users WHERE id = $1`, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
function isPgError(error: unknown): error is { code: string } {
|
||||
|
||||
Reference in New Issue
Block a user