mirror of
https://github.com/zoriya/flood.git
synced 2025-12-05 23:06:20 +00:00
337 lines
9.8 KiB
TypeScript
337 lines
9.8 KiB
TypeScript
import type {Response} from 'express';
|
|
import express from 'express';
|
|
import rateLimit from 'express-rate-limit';
|
|
import passport from 'passport';
|
|
|
|
import config from '../../../config';
|
|
import type {
|
|
AuthAuthenticationOptions,
|
|
AuthAuthenticationResponse,
|
|
AuthRegistrationOptions,
|
|
AuthUpdateUserOptions,
|
|
AuthVerificationResponse,
|
|
} from '../../../shared/schema/api/auth';
|
|
import {
|
|
authAuthenticationSchema,
|
|
authRegistrationSchema,
|
|
authUpdateUserSchema,
|
|
AuthVerificationPreloadConfigs,
|
|
} from '../../../shared/schema/api/auth';
|
|
import type {Credentials, UserInDatabase} from '../../../shared/schema/Auth';
|
|
import requireAdmin from '../../middleware/requireAdmin';
|
|
import Users from '../../models/Users';
|
|
import {bootstrapServicesForUser, destroyUserServices} from '../../services';
|
|
import {getAuthToken, getCookieOptions} from '../../util/authUtil';
|
|
|
|
const router = express.Router();
|
|
|
|
const failedLoginResponse = 'Failed login.';
|
|
|
|
// Limit each IP to 200 request every 5 minutes
|
|
// to prevent brute forcing password or denial-of-service
|
|
router.use(
|
|
'/',
|
|
rateLimit({
|
|
windowMs: 5 * 60 * 1000,
|
|
max: 200,
|
|
}),
|
|
);
|
|
|
|
const sendAuthenticationResponse = (
|
|
res: Response,
|
|
credentials: Required<Pick<Credentials, 'username' | 'level'>>,
|
|
): Response => {
|
|
const {username, level} = credentials;
|
|
|
|
res.cookie('jwt', getAuthToken(username), getCookieOptions());
|
|
|
|
const response: AuthAuthenticationResponse = {
|
|
success: true,
|
|
username,
|
|
level,
|
|
};
|
|
|
|
return res.json(response);
|
|
};
|
|
|
|
const preloadConfigs: AuthVerificationPreloadConfigs = {
|
|
authMethod: config.authMethod,
|
|
pollInterval: config.torrentClientPollInterval,
|
|
};
|
|
|
|
router.use('/users', passport.authenticate('jwt', {session: false}), requireAdmin);
|
|
|
|
/**
|
|
* POST /api/auth/authenticate
|
|
* @summary Authenticates a user
|
|
* @tags Auth
|
|
* @security None
|
|
* @param {AuthAuthenticationOptions} request.body.required - options - application/json
|
|
* @return {object} 422 - request validation error - application/json
|
|
* @return {object} 401 - incorrect username or password - application/json
|
|
* @return {AuthAuthenticationResponse} 200 - success response - application/json
|
|
*/
|
|
router.post<unknown, unknown, AuthAuthenticationOptions>('/authenticate', async (req, res): Promise<Response> => {
|
|
if (config.authMethod === 'none') {
|
|
return sendAuthenticationResponse(res, Users.getConfigUser());
|
|
}
|
|
|
|
const parsedResult = authAuthenticationSchema.safeParse(req.body);
|
|
|
|
if (!parsedResult.success) {
|
|
return res.status(422).json({message: 'Validation error.'});
|
|
}
|
|
|
|
const credentials = parsedResult.data;
|
|
|
|
return Users.comparePassword(credentials).then(
|
|
(level) =>
|
|
sendAuthenticationResponse(res, {
|
|
...credentials,
|
|
level,
|
|
}),
|
|
() =>
|
|
res.status(401).json({
|
|
message: failedLoginResponse,
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Allow unauthenticated registration if no users are currently registered.
|
|
router.use('/register', (req, res, next) => {
|
|
Users.initialUserGate({
|
|
handleInitialUser: () => {
|
|
next();
|
|
},
|
|
handleSubsequentUser: () => {
|
|
passport.authenticate('jwt', {session: false}, (err: unknown, user: UserInDatabase) => {
|
|
if (err || !user) {
|
|
return res.status(401).send('Unauthorized');
|
|
}
|
|
req.user = user;
|
|
// Only admin users can create users
|
|
requireAdmin(req, res, next);
|
|
})(req, res, next);
|
|
},
|
|
});
|
|
});
|
|
|
|
/**
|
|
* POST /api/auth/register
|
|
* @summary Registers a user
|
|
* @tags Auth
|
|
* @security None - initial request
|
|
* @security Administrator - subsequent requests
|
|
* @param {AuthRegistrationOptions} request.body.required - options - application/json
|
|
* @param {'true' | 'false'} cookie.query - whether to Set-Cookie if succeeded
|
|
* @return {string} 404 - registration is disabled
|
|
* @return {string} 403 - user is not authorized to create user
|
|
* @return {object} 422 - request validation error - application/json
|
|
* @return {{username: string}} 200 - success response if cookie=false - application/json
|
|
* @return {AuthAuthenticationResponse} 200 - success response - application/json
|
|
*/
|
|
router.post<unknown, unknown, AuthRegistrationOptions, {cookie: string}>(
|
|
'/register',
|
|
async (req, res): Promise<Response> => {
|
|
// No user can be registered when authMethod is none
|
|
if (config.authMethod === 'none') {
|
|
// Return 404
|
|
return res.status(404).send('Not found');
|
|
}
|
|
|
|
const parsedResult = authRegistrationSchema.safeParse(req.body);
|
|
|
|
if (!parsedResult.success) {
|
|
return res.status(422).json({message: 'Validation error.'});
|
|
}
|
|
|
|
const credentials = parsedResult.data;
|
|
|
|
// Attempt to save the user
|
|
return Users.createUser(credentials).then(
|
|
(user) => {
|
|
bootstrapServicesForUser(user);
|
|
|
|
if (req.query.cookie === 'false') {
|
|
return res.status(200).json({username: user.username});
|
|
}
|
|
|
|
return sendAuthenticationResponse(res, credentials);
|
|
},
|
|
({message}) => res.status(500).json({message}),
|
|
);
|
|
},
|
|
);
|
|
|
|
// Allow unauthenticated verification if no users are currently registered.
|
|
router.use('/verify', (req, res, next) => {
|
|
// Unconditionally provide a token if auth is disabled
|
|
if (config.authMethod === 'none') {
|
|
const {username, level} = Users.getConfigUser();
|
|
|
|
res.cookie('jwt', getAuthToken(username), getCookieOptions());
|
|
|
|
const response: AuthVerificationResponse = {
|
|
initialUser: false,
|
|
username,
|
|
level,
|
|
configs: preloadConfigs,
|
|
};
|
|
|
|
res.json(response);
|
|
return;
|
|
}
|
|
|
|
Users.initialUserGate({
|
|
handleInitialUser: () => {
|
|
const response: AuthVerificationResponse = {
|
|
initialUser: true,
|
|
configs: preloadConfigs,
|
|
};
|
|
res.json(response);
|
|
},
|
|
handleSubsequentUser: () => {
|
|
passport.authenticate('jwt', {session: false}, (err: unknown, user: UserInDatabase) => {
|
|
if (err || !user) {
|
|
res.status(401).json({
|
|
configs: preloadConfigs,
|
|
});
|
|
return;
|
|
}
|
|
|
|
req.user = user;
|
|
next();
|
|
})(req, res, next);
|
|
},
|
|
});
|
|
});
|
|
|
|
/**
|
|
* GET /api/auth/verify
|
|
* @summary Verifies the connectivity and validity of session
|
|
* @tags Auth
|
|
* @security User
|
|
* @return {string} 401 - not authenticated or token expired
|
|
* @return {string} 500 - authenticated succeeded but user is unattached (this should NOT happen)
|
|
* @return {AuthVerificationResponse} 200 - success response - application/json
|
|
*/
|
|
router.get('/verify', (req, res): Response => {
|
|
if (req.user == null) {
|
|
return res.status(500).send('Unattached user.');
|
|
}
|
|
|
|
const response: AuthVerificationResponse = {
|
|
initialUser: false,
|
|
username: req.user.username,
|
|
level: req.user.level,
|
|
configs: preloadConfigs,
|
|
};
|
|
|
|
return res.json(response);
|
|
});
|
|
|
|
// All subsequent routes are protected.
|
|
router.use('/', passport.authenticate('jwt', {session: false}));
|
|
|
|
/**
|
|
* GET /api/auth/logout
|
|
* @summary Clears the session cookie
|
|
* @tags Auth
|
|
* @security User
|
|
* @return {string} 401 - not authenticated or token expired
|
|
* @return {} 200 - success response
|
|
*/
|
|
router.get('/logout', (_req, res) => {
|
|
res.clearCookie('jwt').send();
|
|
});
|
|
|
|
// All subsequent routes need administrator access.
|
|
router.use('/', requireAdmin);
|
|
|
|
router.use('/users', (_req, res, next) => {
|
|
// No operation on user when authMethod is none
|
|
if (config.authMethod === 'none') {
|
|
// Return 404
|
|
res.status(404).send('Not found');
|
|
}
|
|
|
|
next();
|
|
});
|
|
|
|
/**
|
|
* GET /api/auth/users
|
|
* @summary Lists all users
|
|
* @tags Auth
|
|
* @security Administrator
|
|
* @return {string} 401 - not authenticated or token expired
|
|
* @return {string} 403 - user is not authorized to list users
|
|
* @return {Array<Pick<UserInDatabase, 'username' | 'level'>>} 200 - success response - application/json
|
|
*/
|
|
router.get('/users', async (_req, res): Promise<Response> => {
|
|
return Users.listUsers().then(
|
|
(users) =>
|
|
res.json(
|
|
users.map((user) => ({
|
|
username: user.username,
|
|
level: user.level,
|
|
})),
|
|
),
|
|
({code, message}) => res.status(500).json({code, message}),
|
|
);
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/auth/users/{username}
|
|
* @summary Deletes a user
|
|
* @tags Auth
|
|
* @security Administrator
|
|
* @param {string} username.path - username of the user to be deleted
|
|
* @return {string} 401 - not authenticated or token expired
|
|
* @return {string} 403 - user is not authorized to delete user
|
|
* @return {{username: string}} 200 - success response - application/json
|
|
*/
|
|
router.delete('/users/:username', async (req, res): Promise<Response> => {
|
|
return Users.removeUser(req.params.username)
|
|
.then(() => res.json({username: req.params.username}))
|
|
.catch(({code, message}) => res.status(500).json({code, message}));
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/auth/users/{username}
|
|
* @summary Updates a user
|
|
* @tags Auth
|
|
* @security Administrator
|
|
* @param {string} username.path - username of the user to be updated
|
|
* @param {AuthUpdateUserOptions} request.body.required - options - application/json
|
|
* @return {string} 401 - not authenticated or token expired
|
|
* @return {string} 403 - user is not authorized to update user
|
|
* @return {object} 422 - request validation error - application/json
|
|
* @return {} 200 - success response
|
|
*/
|
|
router.patch<{username: Credentials['username']}, unknown, AuthUpdateUserOptions>(
|
|
'/users/:username',
|
|
async (req, res): Promise<Response> => {
|
|
const {username} = req.params;
|
|
|
|
const parsedResult = authUpdateUserSchema.safeParse(req.body);
|
|
|
|
if (!parsedResult.success) {
|
|
return res.status(422).json({message: 'Validation error.'});
|
|
}
|
|
|
|
const patch = parsedResult.data;
|
|
|
|
return Users.updateUser(username, patch)
|
|
.then((newUsername) => {
|
|
return Users.lookupUser(newUsername).then(async (user) => {
|
|
await destroyUserServices(user._id);
|
|
bootstrapServicesForUser(user);
|
|
return res.status(200).json({});
|
|
});
|
|
})
|
|
.catch(({code, message}) => res.status(500).json({code, message}));
|
|
},
|
|
);
|
|
|
|
export default router;
|