Skip to content

Commit

Permalink
feat: disabled users
Browse files Browse the repository at this point in the history
resolves #41
  • Loading branch information
sylv committed May 6, 2024
1 parent db4728f commit a865996
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 154 deletions.
34 changes: 26 additions & 8 deletions packages/api/src/migrations/.snapshot-micro.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@
"primary": false,
"nullable": true,
"mappedType": "array"
},
"disabled_reason": {
"name": "disabled_reason",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "string"
}
},
"name": "users",
Expand Down Expand Up @@ -285,7 +294,7 @@
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"length": 0,
"mappedType": "datetime"
},
"created_at": {
Expand All @@ -295,7 +304,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"length": 0,
"mappedType": "datetime"
},
"owner_id": {
Expand Down Expand Up @@ -385,7 +394,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"length": 0,
"mappedType": "datetime"
},
"owner_id": {
Expand Down Expand Up @@ -472,7 +481,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"length": 0,
"mappedType": "datetime"
},
"skip_verification": {
Expand All @@ -492,7 +501,7 @@
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"length": 0,
"mappedType": "datetime"
}
},
Expand Down Expand Up @@ -595,6 +604,15 @@
"nullable": false,
"mappedType": "string"
},
"is_utf8": {
"name": "is_utf8",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "boolean"
},
"metadata_height": {
"name": "metadata_height",
"type": "int",
Expand Down Expand Up @@ -647,7 +665,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"length": 0,
"mappedType": "datetime"
},
"owner_id": {
Expand Down Expand Up @@ -792,7 +810,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"length": 0,
"mappedType": "datetime"
}
},
Expand Down Expand Up @@ -853,7 +871,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"length": 0,
"mappedType": "datetime"
}
},
Expand Down
13 changes: 13 additions & 0 deletions packages/api/src/migrations/Migration20240506030901.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Migration } from '@mikro-orm/migrations';

export class Migration20240506030901 extends Migration {

async up(): Promise<void> {
this.addSql('alter table "users" add column "disabled_reason" varchar(255) null;');
}

async down(): Promise<void> {
this.addSql('alter table "users" drop column "disabled_reason";');
}

}
12 changes: 12 additions & 0 deletions packages/api/src/modules/auth/account-disabled.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { UnauthorizedException } from '@nestjs/common';

export class AccountDisabledError extends UnauthorizedException {
constructor(message: string) {
super({
// nestjs will filter out any additional keys we add to this object for graphql.
// unfortunately, the only way for the frontend to pick this up without rewriting
// nestjs error handling is to append the type to the message.
message: `ACCOUNT_DISABLED: ${message}`,
});
}
}
5 changes: 5 additions & 0 deletions packages/api/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import crypto from 'crypto';
import { authenticator } from 'otplib';
import { User } from '../user/user.entity.js';
import type { OTPEnabledDto } from './dto/otp-enabled.dto.js';
import { AccountDisabledError } from './account-disabled.error.js';

export enum TokenType {
USER = 'USER',
Expand Down Expand Up @@ -68,6 +69,10 @@ export class AuthService {
await this.validateOTPCode(otpCode, user);
}

if (user.disabledReason) {
throw new AccountDisabledError(user.disabledReason)
}

return user;
}

Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/modules/auth/guards/permission.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Reflector } from '@nestjs/core';
import { Permission } from '../../../constants.js';
import { getRequest } from '../../../helpers/get-request.js';
import { UserService } from '../../user/user.service.js';
import { AccountDisabledError } from '../account-disabled.error.js';

@Injectable()
export class PermissionGuard implements CanActivate {
Expand All @@ -17,6 +18,10 @@ export class PermissionGuard implements CanActivate {
const userId = request.user.id;
const user = await this.userService.getUser(userId, false);
if (!user) return false;
if (user.disabledReason) {
throw new AccountDisabledError(user.disabledReason)
}

if (this.userService.checkPermissions(user.permissions, Permission.ADMINISTRATOR)) return true;
if (!this.userService.checkPermissions(user.permissions, requiredPermissions)) return false;
return true;
Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/modules/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Strategy } from 'passport-jwt';
import { config } from '../../../config.js';
import { User } from '../../user/user.entity.js';
import { TokenType } from '../auth.service.js';
import { AccountDisabledError } from '../account-disabled.error.js';

export interface JWTPayloadUser {
id: string;
Expand All @@ -33,6 +34,10 @@ export class JWTStrategy extends PassportStrategy(Strategy) {
if (!payload.secret) throw new UnauthorizedException('Outdated JWT - try refresh your session');
const user = await this.userRepo.findOne({ secret: payload.secret });
if (!user) throw new UnauthorizedException('Invalid token secret');
if (user.disabledReason) {
throw new AccountDisabledError(user.disabledReason)
}

return user;
}
}
4 changes: 4 additions & 0 deletions packages/api/src/modules/user/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,9 @@ export class User {
@Property({ nullable: true, hidden: true, type: ArrayType })
otpRecoveryCodes?: string[];

@Exclude()
@Property({ hidden: true, nullable: true })
disabledReason?: string;

[OptionalProps]: 'permissions' | 'tags' | 'verifiedEmail';
}
16 changes: 8 additions & 8 deletions packages/web/src/@generated/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ const documents = {
types.GetFilesDocument,
'\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':
types.GetPastesDocument,
'\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n':
types.LoginDocument,
'\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':
types.ConfigDocument,
'\n fragment RegularUser on User {\n id\n username\n email\n verifiedEmail\n }\n':
types.RegularUserFragmentDoc,
'\n query GetUser {\n user {\n ...RegularUser\n }\n }\n': types.GetUserDocument,
'\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n':
types.LoginDocument,
'\n mutation Logout {\n logout\n }\n': types.LogoutDocument,
'\n query GenerateOTP {\n generateOTP {\n recoveryCodes\n qrauthUrl\n secret\n }\n }\n':
types.GenerateOtpDocument,
Expand Down Expand Up @@ -100,6 +100,12 @@ export function graphql(
export function graphql(
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',
): (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'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n',
): (typeof documents)['\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand All @@ -118,12 +124,6 @@ export function graphql(
export function graphql(
source: '\n query GetUser {\n user {\n ...RegularUser\n }\n }\n',
): (typeof documents)['\n query GetUser {\n user {\n ...RegularUser\n }\n }\n'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n',
): (typeof documents)['\n mutation Login($username: String!, $password: String!, $otp: String) {\n login(username: $username, password: $password, otpCode: $otp) {\n ...RegularUser\n }\n }\n'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading

0 comments on commit a865996

Please sign in to comment.