phase-10.5: market screener ui enhancements
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* AuthController — HTTP layer for authentication.
|
||||
*
|
||||
* POST /auth/register — create account (requires invite code generated at boot)
|
||||
* POST /auth/login — verify credentials, returns JWT
|
||||
*/
|
||||
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { AuthService } from './AuthService.js';
|
||||
|
||||
interface RegisterBody {
|
||||
email: string;
|
||||
password: string;
|
||||
inviteCode: string;
|
||||
role?: 'trader' | 'viewer';
|
||||
}
|
||||
|
||||
interface LoginBody {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface ForgotBody {
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ResetBody {
|
||||
token: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const registerSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['email', 'password', 'inviteCode'],
|
||||
properties: {
|
||||
email: { type: 'string', format: 'email' },
|
||||
password: { type: 'string', minLength: 8 },
|
||||
inviteCode: { type: 'string' },
|
||||
role: { type: 'string', enum: ['trader', 'viewer'] },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const loginSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['email', 'password'],
|
||||
properties: {
|
||||
email: { type: 'string', format: 'email' },
|
||||
password: { type: 'string' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const forgotSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['email'],
|
||||
properties: {
|
||||
email: { type: 'string', format: 'email' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resetSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['token', 'password'],
|
||||
properties: {
|
||||
token: { type: 'string', minLength: 32 },
|
||||
password: { type: 'string', minLength: 8 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export class AuthController {
|
||||
readonly #inviteCode: string;
|
||||
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
inviteCode: string,
|
||||
) {
|
||||
this.#inviteCode = inviteCode;
|
||||
}
|
||||
|
||||
register(app: FastifyInstance): void {
|
||||
app.post('/auth/register', { schema: registerSchema }, this.#register.bind(this));
|
||||
app.post('/auth/login', { schema: loginSchema }, this.#login.bind(this));
|
||||
app.post('/auth/forgot-password', { schema: forgotSchema }, this.#forgot.bind(this));
|
||||
app.post('/auth/reset-password', { schema: resetSchema }, this.#reset.bind(this));
|
||||
}
|
||||
|
||||
async #register(req: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||
const { email, password, inviteCode, role } = req.body as RegisterBody;
|
||||
|
||||
if (inviteCode !== this.#inviteCode) {
|
||||
return reply.code(403).send({ error: 'Invalid invite code' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = this.authService.register(email, password, role ?? 'viewer');
|
||||
reply.code(201).send(result);
|
||||
} catch (err: unknown) {
|
||||
const e = err as { message: string; statusCode?: number };
|
||||
reply.code(e.statusCode ?? 500).send({ error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
async #login(req: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||
const { email, password } = req.body as LoginBody;
|
||||
try {
|
||||
const result = this.authService.login(email, password);
|
||||
reply.send(result);
|
||||
} catch (err: unknown) {
|
||||
const e = err as { message: string; statusCode?: number };
|
||||
reply.code(e.statusCode ?? 500).send({ error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
async #forgot(req: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||
const { email } = req.body as ForgotBody;
|
||||
const origin = process.env.CLIENT_ORIGIN ?? 'http://localhost:5173';
|
||||
try {
|
||||
this.authService.forgotPassword(email, origin);
|
||||
} catch (err) {
|
||||
// Log server-side but never expose details to client
|
||||
console.error('[forgot-password] error:', err);
|
||||
}
|
||||
// Always return 200 — never reveal whether the email exists or any error occurred
|
||||
reply.send({
|
||||
message: 'If that email is registered, a reset link has been printed to the server console.',
|
||||
});
|
||||
}
|
||||
|
||||
async #reset(req: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||
const { token, password } = req.body as ResetBody;
|
||||
try {
|
||||
this.authService.resetPassword(token, password);
|
||||
reply.send({ message: 'Password updated. You can now log in.' });
|
||||
} catch (err: unknown) {
|
||||
const e = err as { message: string; statusCode?: number };
|
||||
reply.code(e.statusCode ?? 500).send({ error: e.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user