server: drop ajaxUtil and handle responses in each endpoint

This commit is contained in:
Jesse Chan
2021-02-21 22:49:03 +08:00
parent 6f7bca075c
commit 2c8c7a4f3d
6 changed files with 647 additions and 750 deletions
+114 -114
View File
@@ -12,7 +12,6 @@ import {
} from '../../../shared/schema/api/auth';
import config from '../../../config';
import {getAuthToken, getCookieOptions} from '../../util/authUtil';
import {getResponseFn, validationError} from '../../util/ajaxUtil';
import requireAdmin from '../../middleware/requireAdmin';
import services from '../../services';
import Users from '../../models/Users';
@@ -43,7 +42,7 @@ router.use(
const sendAuthenticationResponse = (
res: Response,
credentials: Required<Pick<Credentials, 'username' | 'level'>>,
): void => {
): Response => {
const {username, level} = credentials;
res.cookie('jwt', getAuthToken(username), getCookieOptions());
@@ -54,7 +53,7 @@ const sendAuthenticationResponse = (
level,
};
res.json(response);
return res.json(response);
};
const preloadConfigs: AuthVerificationPreloadConfigs = {
@@ -74,36 +73,34 @@ router.use('/users', passport.authenticate('jwt', {session: false}), requireAdmi
* @return {object} 401 - incorrect username or password - application/json
* @return {AuthAuthenticationResponse} 200 - success response - application/json
*/
router.post<unknown, unknown, AuthAuthenticationOptions>('/authenticate', (req, res) => {
if (config.authMethod === 'none') {
sendAuthenticationResponse(res, Users.getConfigUser());
return;
}
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);
const parsedResult = authAuthenticationSchema.safeParse(req.body);
if (!parsedResult.success) {
validationError(res, parsedResult.error);
return;
}
if (!parsedResult.success) {
return res.status(422).json({message: 'Validation error.'});
}
const credentials = parsedResult.data;
const credentials = parsedResult.data;
Users.comparePassword(credentials).then(
(level) => {
sendAuthenticationResponse(res, {
...credentials,
level,
});
},
() => {
// Incorrect username or password.
res.status(401).json({
message: failedLoginResponse,
});
},
);
});
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) => {
@@ -114,8 +111,7 @@ router.use('/register', (req, res, next) => {
handleSubsequentUser: () => {
passport.authenticate('jwt', {session: false}, (err, user: UserInDatabase) => {
if (err || !user) {
res.status(401).send('Unauthorized');
return;
return res.status(401).send('Unauthorized');
}
req.user = user;
// Only admin users can create users
@@ -139,40 +135,38 @@ router.use('/register', (req, res, next) => {
* @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', (req, res) => {
// No user can be registered when authMethod is none
if (config.authMethod === 'none') {
// Return 404
res.status(404).send('Not found');
return;
}
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);
const parsedResult = authRegistrationSchema.safeParse(req.body);
if (!parsedResult.success) {
validationError(res, parsedResult.error);
return;
}
if (!parsedResult.success) {
return res.status(422).json({message: 'Validation error.'});
}
const credentials = parsedResult.data;
const credentials = parsedResult.data;
// Attempt to save the user
Users.createUser(credentials).then(
(user) => {
services.bootstrapServicesForUser(user);
// Attempt to save the user
return Users.createUser(credentials).then(
(user) => {
services.bootstrapServicesForUser(user);
if (req.query.cookie === 'false') {
getResponseFn(res)({username: user.username});
return;
}
if (req.query.cookie === 'false') {
return res.status(200).json({username: user.username});
}
sendAuthenticationResponse(res, credentials);
},
(err) => {
getResponseFn(res)({username: credentials.username}, err);
},
);
});
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) => {
@@ -226,21 +220,23 @@ router.use('/verify', (req, res, next) => {
* @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) => {
if (req.user == null) {
res.status(500).send('Unattached user.');
return;
}
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,
};
const response: AuthVerificationResponse = {
initialUser: false,
username: req.user.username,
level: req.user.level,
configs: preloadConfigs,
};
res.json(response);
});
return res.json(response);
},
);
// All subsequent routes are protected.
router.use('/', passport.authenticate('jwt', {session: false}));
@@ -279,18 +275,21 @@ router.use('/users', (_req, res, next) => {
* @return {string} 403 - user is not authorized to list users
* @return {Array<Pick<UserInDatabase, 'username' | 'level'>>} 200 - success response - application/json
*/
router.get('/users', (_req, res) => {
Users.listUsers().then(
(users) =>
res.json(
users.map((user) => ({
username: user.username,
level: user.level,
})),
),
() => res.status(500).json(new Error()),
);
});
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}
@@ -302,16 +301,17 @@ router.get('/users', (_req, res) => {
* @return {string} 403 - user is not authorized to delete user
* @return {{username: string}} 200 - success response - application/json
*/
router.delete('/users/:username', (req, res) => {
Users.removeUser(req.params.username)
.then((id) => {
services.destroyUserServices(id);
res.json({username: req.params.username});
})
.catch((err) => {
res.status(500).json(err);
});
});
router.delete(
'/users/:username',
async (req, res): Promise<Response> => {
return Users.removeUser(req.params.username)
.then((id) => {
services.destroyUserServices(id);
return res.json({username: req.params.username});
})
.catch(({code, message}) => res.status(500).json({code, message}));
},
);
/**
* PATCH /api/auth/users/{username}
@@ -325,29 +325,29 @@ router.delete('/users/:username', (req, res) => {
* @return {object} 422 - request validation error - application/json
* @return {} 200 - success response
*/
router.patch<{username: Credentials['username']}, unknown, AuthUpdateUserOptions>('/users/:username', (req, res) => {
const {username} = req.params;
router.patch<{username: Credentials['username']}, unknown, AuthUpdateUserOptions>(
'/users/:username',
async (req, res): Promise<Response> => {
const {username} = req.params;
const parsedResult = authUpdateUserSchema.safeParse(req.body);
const parsedResult = authUpdateUserSchema.safeParse(req.body);
if (!parsedResult.success) {
validationError(res, parsedResult.error);
return;
}
if (!parsedResult.success) {
return res.status(422).json({message: 'Validation error.'});
}
const patch = parsedResult.data;
const patch = parsedResult.data;
Users.updateUser(username, patch)
.then((newUsername) => {
return Users.lookupUser(newUsername).then((user) => {
services.destroyUserServices(user._id);
services.bootstrapServicesForUser(user);
res.send();
});
})
.catch((err) => {
res.status(500).json(err);
});
});
return Users.updateUser(username, patch)
.then((newUsername) => {
return Users.lookupUser(newUsername).then((user) => {
services.destroyUserServices(user._id);
services.bootstrapServicesForUser(user);
return res.status(200).json({});
});
})
.catch(({code, message}) => res.status(500).json({code, message}));
},
);
export default router;
+25 -27
View File
@@ -1,9 +1,8 @@
import express from 'express';
import express, {Response} from 'express';
import type {ClientSettings} from '@shared/types/ClientSettings';
import type {SetClientSettingsOptions} from '@shared/types/api/client';
import {getResponseFn} from '../../util/ajaxUtil';
import requireAdmin from '../../middleware/requireAdmin';
// Those settings don't require administrator access.
@@ -19,16 +18,14 @@ const router = express.Router();
* @return {{isConnected: true}} 200 - success response - application/json
* @return {{isConnected: false}} 500 - failure response - application/json
*/
router.get('/connection-test', (req, res) => {
req.services.clientGatewayService
.testGateway()
.then(() => {
res.status(200).json({isConnected: true});
})
.catch(() => {
res.status(500).json({isConnected: false});
});
});
router.get(
'/connection-test',
async (req, res): Promise<Response> =>
req.services.clientGatewayService.testGateway().then(
() => res.status(200).json({isConnected: true}),
() => res.status(500).json({isConnected: false}),
),
);
/**
* GET /api/client/settings
@@ -38,14 +35,14 @@ router.get('/connection-test', (req, res) => {
* @return {ClientSettings} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.get('/settings', (req, res) => {
const callback = getResponseFn(res);
req.services.clientGatewayService
.getClientSettings()
.then(callback)
.catch((e) => callback(null, e));
});
router.get(
'/settings',
async (req, res): Promise<Response> =>
req.services.clientGatewayService.getClientSettings().then(
(settings) => res.status(200).json(settings),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* PATCH /api/client/settings
@@ -70,13 +67,14 @@ router.patch('/settings', (req, res, next) => {
next();
}
});
router.patch<unknown, unknown, SetClientSettingsOptions>('/settings', (req, res) => {
const callback = getResponseFn(res);
req.services.clientGatewayService
.setClientSettings(req.body)
.then(callback)
.catch((e) => callback(null, e));
});
router.patch<unknown, unknown, SetClientSettingsOptions>(
'/settings',
async (req, res): Promise<Response> =>
req.services.clientGatewayService.setClientSettings(req.body).then(
(response) => res.status(200).json(response),
({code, message}) => res.status(500).json({code, message}),
),
);
export default router;
+75 -108
View File
@@ -1,9 +1,8 @@
import express from 'express';
import express, {Response} from 'express';
import type {AddFeedOptions, AddRuleOptions, ModifyFeedOptions} from '@shared/types/api/feed-monitor';
import {accessDeniedError, isAllowedPath, sanitizePath} from '../../util/fileUtil';
import {getResponseFn} from '../../util/ajaxUtil';
const router = express.Router();
@@ -15,18 +14,14 @@ const router = express.Router();
* @return {{feeds: Array<Feed>; rules: Array<Rule>}} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.get('/', (req, res) => {
const callback = getResponseFn(res);
req.services.feedService
.getAll()
.then((feedsAndRules) => {
callback(feedsAndRules);
})
.catch((error) => {
callback(null, error);
});
});
router.get(
'/',
async (req, res): Promise<Response> =>
req.services.feedService.getAll().then(
(feedsAndRules) => res.status(200).json(feedsAndRules),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* DELETE /api/feed-monitor/{id}
@@ -37,18 +32,14 @@ router.get('/', (req, res) => {
* @return {} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.delete<{id: string}>('/:id', (req, res) => {
const callback = getResponseFn(res);
req.services.feedService
.removeItem(req.params.id)
.then(() => {
callback(null);
})
.catch((error) => {
callback(null, error);
});
});
router.delete<{id: string}>(
'/:id',
async (req, res): Promise<Response> =>
req.services.feedService.removeItem(req.params.id).then(
(response) => res.status(200).json(response),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* GET /api/feed-monitor/feeds/{id?}
@@ -59,18 +50,14 @@ router.delete<{id: string}>('/:id', (req, res) => {
* @return {Array<Feed>}} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.get<{id?: string}>('/feeds/:id?', (req, res) => {
const callback = getResponseFn(res);
req.services.feedService
.getFeeds(req.params.id)
.then((feeds) => {
callback(feeds);
})
.catch((error) => {
callback(null, error);
});
});
router.get<{id?: string}>(
'/feeds/:id?',
async (req, res): Promise<Response> =>
req.services.feedService.getFeeds(req.params.id).then(
(feeds) => res.status(200).json(feeds),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* PUT /api/feed-monitor/feeds
@@ -81,18 +68,14 @@ router.get<{id?: string}>('/feeds/:id?', (req, res) => {
* @return {Feed} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.put<unknown, unknown, AddFeedOptions>('/feeds', (req, res) => {
const callback = getResponseFn(res);
req.services.feedService
.addFeed(req.body)
.then((feed) => {
callback(feed);
})
.catch((error) => {
callback(null, error);
});
});
router.put<unknown, unknown, AddFeedOptions>(
'/feeds',
async (req, res): Promise<Response> =>
req.services.feedService.addFeed(req.body).then(
(feed) => res.status(200).json(feed),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* PATCH /api/feed-monitor/feeds/{id}
@@ -104,18 +87,14 @@ router.put<unknown, unknown, AddFeedOptions>('/feeds', (req, res) => {
* @return {} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.patch<{id: string}, unknown, ModifyFeedOptions>('/feeds/:id', (req, res) => {
const callback = getResponseFn(res);
req.services.feedService
.modifyFeed(req.params.id, req.body)
.then(() => {
callback(null);
})
.catch((error) => {
callback(null, error);
});
});
router.patch<{id: string}, unknown, ModifyFeedOptions>(
'/feeds/:id',
async (req, res): Promise<Response> =>
req.services.feedService.modifyFeed(req.params.id, req.body).then(
(response) => res.status(200).json(response),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* GET /api/feed-monitor/feeds/{id}/items?search=<string>
@@ -127,18 +106,14 @@ router.patch<{id: string}, unknown, ModifyFeedOptions>('/feeds/:id', (req, res)
* @return {Array<Item>} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.get<{id: string}, unknown, ModifyFeedOptions, {search: string}>('/feeds/:id/items', (req, res) => {
const callback = getResponseFn(res);
req.services.feedService
.getItems(req.params.id, req.query.search)
.then((items) => {
callback(items);
})
.catch((error) => {
callback(null, error);
});
});
router.get<{id: string}, unknown, ModifyFeedOptions, {search: string}>(
'/feeds/:id/items',
async (req, res): Promise<Response> =>
req.services.feedService.getItems(req.params.id, req.query.search).then(
(items) => res.status(200).json(items),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* GET /api/feed-monitor/rules
@@ -148,18 +123,14 @@ router.get<{id: string}, unknown, ModifyFeedOptions, {search: string}>('/feeds/:
* @return {Array<Rule>}} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.get('/rules', (req, res) => {
const callback = getResponseFn(res);
req.services.feedService
.getRules()
.then((rules) => {
callback(rules);
})
.catch((error) => {
callback(null, error);
});
});
router.get(
'/rules',
async (req, res): Promise<Response> =>
req.services.feedService.getRules().then(
(rules) => res.status(200).json(rules),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* PUT /api/feed-monitor/rules
@@ -170,29 +141,25 @@ router.get('/rules', (req, res) => {
* @return {Rule} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.put<unknown, unknown, AddRuleOptions>('/rules', (req, res) => {
const callback = getResponseFn(res);
let sanitizedPath: string | null = null;
try {
sanitizedPath = sanitizePath(req.body.destination);
if (!isAllowedPath(sanitizedPath)) {
callback(null, accessDeniedError());
return;
router.put<unknown, unknown, AddRuleOptions>(
'/rules',
async (req, res): Promise<Response> => {
let sanitizedPath: string | null = null;
try {
sanitizedPath = sanitizePath(req.body.destination);
if (!isAllowedPath(sanitizedPath)) {
const {code, message} = accessDeniedError();
return res.status(403).json({code, message});
}
} catch ({code, message}) {
return res.status(403).json({code, message});
}
} catch (e) {
callback(null, e);
return;
}
req.services.feedService
.addRule({...req.body, destination: sanitizedPath})
.then((rule) => {
callback(rule);
})
.catch((error) => {
callback(null, error);
});
});
return req.services.feedService.addRule({...req.body, destination: sanitizedPath}).then(
(rule) => res.status(200).json(rule),
({code, message}) => res.status(500).json({code, message}),
);
},
);
export default router;
+30 -43
View File
@@ -19,7 +19,6 @@ import clientActivityStream from '../../middleware/clientActivityStream';
import eventStream from '../../middleware/eventStream';
import feedMonitorRoutes from './feed-monitor';
import {getAuthToken, verifyToken} from '../../util/authUtil';
import {getResponseFn} from '../../util/ajaxUtil';
import torrentsRoutes from './torrents';
const router = express.Router();
@@ -172,8 +171,8 @@ router.get<unknown, unknown, unknown, {snapshot: HistorySnapshot}>('/history', (
(snapshot) => {
res.json(snapshot);
},
(err) => {
res.status(500).json(err);
({code, message}) => {
res.status(500).json({code, message});
},
);
});
@@ -192,8 +191,8 @@ router.get<unknown, unknown, unknown, NotificationFetchOptions>('/notifications'
(notifications) => {
res.status(200).json(notifications);
},
(err: Error) => {
res.status(500).json({message: err.message});
({code, message}) => {
res.status(500).json({code, message});
},
);
});
@@ -211,8 +210,8 @@ router.delete('/notifications', (req, res) => {
() => {
res.status(200).send();
},
(err: Error) => {
res.status(500).json({message: err.message});
({code, message}) => {
res.status(500).json({code, message});
},
);
});
@@ -225,18 +224,14 @@ router.delete('/notifications', (req, res) => {
* @return {FloodSettings} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.get('/settings', (req, res) => {
const callback = getResponseFn(res);
req.services.settingService
.get(null)
.then((settings) => {
callback(settings as FloodSettings);
})
.catch((err) => {
callback(null, err);
});
});
router.get(
'/settings',
async (req, res): Promise<Response> =>
req.services.settingService.get(null).then(
(settings) => res.status(200).json(settings),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* GET /api/settings/{property}
@@ -247,18 +242,14 @@ router.get('/settings', (req, res) => {
* @return {Partial<FloodSettings>} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.get<{property: keyof FloodSettings}>('/settings/:property', (req, res) => {
const callback = getResponseFn(res);
req.services.settingService
.get(req.params.property)
.then((settings) => {
callback(settings);
})
.catch((err) => {
callback(null, err);
});
});
router.get<{property: keyof FloodSettings}>(
'/settings/:property',
async (req, res): Promise<Response> =>
req.services.settingService.get(req.params.property).then(
(setting) => res.status(200).json(setting),
({code, message}) => res.status(500).json({code, message}),
),
);
/**
* PATCH /api/settings
@@ -269,17 +260,13 @@ router.get<{property: keyof FloodSettings}>('/settings/:property', (req, res) =>
* @return {Partial<FloodSettings>} 200 - success response - application/json
* @return {Error} 500 - failure response - application/json
*/
router.patch<unknown, unknown, SetFloodSettingsOptions>('/settings', (req, res) => {
const callback = getResponseFn(res);
req.services.settingService
.set(req.body)
.then((savedSettings) => {
callback(savedSettings);
})
.catch((err) => {
callback(null, err);
});
});
router.patch<unknown, unknown, SetFloodSettingsOptions>(
'/settings',
async (req, res): Promise<Response> =>
req.services.settingService.set(req.body).then(
(savedSettings) => res.status(200).json(savedSettings),
({code, message}) => res.status(500).json({code, message}),
),
);
export default router;
File diff suppressed because it is too large Load Diff
-28
View File
@@ -1,28 +0,0 @@
import type {Response} from 'express';
export const validationError = (res: Response, err: Error) => {
res.status(422).json({
message: 'Validation error.',
error: err,
});
};
export const getResponseFn = (res: Response) => <D extends unknown>(data: D, error?: Error | string) => {
if (error) {
if (process.env.NODE_ENV === 'development') {
console.trace(error);
}
if (typeof error === 'string') {
res.status(500).json(Error(error));
return;
}
res.status(500).json({
message: error.message,
code: (error as NodeJS.ErrnoException).code,
});
} else {
res.json(data);
}
};