Skip to content

Commit a865996

Browse files
committed
feat: disabled users
resolves #41
1 parent db4728f commit a865996

File tree

12 files changed

+240
-154
lines changed

12 files changed

+240
-154
lines changed

packages/api/src/migrations/.snapshot-micro.json

+26-8
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@
116116
"primary": false,
117117
"nullable": true,
118118
"mappedType": "array"
119+
},
120+
"disabled_reason": {
121+
"name": "disabled_reason",
122+
"type": "varchar(255)",
123+
"unsigned": false,
124+
"autoincrement": false,
125+
"primary": false,
126+
"nullable": true,
127+
"mappedType": "string"
119128
}
120129
},
121130
"name": "users",
@@ -285,7 +294,7 @@
285294
"autoincrement": false,
286295
"primary": false,
287296
"nullable": true,
288-
"length": 6,
297+
"length": 0,
289298
"mappedType": "datetime"
290299
},
291300
"created_at": {
@@ -295,7 +304,7 @@
295304
"autoincrement": false,
296305
"primary": false,
297306
"nullable": false,
298-
"length": 6,
307+
"length": 0,
299308
"mappedType": "datetime"
300309
},
301310
"owner_id": {
@@ -385,7 +394,7 @@
385394
"autoincrement": false,
386395
"primary": false,
387396
"nullable": false,
388-
"length": 6,
397+
"length": 0,
389398
"mappedType": "datetime"
390399
},
391400
"owner_id": {
@@ -472,7 +481,7 @@
472481
"autoincrement": false,
473482
"primary": false,
474483
"nullable": false,
475-
"length": 6,
484+
"length": 0,
476485
"mappedType": "datetime"
477486
},
478487
"skip_verification": {
@@ -492,7 +501,7 @@
492501
"autoincrement": false,
493502
"primary": false,
494503
"nullable": true,
495-
"length": 6,
504+
"length": 0,
496505
"mappedType": "datetime"
497506
}
498507
},
@@ -595,6 +604,15 @@
595604
"nullable": false,
596605
"mappedType": "string"
597606
},
607+
"is_utf8": {
608+
"name": "is_utf8",
609+
"type": "boolean",
610+
"unsigned": false,
611+
"autoincrement": false,
612+
"primary": false,
613+
"nullable": true,
614+
"mappedType": "boolean"
615+
},
598616
"metadata_height": {
599617
"name": "metadata_height",
600618
"type": "int",
@@ -647,7 +665,7 @@
647665
"autoincrement": false,
648666
"primary": false,
649667
"nullable": false,
650-
"length": 6,
668+
"length": 0,
651669
"mappedType": "datetime"
652670
},
653671
"owner_id": {
@@ -792,7 +810,7 @@
792810
"autoincrement": false,
793811
"primary": false,
794812
"nullable": false,
795-
"length": 6,
813+
"length": 0,
796814
"mappedType": "datetime"
797815
}
798816
},
@@ -853,7 +871,7 @@
853871
"autoincrement": false,
854872
"primary": false,
855873
"nullable": false,
856-
"length": 6,
874+
"length": 0,
857875
"mappedType": "datetime"
858876
}
859877
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Migration } from '@mikro-orm/migrations';
2+
3+
export class Migration20240506030901 extends Migration {
4+
5+
async up(): Promise<void> {
6+
this.addSql('alter table "users" add column "disabled_reason" varchar(255) null;');
7+
}
8+
9+
async down(): Promise<void> {
10+
this.addSql('alter table "users" drop column "disabled_reason";');
11+
}
12+
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { UnauthorizedException } from '@nestjs/common';
2+
3+
export class AccountDisabledError extends UnauthorizedException {
4+
constructor(message: string) {
5+
super({
6+
// nestjs will filter out any additional keys we add to this object for graphql.
7+
// unfortunately, the only way for the frontend to pick this up without rewriting
8+
// nestjs error handling is to append the type to the message.
9+
message: `ACCOUNT_DISABLED: ${message}`,
10+
});
11+
}
12+
}

packages/api/src/modules/auth/auth.service.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import crypto from 'crypto';
77
import { authenticator } from 'otplib';
88
import { User } from '../user/user.entity.js';
99
import type { OTPEnabledDto } from './dto/otp-enabled.dto.js';
10+
import { AccountDisabledError } from './account-disabled.error.js';
1011

1112
export enum TokenType {
1213
USER = 'USER',
@@ -68,6 +69,10 @@ export class AuthService {
6869
await this.validateOTPCode(otpCode, user);
6970
}
7071

72+
if (user.disabledReason) {
73+
throw new AccountDisabledError(user.disabledReason)
74+
}
75+
7176
return user;
7277
}
7378

packages/api/src/modules/auth/guards/permission.guard.ts

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Reflector } from '@nestjs/core';
44
import { Permission } from '../../../constants.js';
55
import { getRequest } from '../../../helpers/get-request.js';
66
import { UserService } from '../../user/user.service.js';
7+
import { AccountDisabledError } from '../account-disabled.error.js';
78

89
@Injectable()
910
export class PermissionGuard implements CanActivate {
@@ -17,6 +18,10 @@ export class PermissionGuard implements CanActivate {
1718
const userId = request.user.id;
1819
const user = await this.userService.getUser(userId, false);
1920
if (!user) return false;
21+
if (user.disabledReason) {
22+
throw new AccountDisabledError(user.disabledReason)
23+
}
24+
2025
if (this.userService.checkPermissions(user.permissions, Permission.ADMINISTRATOR)) return true;
2126
if (!this.userService.checkPermissions(user.permissions, requiredPermissions)) return false;
2227
return true;

packages/api/src/modules/auth/strategies/jwt.strategy.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Strategy } from 'passport-jwt';
77
import { config } from '../../../config.js';
88
import { User } from '../../user/user.entity.js';
99
import { TokenType } from '../auth.service.js';
10+
import { AccountDisabledError } from '../account-disabled.error.js';
1011

1112
export interface JWTPayloadUser {
1213
id: string;
@@ -33,6 +34,10 @@ export class JWTStrategy extends PassportStrategy(Strategy) {
3334
if (!payload.secret) throw new UnauthorizedException('Outdated JWT - try refresh your session');
3435
const user = await this.userRepo.findOne({ secret: payload.secret });
3536
if (!user) throw new UnauthorizedException('Invalid token secret');
37+
if (user.disabledReason) {
38+
throw new AccountDisabledError(user.disabledReason)
39+
}
40+
3641
return user;
3742
}
3843
}

packages/api/src/modules/user/user.entity.ts

+4
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,9 @@ export class User {
8080
@Property({ nullable: true, hidden: true, type: ArrayType })
8181
otpRecoveryCodes?: string[];
8282

83+
@Exclude()
84+
@Property({ hidden: true, nullable: true })
85+
disabledReason?: string;
86+
8387
[OptionalProps]: 'permissions' | 'tags' | 'verifiedEmail';
8488
}

packages/web/src/@generated/gql.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ const documents = {
2323
types.GetFilesDocument,
2424
'\n query GetPastes($after: String) {\n user {\n pastes(first: 24, after: $after) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n node {\n id\n ...PasteCard\n }\n }\n }\n }\n }\n':
2525
types.GetPastesDocument,
26+
'\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n':
27+
types.LoginDocument,
2628
'\n query Config {\n config {\n allowTypes\n inquiriesEmail\n requireEmails\n uploadLimit\n currentHost {\n normalised\n redirect\n }\n rootHost {\n normalised\n url\n }\n hosts {\n normalised\n }\n }\n }\n':
2729
types.ConfigDocument,
2830
'\n fragment RegularUser on User {\n id\n username\n email\n verifiedEmail\n }\n':
2931
types.RegularUserFragmentDoc,
3032
'\n query GetUser {\n user {\n ...RegularUser\n }\n }\n': types.GetUserDocument,
31-
'\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n':
32-
types.LoginDocument,
3333
'\n mutation Logout {\n logout\n }\n': types.LogoutDocument,
3434
'\n query GenerateOTP {\n generateOTP {\n recoveryCodes\n qrauthUrl\n secret\n }\n }\n':
3535
types.GenerateOtpDocument,
@@ -100,6 +100,12 @@ export function graphql(
100100
export function graphql(
101101
source: '\n query GetPastes($after: String) {\n user {\n pastes(first: 24, after: $after) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n node {\n id\n ...PasteCard\n }\n }\n }\n }\n }\n',
102102
): (typeof documents)['\n query GetPastes($after: String) {\n user {\n pastes(first: 24, after: $after) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n node {\n id\n ...PasteCard\n }\n }\n }\n }\n }\n'];
103+
/**
104+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
105+
*/
106+
export function graphql(
107+
source: '\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n',
108+
): (typeof documents)['\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n'];
103109
/**
104110
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
105111
*/
@@ -118,12 +124,6 @@ export function graphql(
118124
export function graphql(
119125
source: '\n query GetUser {\n user {\n ...RegularUser\n }\n }\n',
120126
): (typeof documents)['\n query GetUser {\n user {\n ...RegularUser\n }\n }\n'];
121-
/**
122-
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
123-
*/
124-
export function graphql(
125-
source: '\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n',
126-
): (typeof documents)['\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n'];
127127
/**
128128
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
129129
*/

0 commit comments

Comments
 (0)