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;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
productId: string;
|
productId: string;
|
||||||
|
plan?: 'free' | 'pro' | 'enterprise';
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
return new SignJWT({ ...payload, type: 'access' })
|
return new SignJWT({ ...payload, type: 'access' })
|
||||||
.setProtectedHeader({ alg: 'HS256' })
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
@ -42,6 +43,7 @@ export async function verifyToken(token: string): Promise<{
|
|||||||
email?: string;
|
email?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
productId?: string;
|
productId?: string;
|
||||||
|
plan?: 'free' | 'pro' | 'enterprise';
|
||||||
type?: string;
|
type?: string;
|
||||||
}> {
|
}> {
|
||||||
const { payload } = await jwtVerify(token, getSecret(), {
|
const { payload } = await jwtVerify(token, getSecret(), {
|
||||||
@ -52,6 +54,7 @@ export async function verifyToken(token: string): Promise<{
|
|||||||
email?: string;
|
email?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
productId?: string;
|
productId?: string;
|
||||||
|
plan?: 'free' | 'pro' | 'enterprise';
|
||||||
type?: string;
|
type?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
import { getRequestProductConfig } from '../../lib/request-context.js';
|
||||||
import { BadRequestError, UnauthorizedError } from '../../lib/errors.js';
|
import { BadRequestError, UnauthorizedError } from '../../lib/errors.js';
|
||||||
import * as repo from './repository.js';
|
import * as repo from './repository.js';
|
||||||
import * as jwt from './jwt.js';
|
import * as jwt from './jwt.js';
|
||||||
@ -36,13 +37,20 @@ export async function authRoutes(app: FastifyInstance) {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
productId,
|
productId,
|
||||||
|
plan: user.plan,
|
||||||
});
|
});
|
||||||
const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });
|
const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
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('; '));
|
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||||
}
|
}
|
||||||
const { email, password, displayName, role, productId } = parsed.data;
|
const { email, password, displayName, role, productId } = parsed.data;
|
||||||
|
const product = getRequestProductConfig(req);
|
||||||
|
|
||||||
const existing = await repo.getByEmail(email, productId);
|
const existing = await repo.getByEmail(email, productId);
|
||||||
if (existing) throw new BadRequestError('Email already registered');
|
if (existing) throw new BadRequestError('Email already registered');
|
||||||
@ -63,6 +72,7 @@ export async function authRoutes(app: FastifyInstance) {
|
|||||||
productId,
|
productId,
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
passwordHash: await repo.hashPassword(password),
|
passwordHash: await repo.hashPassword(password),
|
||||||
|
plan: product.defaultPlan,
|
||||||
role,
|
role,
|
||||||
displayName,
|
displayName,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
@ -77,6 +87,7 @@ export async function authRoutes(app: FastifyInstance) {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
productId,
|
productId,
|
||||||
|
plan: user.plan,
|
||||||
});
|
});
|
||||||
const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });
|
const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });
|
||||||
|
|
||||||
@ -84,7 +95,13 @@ export async function authRoutes(app: FastifyInstance) {
|
|||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
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,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
productId: user.productId,
|
productId: user.productId,
|
||||||
|
plan: user.plan,
|
||||||
});
|
});
|
||||||
return { accessToken };
|
return { accessToken };
|
||||||
} catch {
|
} catch {
|
||||||
@ -123,7 +141,13 @@ export async function authRoutes(app: FastifyInstance) {
|
|||||||
const payload = await jwt.verifyToken(token);
|
const payload = await jwt.verifyToken(token);
|
||||||
const user = await repo.getById(payload.sub);
|
const user = await repo.getById(payload.sub);
|
||||||
if (!user) throw new UnauthorizedError('User not found');
|
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 {
|
} catch {
|
||||||
throw new UnauthorizedError('Invalid or expired token');
|
throw new UnauthorizedError('Invalid or expired token');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export interface UserDoc {
|
|||||||
productId: string;
|
productId: string;
|
||||||
email: string;
|
email: string;
|
||||||
passwordHash: string;
|
passwordHash: string;
|
||||||
|
plan: 'free' | 'pro' | 'enterprise';
|
||||||
role: 'super_admin' | 'admin' | 'viewer' | 'user';
|
role: 'super_admin' | 'admin' | 'viewer' | 'user';
|
||||||
displayName: string;
|
displayName: string;
|
||||||
status: 'active' | 'disabled';
|
status: 'active' | 'disabled';
|
||||||
@ -22,6 +23,7 @@ export interface TokenPayload {
|
|||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
productId: string;
|
productId: string;
|
||||||
|
plan?: 'free' | 'pro' | 'enterprise';
|
||||||
iat: number;
|
iat: number;
|
||||||
exp: number;
|
exp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user