264 lines
9.2 KiB
TypeScript
264 lines
9.2 KiB
TypeScript
|
|
import * as plugins from '../../plugins.ts';
|
||
|
|
import * as interfaces from '../../../ts_interfaces/index.ts';
|
||
|
|
import type { OpsServer } from '../classes.opsserver.ts';
|
||
|
|
import { AuthService } from '../../services/auth.service.ts';
|
||
|
|
import { User } from '../../models/user.ts';
|
||
|
|
import { AuthProvider, PlatformSettings } from '../../models/index.ts';
|
||
|
|
|
||
|
|
export class AuthHandler {
|
||
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||
|
|
private authService: AuthService;
|
||
|
|
|
||
|
|
constructor(private opsServerRef: OpsServer) {
|
||
|
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||
|
|
this.authService = new AuthService();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize auth handler - must be called after construction
|
||
|
|
*/
|
||
|
|
public async initialize(): Promise<void> {
|
||
|
|
this.registerHandlers();
|
||
|
|
}
|
||
|
|
|
||
|
|
private registerHandlers(): void {
|
||
|
|
// Login
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_Login>(
|
||
|
|
'login',
|
||
|
|
async (dataArg) => {
|
||
|
|
try {
|
||
|
|
const { email, password } = dataArg;
|
||
|
|
|
||
|
|
if (!email || !password) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Email and password are required');
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await this.authService.login(email, password);
|
||
|
|
|
||
|
|
if (!result.success || !result.user || !result.accessToken || !result.refreshToken) {
|
||
|
|
return {
|
||
|
|
errorCode: result.errorCode,
|
||
|
|
errorMessage: result.errorMessage,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const user = result.user;
|
||
|
|
const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
|
||
|
|
|
||
|
|
const identity: interfaces.data.IIdentity = {
|
||
|
|
jwt: result.accessToken,
|
||
|
|
refreshJwt: result.refreshToken,
|
||
|
|
userId: user.id,
|
||
|
|
email: user.email,
|
||
|
|
username: user.username,
|
||
|
|
displayName: user.displayName,
|
||
|
|
isSystemAdmin: user.isSystemAdmin,
|
||
|
|
expiresAt,
|
||
|
|
sessionId: result.sessionId!,
|
||
|
|
};
|
||
|
|
|
||
|
|
return {
|
||
|
|
identity,
|
||
|
|
user: {
|
||
|
|
id: user.id,
|
||
|
|
email: user.email,
|
||
|
|
username: user.username,
|
||
|
|
displayName: user.displayName,
|
||
|
|
avatarUrl: user.avatarUrl,
|
||
|
|
isSystemAdmin: user.isSystemAdmin,
|
||
|
|
isActive: user.isActive,
|
||
|
|
createdAt: user.createdAt instanceof Date
|
||
|
|
? user.createdAt.toISOString()
|
||
|
|
: String(user.createdAt),
|
||
|
|
lastLoginAt: user.lastLoginAt instanceof Date
|
||
|
|
? user.lastLoginAt.toISOString()
|
||
|
|
: user.lastLoginAt
|
||
|
|
? String(user.lastLoginAt)
|
||
|
|
: undefined,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Login failed');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Refresh Token
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RefreshToken>(
|
||
|
|
'refreshToken',
|
||
|
|
async (dataArg) => {
|
||
|
|
try {
|
||
|
|
if (!dataArg.identity?.refreshJwt) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Refresh token is required');
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await this.authService.refresh(dataArg.identity.refreshJwt);
|
||
|
|
|
||
|
|
if (!result.success || !result.user || !result.accessToken) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError(
|
||
|
|
result.errorMessage || 'Token refresh failed',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const user = result.user;
|
||
|
|
const expiresAt = Date.now() + 15 * 60 * 1000;
|
||
|
|
|
||
|
|
return {
|
||
|
|
identity: {
|
||
|
|
jwt: result.accessToken,
|
||
|
|
refreshJwt: dataArg.identity.refreshJwt,
|
||
|
|
userId: user.id,
|
||
|
|
email: user.email,
|
||
|
|
username: user.username,
|
||
|
|
displayName: user.displayName,
|
||
|
|
isSystemAdmin: user.isSystemAdmin,
|
||
|
|
expiresAt,
|
||
|
|
sessionId: result.sessionId || dataArg.identity.sessionId,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Token refresh failed');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Logout
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_Logout>(
|
||
|
|
'logout',
|
||
|
|
async (dataArg) => {
|
||
|
|
try {
|
||
|
|
if (!dataArg.identity?.jwt) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Identity required');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (dataArg.all) {
|
||
|
|
const count = await this.authService.logoutAll(dataArg.identity.userId);
|
||
|
|
return { message: `Logged out from ${count} sessions` };
|
||
|
|
}
|
||
|
|
|
||
|
|
const sessionId = dataArg.sessionId || dataArg.identity.sessionId;
|
||
|
|
if (sessionId) {
|
||
|
|
await this.authService.logout(sessionId, {
|
||
|
|
userId: dataArg.identity.userId,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return { message: 'Logged out successfully' };
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Logout failed');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Get Me
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetMe>(
|
||
|
|
'getMe',
|
||
|
|
async (dataArg) => {
|
||
|
|
try {
|
||
|
|
if (!dataArg.identity?.jwt) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Identity required');
|
||
|
|
}
|
||
|
|
|
||
|
|
const validated = await this.authService.validateAccessToken(dataArg.identity.jwt);
|
||
|
|
if (!validated) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Invalid or expired token');
|
||
|
|
}
|
||
|
|
|
||
|
|
const user = validated.user;
|
||
|
|
return {
|
||
|
|
user: {
|
||
|
|
id: user.id,
|
||
|
|
email: user.email,
|
||
|
|
username: user.username,
|
||
|
|
displayName: user.displayName,
|
||
|
|
avatarUrl: user.avatarUrl,
|
||
|
|
isSystemAdmin: user.isSystemAdmin,
|
||
|
|
isActive: user.isActive,
|
||
|
|
createdAt: user.createdAt instanceof Date
|
||
|
|
? user.createdAt.toISOString()
|
||
|
|
: String(user.createdAt),
|
||
|
|
lastLoginAt: user.lastLoginAt instanceof Date
|
||
|
|
? user.lastLoginAt.toISOString()
|
||
|
|
: user.lastLoginAt
|
||
|
|
? String(user.lastLoginAt)
|
||
|
|
: undefined,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Failed to get user info');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Get Auth Providers (public)
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAuthProviders>(
|
||
|
|
'getAuthProviders',
|
||
|
|
async (_dataArg) => {
|
||
|
|
try {
|
||
|
|
const settings = await PlatformSettings.get();
|
||
|
|
const providers = await AuthProvider.getActiveProviders();
|
||
|
|
|
||
|
|
return {
|
||
|
|
providers: providers.map((p) => p.toPublicInfo()),
|
||
|
|
localAuthEnabled: settings.auth.localAuthEnabled,
|
||
|
|
defaultProviderId: settings.auth.defaultProviderId,
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Failed to get auth providers');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Guard for valid identity - validates JWT via AuthService
|
||
|
|
public validIdentityGuard = new plugins.smartguard.Guard<{
|
||
|
|
identity: interfaces.data.IIdentity;
|
||
|
|
}>(
|
||
|
|
async (dataArg) => {
|
||
|
|
if (!dataArg.identity?.jwt) return false;
|
||
|
|
try {
|
||
|
|
const validated = await this.authService.validateAccessToken(dataArg.identity.jwt);
|
||
|
|
if (!validated) return false;
|
||
|
|
// Verify the userId matches the identity claim
|
||
|
|
if (dataArg.identity.userId !== validated.user.id) return false;
|
||
|
|
return true;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{ failedHint: 'identity is not valid', name: 'validIdentityGuard' },
|
||
|
|
);
|
||
|
|
|
||
|
|
// Guard for admin identity - validates JWT + checks isSystemAdmin
|
||
|
|
public adminIdentityGuard = new plugins.smartguard.Guard<{
|
||
|
|
identity: interfaces.data.IIdentity;
|
||
|
|
}>(
|
||
|
|
async (dataArg) => {
|
||
|
|
const isValid = await this.validIdentityGuard.exec(dataArg);
|
||
|
|
if (!isValid) return false;
|
||
|
|
// Check isSystemAdmin claim from the identity
|
||
|
|
if (!dataArg.identity.isSystemAdmin) return false;
|
||
|
|
// Double-check from database
|
||
|
|
const user = await User.findById(dataArg.identity.userId);
|
||
|
|
if (!user || !user.isSystemAdmin) return false;
|
||
|
|
return true;
|
||
|
|
},
|
||
|
|
{ failedHint: 'user is not admin', name: 'adminIdentityGuard' },
|
||
|
|
);
|
||
|
|
}
|