gpstracker/src/auth.ts
Fabio Manganiello d49f8ec013
Fixed preRequestHandler route setup.
The map is a static object defined on the root `Route` class.

This means that any changes made by its derived classes (e.g. by
specifying the `@authenticate` annotation on some methods) will impact
the object for all the other derived classes too.

To prevent clashes, the structure of the map has been changed from:

```
method -> preRequestHandler
```

to:

```
RouteClass.name -> method -> preRequestHandler
```
2025-03-23 23:44:20 +01:00

78 lines
2.4 KiB
TypeScript

import { Request } from 'express';
import { Forbidden, Unauthorized } from './errors';
import { Optional } from '~/types';
import { RoleName, User, UserSession } from '~/models';
import Route from './routes/Route';
class AuthInfo {
public user: User;
public session: Optional<UserSession>;
constructor(user: User, session: Optional<UserSession> = null) {
this.user = user;
this.session = session;
}
}
function authenticate(roles: RoleName[] = []) {
return function (route: any, method: string) {
const routeClass = (<typeof Route> route.constructor);
if (!routeClass.preRequestHandlers[routeClass.name]) {
routeClass.preRequestHandlers[routeClass.name] = {}
}
routeClass.preRequestHandlers[routeClass.name][method] = async (req: Request): Promise<AuthInfo> => {
let user: Optional<User>;
let session: Optional<UserSession>;
// Check the `session` cookie or the `Authorization` header for the session token
let token = req.headers?.cookie?.match(/session=([^;]+)/)?.[1];
if (!token?.length) {
const authHeader = req.headers?.authorization;
if (authHeader?.startsWith('Bearer ')) {
token = authHeader.slice(7);
}
}
// Check if the token is valid
if (token?.length) {
session = await $repos.userSessions.byToken(token);
if (session) {
user = await $repos.users.get(session.userId);
}
}
if (!(session && user)) {
// Check the `username` and `password` query or body parameters
const username = req.body?.username || req.query?.username;
const password = req.body?.password || req.query?.password;
if (username?.length && password?.length) {
user = await $repos.users.find(username);
if (!(user && user.checkPassword(password))) {
user = null;
}
}
}
if (!user) {
throw new Unauthorized('Invalid credentials');
}
if (roles.length) {
// Check if the user has the required roles
const userRoles = new Set((await user.roles()).map((role) => role.name));
const missingPermissions = roles.filter((role) => !userRoles.has(role));
if (missingPermissions.length) {
throw new Forbidden('Missing required roles: [' + missingPermissions.join(', ') + ']');
}
}
return new AuthInfo(user, session);
}
}
}
export { AuthInfo, authenticate };