backend: remove default user, update user interface to include password hash and a list of roles, and refactor associated code

This commit is contained in:
juancarmore 2025-05-31 00:10:03 +02:00
parent 7d128ed699
commit a118b5cf92
7 changed files with 44 additions and 71 deletions

View File

@ -112,5 +112,7 @@ export const getProfile = (req: Request, res: Response) => {
return rejectRequestFromMeetError(res, error); return rejectRequestFromMeetError(res, error);
} }
return res.status(200).json(user); const userService = container.get(UserService);
const userDTO = userService.convertToDTO(user);
return res.status(200).json(userDTO);
}; };

View File

@ -23,8 +23,6 @@ export const {
// Authentication configuration // Authentication configuration
MEET_API_KEY = 'meet-api-key', MEET_API_KEY = 'meet-api-key',
MEET_USER = 'user',
MEET_SECRET = 'user',
MEET_ADMIN_USER = 'admin', MEET_ADMIN_USER = 'admin',
MEET_ADMIN_SECRET = 'admin', MEET_ADMIN_SECRET = 'admin',

View File

@ -82,7 +82,7 @@ export const tokenAndRoleValidator = (role: UserRole) => {
throw errorWithControl(errorInvalidTokenSubject(), true); throw errorWithControl(errorInvalidTokenSubject(), true);
} }
if (user.role !== role) { if (!user.roles.includes(role)) {
throw errorWithControl(errorInsufficientPermissions(), false); throw errorWithControl(errorInsufficientPermissions(), false);
} }
@ -134,10 +134,8 @@ export const apiKeyValidator = async (req: Request) => {
throw errorWithControl(errorInvalidApiKey(), true); throw errorWithControl(errorInvalidApiKey(), true);
} }
const apiUser = { const userService = container.get(UserService);
username: INTERNAL_CONFIG.API_USER, const apiUser = userService.getApiUser();
role: UserRole.APP
};
req.session = req.session || {}; req.session = req.session || {};
req.session.user = apiUser; req.session.user = apiUser;
@ -152,7 +150,8 @@ export const allowAnonymous = async (req: Request) => {
}; };
// Return the authenticated user if available, otherwise return an anonymous user // Return the authenticated user if available, otherwise return an anonymous user
const getAuthenticatedUserOrAnonymous = async (req: Request) => { const getAuthenticatedUserOrAnonymous = async (req: Request): Promise<User> => {
const userService = container.get(UserService);
let user: User | null = null; let user: User | null = null;
// Check if there is a user already authenticated // Check if there is a user already authenticated
@ -163,7 +162,6 @@ const getAuthenticatedUserOrAnonymous = async (req: Request) => {
const tokenService = container.get(TokenService); const tokenService = container.get(TokenService);
const payload = await tokenService.verifyToken(token); const payload = await tokenService.verifyToken(token);
const username = payload.sub; const username = payload.sub;
const userService = container.get(UserService);
user = username ? await userService.getUser(username) : null; user = username ? await userService.getUser(username) : null;
} catch (error) { } catch (error) {
const logger = container.get(LoggerService); const logger = container.get(LoggerService);
@ -172,10 +170,7 @@ const getAuthenticatedUserOrAnonymous = async (req: Request) => {
} }
if (!user) { if (!user) {
user = { user = userService.getAnonymousUser();
username: INTERNAL_CONFIG.ANONYMOUS_USER,
role: UserRole.USER
};
} }
return user; return user;

View File

@ -1,6 +1,5 @@
import { User } from '@typings-ce'; import { User } from '@typings-ce';
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { MEET_ADMIN_SECRET, MEET_ADMIN_USER } from '../environment.js';
import { PasswordHelper } from '../helpers/index.js'; import { PasswordHelper } from '../helpers/index.js';
import { LoggerService, MeetStorageService, UserService } from './index.js'; import { LoggerService, MeetStorageService, UserService } from './index.js';
@ -13,28 +12,12 @@ export class AuthService {
) {} ) {}
async authenticate(username: string, password: string): Promise<User | null> { async authenticate(username: string, password: string): Promise<User | null> {
const isAdmin = this.authenticateAdmin(username, password); const user = await this.userService.getUser(username);
const isUser = await this.authenticateUser(username, password);
if (isAdmin || isUser) { if (!user || !(await PasswordHelper.verifyPassword(password, user.passwordHash))) {
return this.userService.getUser(username); return null;
} }
return null; return user;
}
private authenticateAdmin(username: string, password: string): boolean {
return username === MEET_ADMIN_USER && password === MEET_ADMIN_SECRET;
}
private async authenticateUser(username: string, password: string): Promise<boolean> {
const userCredentials = await this.userService.getStoredUserCredentials();
if (!userCredentials) {
return false;
}
const isPasswordValid = await PasswordHelper.verifyPassword(password, userCredentials.passwordHash);
return username === userCredentials.username && isPasswordValid;
} }
} }

View File

@ -21,7 +21,7 @@ export class TokenService {
identity: user.username, identity: user.username,
ttl: MEET_ACCESS_TOKEN_EXPIRATION, ttl: MEET_ACCESS_TOKEN_EXPIRATION,
metadata: JSON.stringify({ metadata: JSON.stringify({
role: user.role roles: user.roles
}) })
}; };
return await this.generateJwtToken(tokenOptions); return await this.generateJwtToken(tokenOptions);
@ -32,7 +32,7 @@ export class TokenService {
identity: user.username, identity: user.username,
ttl: MEET_REFRESH_TOKEN_EXPIRATION, ttl: MEET_REFRESH_TOKEN_EXPIRATION,
metadata: JSON.stringify({ metadata: JSON.stringify({
role: user.role roles: user.roles
}) })
}; };
return await this.generateJwtToken(tokenOptions); return await this.generateJwtToken(tokenOptions);

View File

@ -1,43 +1,35 @@
import { SingleUserAuth, SingleUserCredentials, User, UserRole } from '@typings-ce'; import { User, UserDTO, UserRole } from '@typings-ce';
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { MEET_ADMIN_USER } from '../environment.js'; import INTERNAL_CONFIG from '../config/internal-config.js';
import { LoggerService, MeetStorageService } from './index.js'; import { MeetStorageService } from './index.js';
@injectable() @injectable()
export class UserService { export class UserService {
constructor( constructor(@inject(MeetStorageService) protected storageService: MeetStorageService) {}
@inject(LoggerService) protected logger: LoggerService,
@inject(MeetStorageService) protected globalPrefService: MeetStorageService
) {}
async getUser(username: string): Promise<User | null> { async getUser(username: string): Promise<User | null> {
if (username === MEET_ADMIN_USER) { return this.storageService.getUser(username);
return {
username: MEET_ADMIN_USER,
role: UserRole.ADMIN
};
}
const userCredentials = await this.getStoredUserCredentials();
if (userCredentials && username === userCredentials.username) {
return {
username,
role: UserRole.USER
};
}
return null;
} }
async getStoredUserCredentials(): Promise<SingleUserCredentials | null> { getAnonymousUser(): User {
try { return {
const { securityPreferences } = await this.globalPrefService.getGlobalPreferences(); username: INTERNAL_CONFIG.ANONYMOUS_USER,
const { method: authMethod } = securityPreferences.authentication; passwordHash: '',
return (authMethod as SingleUserAuth).credentials; roles: [UserRole.USER]
} catch (error) { };
this.logger.error('Error getting stored user credentials:' + error); }
return null;
} getApiUser(): User {
return {
username: INTERNAL_CONFIG.API_USER,
passwordHash: '',
roles: [UserRole.APP]
};
}
// Convert user to UserDTO to remove sensitive information
convertToDTO(user: User): UserDTO {
const { passwordHash, ...userDTO } = user;
return userDTO;
} }
} }

View File

@ -1,6 +1,7 @@
export interface User { export interface User {
username: string; username: string;
role: UserRole; passwordHash: string;
roles: UserRole[];
} }
export const enum UserRole { export const enum UserRole {
@ -8,3 +9,5 @@ export const enum UserRole {
USER = 'user', USER = 'user',
APP = 'app' APP = 'app'
} }
export type UserDTO = Omit<User, 'passwordHash'>;