feat(opsserver,web): replace the Angular UI and REST management layer with a TypedRequest-based ops server and bundled web frontend
This commit is contained in:
263
ts/opsserver/handlers/auth.handler.ts
Normal file
263
ts/opsserver/handlers/auth.handler.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
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' },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user