feat(platform-service): add plan field to auth UserDoc + auth responses
- Added plan to auth UserDoc model and token payload typing - Register flow initializes user.plan from product default plan - Login/Register/Me responses now include user.plan - Access tokens now include optional plan claim - Verified: tsc --noEmit clean, 19 test files / 178 tests passing
This commit is contained in:
parent
0fee7e9ee7
commit
a9ac953ed1
@ -16,6 +16,7 @@ export async function createAccessToken(payload: {
|
||||
email: string;
|
||||
role: string;
|
||||
productId: string;
|
||||
plan?: 'free' | 'pro' | 'enterprise';
|
||||
}): Promise<string> {
|
||||
return new SignJWT({ ...payload, type: 'access' })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
@ -42,6 +43,7 @@ export async function verifyToken(token: string): Promise<{
|
||||
email?: string;
|
||||
role?: string;
|
||||
productId?: string;
|
||||
plan?: 'free' | 'pro' | 'enterprise';
|
||||
type?: string;
|
||||
}> {
|
||||
const { payload } = await jwtVerify(token, getSecret(), {
|
||||
@ -52,6 +54,7 @@ export async function verifyToken(token: string): Promise<{
|
||||
email?: string;
|
||||
role?: string;
|
||||
productId?: string;
|
||||
plan?: 'free' | 'pro' | 'enterprise';
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { getRequestProductConfig } from '../../lib/request-context.js';
|
||||
import { BadRequestError, UnauthorizedError } from '../../lib/errors.js';
|
||||
import * as repo from './repository.js';
|
||||
import * as jwt from './jwt.js';
|
||||
@ -36,13 +37,20 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
productId,
|
||||
plan: user.plan,
|
||||
});
|
||||
const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user: { id: user.id, email: user.email, role: user.role, displayName: user.displayName },
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
plan: user.plan,
|
||||
displayName: user.displayName,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -53,6 +61,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const { email, password, displayName, role, productId } = parsed.data;
|
||||
const product = getRequestProductConfig(req);
|
||||
|
||||
const existing = await repo.getByEmail(email, productId);
|
||||
if (existing) throw new BadRequestError('Email already registered');
|
||||
@ -63,6 +72,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
productId,
|
||||
email: email.toLowerCase(),
|
||||
passwordHash: await repo.hashPassword(password),
|
||||
plan: product.defaultPlan,
|
||||
role,
|
||||
displayName,
|
||||
status: 'active',
|
||||
@ -77,6 +87,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
productId,
|
||||
plan: user.plan,
|
||||
});
|
||||
const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });
|
||||
|
||||
@ -84,7 +95,13 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user: { id: user.id, email: user.email, role: user.role, displayName: user.displayName },
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
plan: user.plan,
|
||||
displayName: user.displayName,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -106,6 +123,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
productId: user.productId,
|
||||
plan: user.plan,
|
||||
});
|
||||
return { accessToken };
|
||||
} catch {
|
||||
@ -123,7 +141,13 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
const payload = await jwt.verifyToken(token);
|
||||
const user = await repo.getById(payload.sub);
|
||||
if (!user) throw new UnauthorizedError('User not found');
|
||||
return { id: user.id, email: user.email, role: user.role, displayName: user.displayName };
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
plan: user.plan,
|
||||
displayName: user.displayName,
|
||||
};
|
||||
} catch {
|
||||
throw new UnauthorizedError('Invalid or expired token');
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ export interface UserDoc {
|
||||
productId: string;
|
||||
email: string;
|
||||
passwordHash: string;
|
||||
plan: 'free' | 'pro' | 'enterprise';
|
||||
role: 'super_admin' | 'admin' | 'viewer' | 'user';
|
||||
displayName: string;
|
||||
status: 'active' | 'disabled';
|
||||
@ -22,6 +23,7 @@ export interface TokenPayload {
|
||||
email: string;
|
||||
role: string;
|
||||
productId: string;
|
||||
plan?: 'free' | 'pro' | 'enterprise';
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user