server: improve error messages

Refactor Error's to ApiError's.

Changelog: Changed
This commit is contained in:
Johann150 2022-12-25 15:33:46 +01:00
parent 09bc3cf95a
commit c2372315f7
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
26 changed files with 151 additions and 62 deletions

View file

@ -1,5 +1,6 @@
import { IsNull } from 'typeorm';
import { Users } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js';
import { signup } from '../../../common/signup.js';
@ -17,6 +18,8 @@ export const meta = {
},
},
},
errors: ['ACCESS_DENIED'],
} as const;
export const paramDef = {
@ -31,10 +34,17 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, _me) => {
const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null;
const noUsers = (await Users.countBy({
host: IsNull(),
})) === 0;
if (!noUsers && !me?.isAdmin) throw new Error('access denied');
if (me == null) {
// check if this is the initial setup
const noUsers = (await Users.countBy({
host: IsNull(),
})) === 0;
if (!noUsers) {
throw new ApiError('ACCESS_DENIED');
}
} else if (!me.isAdmin) {
throw new ApiError('ACCESS_DENIED');
}
const { account, secret } = await signup({
username: ps.username,

View file

@ -1,4 +1,5 @@
import { Users } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import { doPostSuspend } from '@/services/suspend-user.js';
import { publishUserEvent } from '@/services/stream.js';
import { createDeleteAccountJob } from '@/queue/index.js';
@ -9,6 +10,8 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'],
} as const;
export const paramDef = {
@ -24,15 +27,11 @@ export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
if (user.isAdmin) {
throw new Error('cannot suspend admin');
}
if (user.isModerator) {
throw new Error('cannot suspend moderator');
throw new ApiError('NO_SUCH_USER');
} else if (user.isAdmin) {
throw new ApiError('IS_ADMIN');
} else if(user.isModerator) {
throw new ApiError('IS_MODERATOR');
}
if (Users.isLocalUser(user)) {

View file

@ -1,5 +1,6 @@
import { Instances } from '@/models/index.js';
import { toPuny } from '@/misc/convert-host.js';
import { ApiError } from '@/server/api/error.js';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
import define from '../../../define.js';
@ -8,6 +9,8 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_OBJECT'],
} as const;
export const paramDef = {
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, me) => {
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
if (instance == null) {
throw new Error('instance not found');
throw new ApiError('NO_SUCH_OBJECT');
}
fetchInstanceMetadata(instance, true);

View file

@ -1,5 +1,6 @@
import { Instances } from '@/models/index.js';
import { toPuny } from '@/misc/convert-host.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js';
export const meta = {
@ -7,6 +8,8 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_OBJECT'],
} as const;
export const paramDef = {
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, me) => {
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
if (instance == null) {
throw new Error('instance not found');
throw new ApiError('NO_SUCH_OBJECT');
}
Instances.update({ host: toPuny(ps.host) }, {

View file

@ -1,12 +1,17 @@
import { Users } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import { publishInternalEvent } from '@/services/stream.js';
import define from '../../../define.js';
export const meta = {
tags: ['admin'],
description: 'Grants a user moderator privileges. Administrators cannot be granted moderator privileges.',
requireCredential: true,
requireAdmin: true,
errors: ['NO_SUCH_USER', 'IS_ADMIN'],
} as const;
export const paramDef = {
@ -22,11 +27,11 @@ export default define(meta, paramDef, async (ps) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
throw new ApiError('NO_SUCH_USER');
}
if (user.isAdmin) {
throw new Error('cannot mark as moderator if admin user');
throw new ApiError('IS_ADMIN');
}
await Users.update(user.id, {

View file

@ -1,4 +1,5 @@
import { Users } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import { publishInternalEvent } from '@/services/stream.js';
import define from '../../../define.js';
@ -7,6 +8,8 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
errors: ['NO_SUCH_USER'],
} as const;
export const paramDef = {
@ -22,7 +25,7 @@ export default define(meta, paramDef, async (ps) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
throw new ApiError('NO_SUCH_USER');
}
await Users.update(user.id, {

View file

@ -51,7 +51,7 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
try {
if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError('INVALID_URL', 'https only');
} catch (e) {
throw new ApiError('INVALID_URL', e);
}

View file

@ -1,6 +1,7 @@
import bcrypt from 'bcryptjs';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { Users, UserProfiles } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../define.js';
export const meta = {
@ -21,6 +22,8 @@ export const meta = {
},
},
},
errors: ['NO_SUCH_USER', 'IS_ADMIN'],
} as const;
export const paramDef = {
@ -36,11 +39,11 @@ export default define(meta, paramDef, async (ps) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
throw new ApiError('NO_SUCH_USER');
}
if (user.isAdmin) {
throw new Error('cannot reset password of admin');
throw new ApiError('IS_ADMIN');
}
const passwd = secureRndstr(8, true);

View file

@ -1,4 +1,5 @@
import { Signins, UserProfiles, Users } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../define.js';
export const meta = {
@ -11,6 +12,8 @@ export const meta = {
type: 'object',
nullable: false, optional: false,
},
errors: ['NO_SUCH_USER', 'IS_ADMIN'],
} as const;
export const paramDef = {
@ -29,12 +32,12 @@ export default define(meta, paramDef, async (ps, me) => {
]);
if (user == null || profile == null) {
throw new Error('user not found');
throw new ApiError('NO_SUCH_USER');
}
const _me = await Users.findOneByOrFail({ id: me.id });
if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) {
throw new Error('cannot show info of admin');
throw new ApiError('IS_ADMIN');
}
if (!_me.isAdmin) {

View file

@ -1,4 +1,5 @@
import { Users } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { publishInternalEvent } from '@/services/stream.js';
import define from '../../define.js';
@ -8,6 +9,8 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_USER', 'IS_ADMIN'],
} as const;
export const paramDef = {
@ -23,11 +26,11 @@ export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
throw new ApiError('NO_SUCH_USER');
}
if (user.isAdmin) {
throw new Error('cannot silence admin');
throw new ApiError('IS_ADMIN');
}
await Users.update(user.id, {

View file

@ -1,6 +1,7 @@
import deleteFollowing from '@/services/following/delete.js';
import { Users, Followings, Notifications } from '@/models/index.js';
import { User } from '@/models/entities/user.js';
import { ApiError } from '@/server/api/error.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { doPostSuspend } from '@/services/suspend-user.js';
import { publishUserEvent } from '@/services/stream.js';
@ -11,6 +12,8 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'],
} as const;
export const paramDef = {
@ -26,15 +29,11 @@ export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
if (user.isAdmin) {
throw new Error('cannot suspend admin');
}
if (user.isModerator) {
throw new Error('cannot suspend moderator');
throw new ApiError('NO_SUCH_USER');
} else if (user.isAdmin) {
throw new ApiError('IS_ADMIN');
} else if (user.isModerator) {
throw new ApiError('IS_MODERATOR');
}
await Users.update(user.id, {

View file

@ -1,4 +1,5 @@
import { Users } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { publishInternalEvent } from '@/services/stream.js';
import define from '../../define.js';
@ -8,6 +9,8 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_USER'],
} as const;
export const paramDef = {
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
throw new ApiError('NO_SUCH_USER');
}
await Users.update(user.id, {

View file

@ -1,4 +1,5 @@
import { Users } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { doPostUnsuspend } from '@/services/unsuspend-user.js';
import define from '../../define.js';
@ -8,6 +9,8 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_USER'],
} as const;
export const paramDef = {
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
throw new ApiError('NO_SUCH_USER');
}
await Users.update(user.id, {

View file

@ -3,6 +3,7 @@ import { genId } from '@/misc/gen-id.js';
import { GalleryPost } from '@/models/entities/gallery-post.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { HOUR } from '@/const.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js';
export const meta = {
@ -46,8 +47,14 @@ export default define(meta, paramDef, async (ps, user) => {
}),
))).filter((file): file is DriveFile => file != null);
if (files.length === 0) {
throw new Error();
if (files.length !== ps.fileIds.length) {
throw new ApiError(
'INVALID_PARAM',
{
param: '#/properties/fileIds/items',
reason: 'contains invalid file IDs',
}
);
}
const post = await GalleryPosts.insert(new GalleryPost({

View file

@ -1,6 +1,7 @@
import { DriveFiles, GalleryPosts } from '@/models/index.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { HOUR } from '@/const.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js';
export const meta = {
@ -20,6 +21,8 @@ export const meta = {
optional: false, nullable: false,
ref: 'GalleryPost',
},
errors: ['INVALID_PARAM'],
} as const;
export const paramDef = {
@ -45,8 +48,14 @@ export default define(meta, paramDef, async (ps, user) => {
}),
))).filter((file): file is DriveFile => file != null);
if (files.length === 0) {
throw new Error();
if (files.length !== ps.fileIds.length) {
throw new ApiError(
'INVALID_PARAM',
{
param: '#/properties/fileIds/items',
reason: 'contains invalid file IDs',
}
);
}
await GalleryPosts.update({

View file

@ -1,11 +1,14 @@
import * as speakeasy from 'speakeasy';
import { UserProfiles } from '@/models/index.js';
import define from '../../../define.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
requireCredential: true,
secure: true,
errors: ['INTERNAL_ERROR', 'ACCESS_DENIED'],
} as const;
export const paramDef = {
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
if (profile.twoFactorTempSecret == null) {
throw new Error('二段階認証の設定が開始されていません');
throw new ApiError('INTERNAL_ERROR', 'Two-step verification has not been initiated.');
}
const verified = (speakeasy as any).totp.verify({
@ -33,7 +36,7 @@ export default define(meta, paramDef, async (ps, user) => {
});
if (!verified) {
throw new Error('not verified');
throw new ApiError('ACCESS_DENIED', 'TOTP missmatch');
}
await UserProfiles.update(user.id, {

View file

@ -9,6 +9,7 @@ import {
Users,
} from '@/models/index.js';
import config from '@/config/index.js';
import { ApiError } from '@/server/api/error.js';
import { publishMainStream } from '@/services/stream.js';
import define from '../../../define.js';
import { procedures, hash } from '../../../2fa.js';
@ -20,6 +21,8 @@ export const meta = {
requireCredential: true,
secure: true,
errors: ['ACCESS_DENIED', 'INTERNAL_ERROR', 'NO_SUCH_OBJECT'],
} as const;
export const paramDef = {
@ -42,20 +45,20 @@ export default define(meta, paramDef, async (ps, user) => {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
throw new ApiError('ACCESS_DENIED');
}
if (!profile.twoFactorEnabled) {
throw new Error('2fa not enabled');
throw new ApiError('INTERNAL_ERROR', '2fa not enabled');
}
const clientData = JSON.parse(ps.clientDataJSON);
if (clientData.type !== 'webauthn.create') {
throw new Error('not a creation attestation');
throw new ApiError('INTERNAL_ERROR', 'not a creation attestation');
}
if (clientData.origin !== config.scheme + '://' + config.host) {
throw new Error('origin mismatch');
throw new ApiError('INTERNAL_ERROR', 'origin mismatch');
}
const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
@ -64,14 +67,14 @@ export default define(meta, paramDef, async (ps, user) => {
const rpIdHash = attestation.authData.slice(0, 32);
if (!rpIdHashReal.equals(rpIdHash)) {
throw new Error('rpIdHash mismatch');
throw new ApiError('INTERNAL_ERROR', 'rpIdHash mismatch');
}
const flags = attestation.authData[32];
// eslint:disable-next-line:no-bitwise
if (!(flags & 1)) {
throw new Error('user not present');
throw new ApiError('INTERNAL_ERROR', 'user not present');
}
const authData = Buffer.from(attestation.authData);
@ -80,11 +83,11 @@ export default define(meta, paramDef, async (ps, user) => {
const publicKeyData = authData.slice(55 + credentialIdLength);
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
if (publicKey.get(3) !== -7) {
throw new Error('alg mismatch');
throw new ApiError('INTERNAL_ERROR', 'algorithm mismatch');
}
if (!(procedures as any)[attestation.fmt]) {
throw new Error('unsupported fmt');
throw new ApiError('INTERNAL_ERROR', 'unsupported fmt');
}
const verificationData = (procedures as any)[attestation.fmt].verify({
@ -95,7 +98,7 @@ export default define(meta, paramDef, async (ps, user) => {
publicKey,
rpIdHash,
});
if (!verificationData.valid) throw new Error('signature invalid');
if (!verificationData.valid) throw new ApiError('INTERNAL_ERROR', 'signature invalid');
const attestationChallenge = await AttestationChallenges.findOneBy({
userId: user.id,
@ -105,7 +108,7 @@ export default define(meta, paramDef, async (ps, user) => {
});
if (!attestationChallenge) {
throw new Error('non-existent challenge');
throw new ApiError('NO_SUCH_OBJECT', 'Attestation challenge not found.');
}
await AttestationChallenges.delete({
@ -118,7 +121,7 @@ export default define(meta, paramDef, async (ps, user) => {
new Date().getTime() - attestationChallenge.createdAt.getTime() >=
5 * MINUTE
) {
throw new Error('expired challenge');
throw new ApiError('NO_SUCH_OBJECT', 'Attestation challenge expired.');
}
const credentialIdString = credentialId.toString('hex');

View file

@ -3,6 +3,7 @@ import * as crypto from 'node:crypto';
import bcrypt from 'bcryptjs';
import { UserProfiles, AttestationChallenges } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js';
import { hash } from '../../../2fa.js';
@ -12,6 +13,8 @@ export const meta = {
requireCredential: true,
secure: true,
errors: ['ACCESS_DENIED', 'INTERNAL_ERROR'],
} as const;
export const paramDef = {
@ -30,11 +33,11 @@ export default define(meta, paramDef, async (ps, user) => {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
throw new ApiError('ACCESS_DENIED');
}
if (!profile.twoFactorEnabled) {
throw new Error('2fa not enabled');
throw new ApiError('INTERNAL_ERROR', '2fa not enabled');
}
// 32 byte challenge

View file

@ -3,12 +3,15 @@ import * as speakeasy from 'speakeasy';
import * as QRCode from 'qrcode';
import config from '@/config/index.js';
import { UserProfiles } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js';
export const meta = {
requireCredential: true,
secure: true,
errors: ['ACCESS_DENIED'],
} as const;
export const paramDef = {
@ -27,7 +30,7 @@ export default define(meta, paramDef, async (ps, user) => {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
throw new ApiError('ACCESS_DENIED');
}
// Generate user's secret key

View file

@ -1,12 +1,15 @@
import bcrypt from 'bcryptjs';
import { UserProfiles, UserSecurityKeys, Users } from '@/models/index.js';
import { publishMainStream } from '@/services/stream.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js';
export const meta = {
requireCredential: true,
secure: true,
errors: ['ACCESS_DENIED'],
} as const;
export const paramDef = {
@ -26,7 +29,7 @@ export default define(meta, paramDef, async (ps, user) => {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
throw new ApiError('ACCESS_DENIED');
}
// Make sure we only delete the user's own creds

View file

@ -1,11 +1,14 @@
import bcrypt from 'bcryptjs';
import { UserProfiles } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js';
export const meta = {
requireCredential: true,
secure: true,
errors: ['ACCESS_DENIED'],
} as const;
export const paramDef = {
@ -24,7 +27,7 @@ export default define(meta, paramDef, async (ps, user) => {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
throw new ApiError('ACCESS_DENIED');
}
await UserProfiles.update(user.id, {

View file

@ -1,11 +1,14 @@
import bcrypt from 'bcryptjs';
import { UserProfiles } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../define.js';
export const meta = {
requireCredential: true,
secure: true,
errors: ['ACCESS_DENIED'],
} as const;
export const paramDef = {
@ -25,7 +28,7 @@ export default define(meta, paramDef, async (ps, user) => {
const same = await bcrypt.compare(ps.currentPassword, profile.password!);
if (!same) {
throw new Error('incorrect password');
throw new ApiError('ACCESS_DENIED');
}
// Generate hash of password

View file

@ -1,12 +1,15 @@
import bcrypt from 'bcryptjs';
import { UserProfiles, Users } from '@/models/index.js';
import { deleteAccount } from '@/services/delete-account.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../define.js';
export const meta = {
requireCredential: true,
secure: true,
errors: ['ACCESS_DENIED'],
} as const;
export const paramDef = {
@ -29,7 +32,7 @@ export default define(meta, paramDef, async (ps, user) => {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
throw new ApiError('ACCESS_DENIED');
}
await deleteAccount(user);

View file

@ -2,12 +2,15 @@ import bcrypt from 'bcryptjs';
import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js';
import { Users, UserProfiles } from '@/models/index.js';
import generateUserToken from '../../common/generate-native-user-token.js';
import { ApiError } from '@/server/api/error.js';
import define from '../../define.js';
export const meta = {
requireCredential: true,
secure: true,
errors: ['ACCESS_DENIED'],
} as const;
export const paramDef = {
@ -29,7 +32,7 @@ export default define(meta, paramDef, async (ps, user) => {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
throw new ApiError('ACCESS_DENIED');
}
const newToken = generateUserToken();

View file

@ -1,4 +1,5 @@
import { resetDb } from '@/db/postgre.js';
import { ApiError } from '@/server/api/error.js';
import define from '../define.js';
export const meta = {
@ -17,7 +18,7 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test');
if (process.env.NODE_ENV !== 'test') throw new ApiError('ACCESS_DENIED');
await resetDb();

View file

@ -187,6 +187,14 @@ export const errors: Record<string, { message: string, httpStatusCode: number }>
message: 'Invalid username.',
httpStatusCode: 400,
},
IS_ADMIN: {
message: 'This action cannot be done to an administrator account.',
httpStatusCode: 400,
},
IS_MODERATOR: {
message: 'This action cannot be done to a moderator account.',
httpStatusCode: 400,
},
LESS_RESTRICTIVE_VISIBILITY: {
message: 'The visibility cannot be less restrictive than the parent note.',
httpStatusCode: 400,