From c2372315f7c9365ea9c650bebe2da690dd0f5e80 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 25 Dec 2022 15:33:46 +0100 Subject: [PATCH] server: improve error messages Refactor Error's to ApiError's. Changelog: Changed --- .../api/endpoints/admin/accounts/create.ts | 18 ++++++++++--- .../api/endpoints/admin/accounts/delete.ts | 17 ++++++------- .../refresh-remote-instance-metadata.ts | 5 +++- .../admin/federation/update-instance.ts | 5 +++- .../api/endpoints/admin/moderators/add.ts | 9 +++++-- .../api/endpoints/admin/moderators/remove.ts | 5 +++- .../server/api/endpoints/admin/relays/add.ts | 2 +- .../api/endpoints/admin/reset-password.ts | 7 ++++-- .../server/api/endpoints/admin/show-user.ts | 7 ++++-- .../api/endpoints/admin/silence-user.ts | 7 ++++-- .../api/endpoints/admin/suspend-user.ts | 17 ++++++------- .../api/endpoints/admin/unsilence-user.ts | 5 +++- .../api/endpoints/admin/unsuspend-user.ts | 5 +++- .../api/endpoints/gallery/posts/create.ts | 11 ++++++-- .../api/endpoints/gallery/posts/update.ts | 13 ++++++++-- .../src/server/api/endpoints/i/2fa/done.ts | 7 ++++-- .../server/api/endpoints/i/2fa/key-done.ts | 25 +++++++++++-------- .../api/endpoints/i/2fa/register-key.ts | 7 ++++-- .../server/api/endpoints/i/2fa/register.ts | 5 +++- .../server/api/endpoints/i/2fa/remove-key.ts | 5 +++- .../server/api/endpoints/i/2fa/unregister.ts | 5 +++- .../server/api/endpoints/i/change-password.ts | 5 +++- .../server/api/endpoints/i/delete-account.ts | 5 +++- .../api/endpoints/i/regenerate-token.ts | 5 +++- .../src/server/api/endpoints/reset-db.ts | 3 ++- packages/backend/src/server/api/error.ts | 8 ++++++ 26 files changed, 151 insertions(+), 62 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 8da4c3d68..3012d2127 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -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, diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index 623b909f4..7c0392e3c 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -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)) { diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts index 5c691a1bc..439a802e1 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts @@ -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); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index bcfe9077e..a810300b5 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -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) }, { diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts index 2c46e1fc5..abea8c851 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts @@ -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, { diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts index 19e507fe4..0a988b820 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts @@ -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, { diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index 147b7298c..5fbce5411 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -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); } diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index 97d6f51d4..4a54a96e3 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -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); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index d7cf37ea3..604426470 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -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) { diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts index 24bd2dd3f..7b0a1bbad 100644 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts @@ -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, { diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 90ad703a3..473fa220e 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -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, { diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts index 6d20233c5..7c088bd76 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts @@ -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, { diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index 4f4fe4a0e..17fad27c4 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -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, { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index a22b603a5..a2c7e6038 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -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({ diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 20cab9243..cd911bf47 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -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({ diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index 730f77916..3800290b7 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -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, { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 37d480e7c..809efe814 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -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 = 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'); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index 555b98d5a..65dd4804f 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -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 diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 33f571772..db80a850f 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -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 diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 4467290b8..4e704ab63 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -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 diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 4deefa37b..d9730bde9 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -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, { diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index 5f625b695..3df7b0237 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -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 diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index ede4a9d03..01268373d 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -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); diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index 37cdf4846..50bd73f4a 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -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(); diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 1d145c31d..ce26e28ee 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -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(); diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index ddd7a3d0d..be9f4aef8 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -187,6 +187,14 @@ export const errors: Record 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,