ref: extract authentication and remove db query in it, add prom metric collection
This commit is contained in:
parent
1224039d8b
commit
84afe7c3ad
@ -16,6 +16,10 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app: handy-server
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "9090"
|
||||
prometheus.io/path: "/metrics"
|
||||
spec:
|
||||
containers:
|
||||
- name: handy
|
||||
|
@ -11,6 +11,7 @@ import { onShutdown } from "@/utils/shutdown";
|
||||
import { allocateSessionSeq, allocateUserSeq } from "@/services/seq";
|
||||
import { randomKeyNaked } from "@/utils/randomKeyNaked";
|
||||
import { AsyncLock } from "@/utils/lock";
|
||||
import { auth } from "@/modules/auth";
|
||||
import {
|
||||
EventRouter,
|
||||
ClientConnection,
|
||||
@ -40,7 +41,7 @@ import { activityCache } from "@/modules/sessionCache";
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
user: Account;
|
||||
userId: string;
|
||||
}
|
||||
interface FastifyInstance {
|
||||
authenticate: any;
|
||||
@ -52,14 +53,6 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
|
||||
// Configure
|
||||
log('Starting API...');
|
||||
const tokenGenerator = await privacyKit.createPersistentTokenGenerator({
|
||||
service: 'handy',
|
||||
seed: process.env.HANDY_MASTER_SECRET!
|
||||
});
|
||||
const tokenVerifier = await privacyKit.createPersistentTokenVerifier({
|
||||
service: 'handy',
|
||||
publicKey: tokenGenerator.publicKey
|
||||
});
|
||||
|
||||
// Start API
|
||||
const app = fastify({
|
||||
@ -89,25 +82,14 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const verified = await tokenVerifier.verify(token);
|
||||
const verified = await auth.verifyToken(token);
|
||||
if (!verified) {
|
||||
log({ module: 'auth-decorator' }, `Auth failed - invalid token`);
|
||||
return reply.code(401).send({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await db.account.findUnique({
|
||||
where: { id: verified.user as string }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
log({ module: 'auth-decorator' }, `Auth failed - user not found: ${verified.user}`);
|
||||
return reply.code(401).send({ error: 'User not found' });
|
||||
}
|
||||
|
||||
log({ module: 'auth-decorator' }, `Auth success - user: ${user.id}`);
|
||||
|
||||
request.user = user;
|
||||
log({ module: 'auth-decorator' }, `Auth success - user: ${verified.userId}`);
|
||||
request.userId = verified.userId;
|
||||
} catch (error) {
|
||||
return reply.code(401).send({ error: 'Authentication failed' });
|
||||
}
|
||||
@ -144,7 +126,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
token: await tokenGenerator.new({ user: user.id })
|
||||
token: await auth.createToken(user.id)
|
||||
});
|
||||
});
|
||||
|
||||
@ -183,7 +165,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
});
|
||||
|
||||
if (answer.response && answer.responseAccountId) {
|
||||
const token = await tokenGenerator.new({ user: answer.responseAccountId!, extras: { session: answer.id } });
|
||||
const token = await auth.createToken(answer.responseAccountId!, { session: answer.id });
|
||||
return reply.send({
|
||||
state: 'authorized',
|
||||
token: token,
|
||||
@ -204,7 +186,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
log({ module: 'auth-response' }, `Auth response endpoint hit - user: ${request.user?.id || 'NO USER'}, publicKey: ${request.body.publicKey.substring(0, 20)}...`);
|
||||
log({ module: 'auth-response' }, `Auth response endpoint hit - user: ${request.userId}, publicKey: ${request.body.publicKey.substring(0, 20)}...`);
|
||||
const publicKey = privacyKit.decodeBase64(request.body.publicKey);
|
||||
const isValid = tweetnacl.box.publicKeyLength === publicKey.length;
|
||||
if (!isValid) {
|
||||
@ -229,7 +211,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
if (!authRequest.response) {
|
||||
await db.terminalAuthRequest.update({
|
||||
where: { id: authRequest.id },
|
||||
data: { response: request.body.response, responseAccountId: request.user.id }
|
||||
data: { response: request.body.response, responseAccountId: request.userId }
|
||||
});
|
||||
}
|
||||
return reply.send({ success: true });
|
||||
@ -268,7 +250,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
});
|
||||
|
||||
if (answer.response && answer.responseAccountId) {
|
||||
const token = await tokenGenerator.new({ user: answer.responseAccountId! });
|
||||
const token = await auth.createToken(answer.responseAccountId!);
|
||||
return reply.send({
|
||||
state: 'authorized',
|
||||
token: token,
|
||||
@ -303,7 +285,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
if (!authRequest.response) {
|
||||
await db.accountAuthRequest.update({
|
||||
where: { id: authRequest.id },
|
||||
data: { response: request.body.response, responseAccountId: request.user.id }
|
||||
data: { response: request.body.response, responseAccountId: request.userId }
|
||||
});
|
||||
}
|
||||
return reply.send({ success: true });
|
||||
@ -372,7 +354,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
typed.get('/v1/sessions', {
|
||||
preHandler: app.authenticate,
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
|
||||
const sessions = await db.session.findMany({
|
||||
where: { accountId: userId },
|
||||
@ -443,7 +425,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
const { tag, metadata } = request.body;
|
||||
|
||||
const session = await db.session.findFirst({
|
||||
@ -535,7 +517,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
const { token } = request.body;
|
||||
|
||||
try {
|
||||
@ -578,7 +560,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
const { token } = request.params;
|
||||
|
||||
try {
|
||||
@ -599,7 +581,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
typed.get('/v1/push-tokens', {
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
|
||||
try {
|
||||
const tokens = await db.accountPushToken.findMany({
|
||||
@ -640,9 +622,18 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
try {
|
||||
const user = await db.account.findUnique({
|
||||
where: { id: request.userId },
|
||||
select: { settings: true, settingsVersion: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return reply.code(500).send({ error: 'Failed to get account settings' });
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
settings: request.user.settings,
|
||||
settingsVersion: request.user.settingsVersion
|
||||
settings: user.settings,
|
||||
settingsVersion: user.settingsVersion
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({ error: 'Failed to get account settings' });
|
||||
@ -674,17 +665,30 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
const { settings, expectedVersion } = request.body;
|
||||
|
||||
try {
|
||||
// Get current user data for version check
|
||||
const currentUser = await db.account.findUnique({
|
||||
where: { id: userId },
|
||||
select: { settings: true, settingsVersion: true }
|
||||
});
|
||||
|
||||
if (!currentUser) {
|
||||
return reply.code(500).send({
|
||||
success: false,
|
||||
error: 'Failed to update account settings'
|
||||
});
|
||||
}
|
||||
|
||||
// Check current version
|
||||
if (request.user.settingsVersion !== expectedVersion) {
|
||||
if (currentUser.settingsVersion !== expectedVersion) {
|
||||
return reply.code(200).send({
|
||||
success: false,
|
||||
error: 'version-mismatch',
|
||||
currentVersion: request.user.settingsVersion,
|
||||
currentSettings: request.user.settings
|
||||
currentVersion: currentUser.settingsVersion,
|
||||
currentSettings: currentUser.settings
|
||||
});
|
||||
}
|
||||
|
||||
@ -754,7 +758,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
const { sessionId, startTime, endTime, groupBy } = request.body;
|
||||
const actualGroupBy = groupBy || 'day';
|
||||
|
||||
@ -886,7 +890,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
const { sessionId } = request.params;
|
||||
|
||||
// Verify session belongs to user
|
||||
@ -940,7 +944,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
const { id, metadata, daemonState } = request.body;
|
||||
|
||||
// Check if machine exists (like sessions do)
|
||||
@ -1017,7 +1021,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
typed.get('/v1/machines', {
|
||||
preHandler: app.authenticate,
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
|
||||
const machines = await db.machine.findMany({
|
||||
where: { accountId: userId },
|
||||
@ -1047,7 +1051,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const userId = request.userId;
|
||||
const { id } = request.params;
|
||||
|
||||
const machine = await db.machine.findFirst({
|
||||
@ -1195,7 +1199,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
return;
|
||||
}
|
||||
|
||||
const verified = await tokenVerifier.verify(token);
|
||||
const verified = await auth.verifyToken(token);
|
||||
if (!verified) {
|
||||
log({ module: 'websocket' }, `Invalid token provided`);
|
||||
socket.emit('error', { message: 'Invalid authentication token' });
|
||||
@ -1203,7 +1207,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = verified.user as string;
|
||||
const userId = verified.userId;
|
||||
log({ module: 'websocket' }, `Token verified: ${userId}, clientType: ${clientType || 'user-scoped'}, sessionId: ${sessionId || 'none'}, machineId: ${machineId || 'none'}, socketId: ${socket.id}`);
|
||||
|
||||
// Store connection based on type
|
||||
|
@ -6,6 +6,7 @@ import { startTimeout } from "./app/timeout";
|
||||
import { redis } from "./services/redis";
|
||||
import { startMetricsServer } from "@/app/metrics";
|
||||
import { activityCache } from "@/modules/sessionCache";
|
||||
import { auth } from "./modules/auth";
|
||||
|
||||
async function main() {
|
||||
|
||||
@ -19,6 +20,9 @@ async function main() {
|
||||
});
|
||||
await redis.ping();
|
||||
|
||||
// Initialize auth module
|
||||
await auth.init();
|
||||
|
||||
//
|
||||
// Start
|
||||
//
|
||||
|
146
sources/modules/auth.ts
Normal file
146
sources/modules/auth.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import * as privacyKit from "privacy-kit";
|
||||
import { log } from "@/utils/log";
|
||||
|
||||
interface TokenCacheEntry {
|
||||
userId: string;
|
||||
extras?: any;
|
||||
cachedAt: number;
|
||||
}
|
||||
|
||||
interface AuthTokens {
|
||||
generator: Awaited<ReturnType<typeof privacyKit.createPersistentTokenGenerator>>;
|
||||
verifier: Awaited<ReturnType<typeof privacyKit.createPersistentTokenVerifier>>;
|
||||
}
|
||||
|
||||
class AuthModule {
|
||||
private tokenCache = new Map<string, TokenCacheEntry>();
|
||||
private tokens: AuthTokens | null = null;
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.tokens) {
|
||||
return; // Already initialized
|
||||
}
|
||||
|
||||
log({ module: 'auth' }, 'Initializing auth module...');
|
||||
|
||||
const generator = await privacyKit.createPersistentTokenGenerator({
|
||||
service: 'handy',
|
||||
seed: process.env.HANDY_MASTER_SECRET!
|
||||
});
|
||||
|
||||
const verifier = await privacyKit.createPersistentTokenVerifier({
|
||||
service: 'handy',
|
||||
publicKey: generator.publicKey
|
||||
});
|
||||
|
||||
this.tokens = { generator, verifier };
|
||||
|
||||
log({ module: 'auth' }, 'Auth module initialized');
|
||||
}
|
||||
|
||||
async createToken(userId: string, extras?: any): Promise<string> {
|
||||
if (!this.tokens) {
|
||||
throw new Error('Auth module not initialized');
|
||||
}
|
||||
|
||||
const payload: any = { user: userId };
|
||||
if (extras) {
|
||||
payload.extras = extras;
|
||||
}
|
||||
|
||||
const token = await this.tokens.generator.new(payload);
|
||||
|
||||
// Cache the token immediately
|
||||
this.tokenCache.set(token, {
|
||||
userId,
|
||||
extras,
|
||||
cachedAt: Date.now()
|
||||
});
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
async verifyToken(token: string): Promise<{ userId: string; extras?: any } | null> {
|
||||
// Check cache first
|
||||
const cached = this.tokenCache.get(token);
|
||||
if (cached) {
|
||||
return {
|
||||
userId: cached.userId,
|
||||
extras: cached.extras
|
||||
};
|
||||
}
|
||||
|
||||
// Cache miss - verify token
|
||||
if (!this.tokens) {
|
||||
throw new Error('Auth module not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
const verified = await this.tokens.verifier.verify(token);
|
||||
if (!verified) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userId = verified.user as string;
|
||||
const extras = verified.extras;
|
||||
|
||||
// Cache the result permanently
|
||||
this.tokenCache.set(token, {
|
||||
userId,
|
||||
extras,
|
||||
cachedAt: Date.now()
|
||||
});
|
||||
|
||||
return { userId, extras };
|
||||
|
||||
} catch (error) {
|
||||
log({ module: 'auth', level: 'error' }, `Token verification failed: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
invalidateUserTokens(userId: string): void {
|
||||
// Remove all tokens for a specific user
|
||||
// This is expensive but rarely needed
|
||||
for (const [token, entry] of this.tokenCache.entries()) {
|
||||
if (entry.userId === userId) {
|
||||
this.tokenCache.delete(token);
|
||||
}
|
||||
}
|
||||
|
||||
log({ module: 'auth' }, `Invalidated tokens for user: ${userId}`);
|
||||
}
|
||||
|
||||
invalidateToken(token: string): void {
|
||||
this.tokenCache.delete(token);
|
||||
}
|
||||
|
||||
getCacheStats(): { size: number; oldestEntry: number | null } {
|
||||
if (this.tokenCache.size === 0) {
|
||||
return { size: 0, oldestEntry: null };
|
||||
}
|
||||
|
||||
let oldest = Date.now();
|
||||
for (const entry of this.tokenCache.values()) {
|
||||
if (entry.cachedAt < oldest) {
|
||||
oldest = entry.cachedAt;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
size: this.tokenCache.size,
|
||||
oldestEntry: oldest
|
||||
};
|
||||
}
|
||||
|
||||
// Cleanup old entries (optional - can be called periodically)
|
||||
cleanup(): void {
|
||||
// Note: Since tokens are cached "forever" as requested,
|
||||
// we don't do automatic cleanup. This method exists if needed later.
|
||||
const stats = this.getCacheStats();
|
||||
log({ module: 'auth' }, `Token cache size: ${stats.size} entries`);
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
export const auth = new AuthModule();
|
Loading…
Reference in New Issue
Block a user