adminjs experimentation
This commit is contained in:
parent
fb4154c9e5
commit
c40eb00d4f
1
.gitignore
vendored
1
.gitignore
vendored
@ -36,6 +36,7 @@ lerna-debug.log*
|
||||
|
||||
# local development environment files
|
||||
.env
|
||||
.adminjs
|
||||
/devdocker
|
||||
/config*.toml
|
||||
/private
|
||||
|
6611
package-lock.json
generated
6611
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -24,6 +24,9 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adminjs/express": "^6.1.0",
|
||||
"@adminjs/nestjs": "^6.1.0",
|
||||
"@adminjs/typeorm": "^5.0.1",
|
||||
"@icynet/oauth2-provider": "^1.0.8",
|
||||
"@nestjs/cache-manager": "^2.2.1",
|
||||
"@nestjs/common": "^10.3.3",
|
||||
@ -32,6 +35,7 @@
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/throttler": "^5.1.2",
|
||||
"adminjs": "^7.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cache-manager": "^5.4.0",
|
||||
"cache-manager-redis-yet": "^4.1.2",
|
||||
@ -43,6 +47,7 @@
|
||||
"cropperjs": "^1.6.1",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^16.4.4",
|
||||
"express-formidable": "^1.2.0",
|
||||
"express-session": "^1.18.0",
|
||||
"express-useragent": "^1.0.15",
|
||||
"geoip-lite": "^1.4.10",
|
||||
|
@ -13,6 +13,8 @@ import { SSRFrontEndModule } from './modules/ssr-front-end/ssr-front-end.module'
|
||||
import { UtilityModule } from './modules/utility/utility.module';
|
||||
import { WellKnownModule } from './modules/well-known/well-known.module';
|
||||
import { CommonCacheModule } from './modules/cache/cache.module';
|
||||
import { AdminjsModule } from './modules/adminjs/adminjs.module';
|
||||
import { AdminjsService } from './modules/adminjs/adminjs.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -37,6 +39,14 @@ import { CommonCacheModule } from './modules/cache/cache.module';
|
||||
SSRFrontEndModule,
|
||||
WellKnownModule,
|
||||
ApiModule,
|
||||
// TODO: https://docs.adminjs.co/installation/plugins/nest
|
||||
import('@adminjs/nestjs').then(({ AdminModule }) =>
|
||||
AdminModule.createAdminAsync({
|
||||
imports: [AdminjsModule],
|
||||
useFactory: (shims: AdminjsService) => shims.getConfiguration(),
|
||||
inject: [AdminjsService],
|
||||
}),
|
||||
),
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, CSRFMiddleware],
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import dotenv from 'dotenv';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { join } from 'path';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
|
12
src/modules/adminjs/adminjs.module.ts
Normal file
12
src/modules/adminjs/adminjs.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigurationModule } from '../config/config.module';
|
||||
import { AdminjsService } from './adminjs.service';
|
||||
import { ObjectsModule } from '../objects/objects.module';
|
||||
import { UserResource } from './resources/user.resource';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigurationModule, ObjectsModule],
|
||||
providers: [AdminjsService, UserResource],
|
||||
exports: [AdminjsService],
|
||||
})
|
||||
export class AdminjsModule {}
|
46
src/modules/adminjs/adminjs.service.ts
Normal file
46
src/modules/adminjs/adminjs.service.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigurationService } from '../config/config.service';
|
||||
import { Upload } from '../objects/upload/upload.entity';
|
||||
import { Privilege } from '../objects/privilege/privilege.entity';
|
||||
import { OAuth2Client } from '../objects/oauth2-client/oauth2-client.entity';
|
||||
import { FormUtilityService } from '../utility/services/form-utility.service';
|
||||
import { TokenService } from '../utility/services/token.service';
|
||||
import { UserResource } from './resources/user.resource';
|
||||
|
||||
/**
|
||||
* Shim service for AdminJs features
|
||||
*/
|
||||
@Injectable({})
|
||||
export class AdminjsService {
|
||||
constructor(
|
||||
private readonly config: ConfigurationService,
|
||||
private readonly formUtil: FormUtilityService,
|
||||
private readonly token: TokenService,
|
||||
private readonly userResource: UserResource,
|
||||
) {}
|
||||
|
||||
async getConfiguration() {
|
||||
const { AdminJS } = await import('adminjs');
|
||||
const AdminJSTypeORM = await import('@adminjs/typeorm');
|
||||
AdminJS.registerAdapter({
|
||||
Database: AdminJSTypeORM.Database,
|
||||
Resource: AdminJSTypeORM.Resource,
|
||||
});
|
||||
return {
|
||||
adminJsOptions: {
|
||||
rootPath: '/admin',
|
||||
resources: [this.userResource, Upload, Privilege, OAuth2Client],
|
||||
},
|
||||
// auth: {
|
||||
// authenticate: (email, password) => Promise.resolve({ email }),
|
||||
// cookieName: 'adminjs',
|
||||
// cookiePassword: 'secret',
|
||||
// },
|
||||
sessionOptions: {
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
secret: 'secret',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
18
src/modules/adminjs/resources/user.resource.ts
Normal file
18
src/modules/adminjs/resources/user.resource.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { User } from 'src/modules/objects/user/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserResource {
|
||||
public resource = User;
|
||||
public options = {
|
||||
listProperties: [
|
||||
'id',
|
||||
'uuid',
|
||||
'email',
|
||||
'username',
|
||||
'display_name',
|
||||
'activated',
|
||||
'created_at',
|
||||
],
|
||||
};
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import * as cors from 'cors';
|
||||
import cors from 'cors';
|
||||
import { ConfigurationModule } from '../config/config.module';
|
||||
import { JWTModule } from '../jwt/jwt.module';
|
||||
import { OAuth2Module } from '../oauth2/oauth2.module';
|
||||
|
@ -6,6 +6,7 @@ import { ParsedQs } from 'qs';
|
||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserAdapter implements OAuth2UserAdapter {
|
||||
@ -13,6 +14,7 @@ export class UserAdapter implements OAuth2UserAdapter {
|
||||
private readonly userService: UserService,
|
||||
private readonly clientService: OAuth2ClientService,
|
||||
private readonly form: FormUtilityService,
|
||||
private readonly token: TokenService,
|
||||
) {}
|
||||
|
||||
getId(user: OAuth2User): number {
|
||||
@ -48,7 +50,7 @@ export class UserAdapter implements OAuth2UserAdapter {
|
||||
}
|
||||
|
||||
checkPassword(user: OAuth2User, password: string): Promise<boolean> {
|
||||
return this.userService.comparePasswords(user.password, password);
|
||||
return this.token.comparePasswords(user.password, password);
|
||||
}
|
||||
|
||||
async fetchFromRequest(
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@ -8,7 +9,7 @@ import {
|
||||
import { User } from '../user/user.entity';
|
||||
|
||||
@Entity()
|
||||
export class AuditLog {
|
||||
export class AuditLog extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Lookup } from 'geoip-lite';
|
||||
import { Details } from 'express-useragent';
|
||||
import { AuditAction } from './audit.enum';
|
||||
import { AuditLog } from './audit.entity';
|
||||
|
||||
export interface UserLoginEntry {
|
||||
login_at: Date;
|
||||
@ -17,3 +18,8 @@ export interface AuditSearchClause {
|
||||
content?: string;
|
||||
flagged?: boolean;
|
||||
}
|
||||
|
||||
export interface AuditResponse extends AuditLog {
|
||||
location?: Partial<Lookup>;
|
||||
user_agent?: Partial<Details>;
|
||||
}
|
||||
|
@ -10,10 +10,14 @@ import {
|
||||
import { User } from '../user/user.entity';
|
||||
import { AuditLog } from './audit.entity';
|
||||
import { AuditAction } from './audit.enum';
|
||||
import { Lookup, lookup } from 'geoip-lite';
|
||||
import { Details, parse } from 'express-useragent';
|
||||
import { lookup } from 'geoip-lite';
|
||||
import { parse } from 'express-useragent';
|
||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||
import { AuditSearchClause, UserLoginEntry } from './audit.interfaces';
|
||||
import {
|
||||
AuditResponse,
|
||||
AuditSearchClause,
|
||||
UserLoginEntry,
|
||||
} from './audit.interfaces';
|
||||
|
||||
const PLUCK_LOCATION = ['country', 'city', 'timezone', 'll'];
|
||||
const PLUCK_USER_AGENT = ['browser', 'version', 'os', 'platform'];
|
||||
@ -86,15 +90,7 @@ export class AuditService {
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
search: AuditSearchClause,
|
||||
): Promise<
|
||||
[
|
||||
(AuditLog & {
|
||||
location?: Partial<Lookup>;
|
||||
user_agent?: Partial<Details>;
|
||||
})[],
|
||||
number,
|
||||
]
|
||||
> {
|
||||
): Promise<[AuditResponse[], number]> {
|
||||
const [list, num] = await this.audit.findAndCount({
|
||||
...this.buildAuditSearch(search),
|
||||
take: limit,
|
||||
@ -104,7 +100,9 @@ export class AuditService {
|
||||
});
|
||||
|
||||
return [
|
||||
list.map((entry) => ({
|
||||
list.map(
|
||||
(entry) =>
|
||||
({
|
||||
...entry,
|
||||
location: entry.actor_ip
|
||||
? this.form.pluckObject(
|
||||
@ -119,7 +117,8 @@ export class AuditService {
|
||||
)
|
||||
: null,
|
||||
actor: this.form.stripObject(entry.actor, ['password']),
|
||||
})),
|
||||
}) as AuditResponse,
|
||||
),
|
||||
num,
|
||||
];
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@ -9,7 +10,7 @@ import {
|
||||
import { User } from '../user/user.entity';
|
||||
|
||||
@Entity()
|
||||
export class Document {
|
||||
export class Document extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
|
@ -38,7 +38,7 @@ export class DocumentService {
|
||||
return {
|
||||
...doc,
|
||||
html,
|
||||
};
|
||||
} as Document & { html: string };
|
||||
}
|
||||
|
||||
public async getDocumentByID(
|
||||
@ -49,6 +49,6 @@ export class DocumentService {
|
||||
return {
|
||||
...doc,
|
||||
html,
|
||||
};
|
||||
} as Document & { html: string };
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@ -12,7 +13,7 @@ import { User } from '../user/user.entity';
|
||||
import { OAuth2ClientURL } from './oauth2-client-url.entity';
|
||||
|
||||
@Entity()
|
||||
export class OAuth2Client {
|
||||
export class OAuth2Client extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@ -16,7 +17,7 @@ export enum OAuth2TokenType {
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class OAuth2Token {
|
||||
export class OAuth2Token extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Privilege {
|
||||
export class Privilege extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@ -9,7 +10,7 @@ import {
|
||||
import { User } from '../user/user.entity';
|
||||
|
||||
@Entity()
|
||||
export class Upload {
|
||||
export class Upload extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@ -20,7 +21,7 @@ export enum UserTokenType {
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class UserToken {
|
||||
export class UserToken extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@ -12,7 +13,7 @@ import { Privilege } from '../privilege/privilege.entity';
|
||||
import { Upload } from '../upload/upload.entity';
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ILike, Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
import { UserTokenType } from '../user-token/user-token.entity';
|
||||
import { User } from './user.entity';
|
||||
@ -149,18 +148,6 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
public async comparePasswords(
|
||||
hash: string,
|
||||
password: string,
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
public async hashPassword(password: string): Promise<string> {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
return bcrypt.hash(password, salt);
|
||||
}
|
||||
|
||||
public async sendActivationEmail(
|
||||
user: User,
|
||||
redirectTo?: string,
|
||||
@ -245,7 +232,7 @@ export class UserService {
|
||||
throw new Error('Username is already in use!');
|
||||
}
|
||||
|
||||
const hashword = await this.hashPassword(newUserInfo.password);
|
||||
const hashword = await this.token.hashPassword(newUserInfo.password);
|
||||
const user = new User();
|
||||
user.email = newUserInfo.email;
|
||||
user.uuid = this.token.createUUID();
|
||||
|
@ -75,7 +75,7 @@ export class LoginController {
|
||||
if (
|
||||
!user ||
|
||||
!user.activated ||
|
||||
!(await this.userService.comparePasswords(user.password, password))
|
||||
!(await this.token.comparePasswords(user.password, password))
|
||||
) {
|
||||
req.flash('form', { username });
|
||||
req.flash('message', {
|
||||
@ -365,7 +365,7 @@ export class LoginController {
|
||||
throw new Error('The passwords do not match!');
|
||||
}
|
||||
|
||||
const hashword = await this.userService.hashPassword(password);
|
||||
const hashword = await this.token.hashPassword(password);
|
||||
token.user.password = hashword;
|
||||
|
||||
await this.userService.updateUser(token.user);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { FactoryProvider } from '@nestjs/common';
|
||||
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||
|
||||
import * as session from 'express-session';
|
||||
import session from 'express-session';
|
||||
import RedisStore from 'connect-redis';
|
||||
import type { RequestHandler } from 'express';
|
||||
import type { Redis } from 'src/modules/redis/redis.providers';
|
||||
|
@ -33,15 +33,15 @@ import { SettingsService } from './settings.service';
|
||||
@Controller('/account')
|
||||
export class SettingsController {
|
||||
constructor(
|
||||
private readonly _service: SettingsService,
|
||||
private readonly _form: FormUtilityService,
|
||||
private readonly _upload: UploadService,
|
||||
private readonly _token: TokenService,
|
||||
private readonly _user: UserService,
|
||||
private readonly _totp: UserTOTPService,
|
||||
private readonly _client: OAuth2ClientService,
|
||||
private readonly _oaToken: OAuth2TokenService,
|
||||
private readonly _audit: AuditService,
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly formService: FormUtilityService,
|
||||
private readonly uploadService: UploadService,
|
||||
private readonly tokenService: TokenService,
|
||||
private readonly userService: UserService,
|
||||
private readonly totpService: UserTOTPService,
|
||||
private readonly clientService: OAuth2ClientService,
|
||||
private readonly oaTokenService: OAuth2TokenService,
|
||||
private readonly auditService: AuditService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -53,7 +53,7 @@ export class SettingsController {
|
||||
@Get('general')
|
||||
@Render('settings/general')
|
||||
public general(@Req() req: Request) {
|
||||
return this._form.populateTemplate(req, { user: req.user });
|
||||
return this.formService.populateTemplate(req, { user: req.user });
|
||||
}
|
||||
|
||||
@Post('general')
|
||||
@ -63,7 +63,7 @@ export class SettingsController {
|
||||
@Body() body: { display_name?: string },
|
||||
) {
|
||||
try {
|
||||
const { display_name } = this._form.trimmed(body, ['display_name']);
|
||||
const { display_name } = this.formService.trimmed(body, ['display_name']);
|
||||
if (!display_name) {
|
||||
throw new Error('Display name is required.');
|
||||
}
|
||||
@ -76,7 +76,7 @@ export class SettingsController {
|
||||
|
||||
req.user.display_name = display_name;
|
||||
|
||||
await this._user.updateUser(req.user);
|
||||
await this.userService.updateUser(req.user);
|
||||
req.flash('message', {
|
||||
error: false,
|
||||
text: 'Display name has been changed!',
|
||||
@ -99,7 +99,7 @@ export class SettingsController {
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
) {
|
||||
try {
|
||||
if (!this._token.verifyCSRF(req)) {
|
||||
if (!this.tokenService.verifyCSRF(req)) {
|
||||
throw new BadRequestException('Invalid session. Please try again.');
|
||||
}
|
||||
|
||||
@ -107,15 +107,18 @@ export class SettingsController {
|
||||
throw new BadRequestException('Avatar upload failed');
|
||||
}
|
||||
|
||||
const matches = await this._upload.checkImageAspect(file);
|
||||
const matches = await this.uploadService.checkImageAspect(file);
|
||||
if (!matches) {
|
||||
throw new BadRequestException(
|
||||
'Avatar should be with a 1:1 aspect ratio.',
|
||||
);
|
||||
}
|
||||
|
||||
const upload = await this._upload.registerUploadedFile(file, req.user);
|
||||
await this._user.updateAvatar(req.user, upload);
|
||||
const upload = await this.uploadService.registerUploadedFile(
|
||||
file,
|
||||
req.user,
|
||||
);
|
||||
await this.userService.updateAvatar(req.user, upload);
|
||||
|
||||
return {
|
||||
file: upload.file,
|
||||
@ -128,7 +131,7 @@ export class SettingsController {
|
||||
|
||||
@Post('avatar/delete')
|
||||
public async deleteUserAvatar(@Req() req: Request, @Res() res: Response) {
|
||||
this._user.deleteAvatar(req.user);
|
||||
this.userService.deleteAvatar(req.user);
|
||||
req.flash('message', {
|
||||
error: false,
|
||||
text: 'Avatar removed successfully.',
|
||||
@ -139,8 +142,8 @@ export class SettingsController {
|
||||
@Get('oauth2')
|
||||
@Render('settings/oauth2')
|
||||
public async authorizations(@Req() req: Request) {
|
||||
const authorizations = await this._client.getAuthorizations(req.user);
|
||||
return this._form.populateTemplate(req, { authorizations });
|
||||
const authorizations = await this.clientService.getAuthorizations(req.user);
|
||||
return this.formService.populateTemplate(req, { authorizations });
|
||||
}
|
||||
|
||||
@Post('oauth2/revoke/:id')
|
||||
@ -149,7 +152,7 @@ export class SettingsController {
|
||||
@Res() res: Response,
|
||||
@Param('id') id: number,
|
||||
) {
|
||||
const getAuth = await this._client.getAuthorization(req.user, id);
|
||||
const getAuth = await this.clientService.getAuthorization(req.user, id);
|
||||
const jsreq =
|
||||
req.header('content-type').startsWith('application/json') ||
|
||||
req.header('accept').startsWith('application/json');
|
||||
@ -169,8 +172,8 @@ export class SettingsController {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._oaToken.wipeClientTokens(getAuth.client, req.user);
|
||||
await this._client.revokeAuthorization(getAuth);
|
||||
await this.oaTokenService.wipeClientTokens(getAuth.client, req.user);
|
||||
await this.clientService.revokeAuthorization(getAuth);
|
||||
|
||||
if (jsreq) {
|
||||
return res.json({ success: true });
|
||||
@ -186,8 +189,8 @@ export class SettingsController {
|
||||
const emailHint = `${mailSplit[0].substring(0, 1)}${asterisks}@${
|
||||
mailSplit[1]
|
||||
}`;
|
||||
const twofactor = await this._totp.userHasTOTP(req.user);
|
||||
return this._form.populateTemplate(req, {
|
||||
const twofactor = await this.totpService.userHasTOTP(req.user);
|
||||
return this.formService.populateTemplate(req, {
|
||||
user: req.user,
|
||||
emailHint,
|
||||
twofactor,
|
||||
@ -211,11 +214,13 @@ export class SettingsController {
|
||||
throw new Error('Please fill out all of the fields.');
|
||||
}
|
||||
|
||||
if (!(await this._user.comparePasswords(req.user.password, password))) {
|
||||
if (
|
||||
!(await this.tokenService.comparePasswords(req.user.password, password))
|
||||
) {
|
||||
throw new Error('Current password is invalid.');
|
||||
}
|
||||
|
||||
if (!new_password.match(this._form.passwordRegex)) {
|
||||
if (!new_password.match(this.formService.passwordRegex)) {
|
||||
throw new Error(
|
||||
'Password must be at least 8 characters long, contain a capital and lowercase letter and a number',
|
||||
);
|
||||
@ -233,10 +238,10 @@ export class SettingsController {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPassword = await this._user.hashPassword(new_password);
|
||||
const newPassword = await this.tokenService.hashPassword(new_password);
|
||||
req.user.password = newPassword;
|
||||
await this._user.updateUser(req.user);
|
||||
await this._audit.auditRequest(
|
||||
await this.userService.updateUser(req.user);
|
||||
await this.auditService.auditRequest(
|
||||
req,
|
||||
AuditAction.PASSWORD_CHANGE,
|
||||
'settings',
|
||||
@ -270,13 +275,13 @@ export class SettingsController {
|
||||
throw new Error('The current email address is invalid.');
|
||||
}
|
||||
|
||||
if (!email.match(this._form.emailRegex)) {
|
||||
if (!email.match(this.formService.emailRegex)) {
|
||||
throw new Error('The new email address is invalid.');
|
||||
}
|
||||
|
||||
if (
|
||||
!current_password ||
|
||||
!(await this._user.comparePasswords(
|
||||
!(await this.tokenService.comparePasswords(
|
||||
req.user.password,
|
||||
current_password,
|
||||
))
|
||||
@ -284,7 +289,7 @@ export class SettingsController {
|
||||
throw new Error('Current password is invalid.');
|
||||
}
|
||||
|
||||
const existing = await this._user.getByEmail(email);
|
||||
const existing = await this.userService.getByEmail(email);
|
||||
if (existing) {
|
||||
throw new Error(
|
||||
'There is already an existing user with this email address.',
|
||||
@ -300,8 +305,12 @@ export class SettingsController {
|
||||
}
|
||||
|
||||
req.user.email = email;
|
||||
await this._user.updateUser(req.user);
|
||||
await this._audit.auditRequest(req, AuditAction.EMAIL_CHANGE, 'settings');
|
||||
await this.userService.updateUser(req.user);
|
||||
await this.auditService.auditRequest(
|
||||
req,
|
||||
AuditAction.EMAIL_CHANGE,
|
||||
'settings',
|
||||
);
|
||||
|
||||
req.flash('message', {
|
||||
error: false,
|
||||
@ -316,7 +325,7 @@ export class SettingsController {
|
||||
@Res() res: Response,
|
||||
@Query('csrf') csrf: string,
|
||||
) {
|
||||
if (!this._token.verifyCSRF(req, csrf)) {
|
||||
if (!this.tokenService.verifyCSRF(req, csrf)) {
|
||||
throw new BadRequestException('Invalid csrf token');
|
||||
}
|
||||
|
||||
@ -326,9 +335,12 @@ export class SettingsController {
|
||||
@Get('logins')
|
||||
@Render('login-list')
|
||||
public async userLogins(@Req() req: Request) {
|
||||
const logins = await this._audit.getUserLogins(req.user, req.session.id);
|
||||
const creation = await this._audit.getUserAccountCreation(req.user);
|
||||
return this._form.populateTemplate(req, {
|
||||
const logins = await this.auditService.getUserLogins(
|
||||
req.user,
|
||||
req.session.id,
|
||||
);
|
||||
const creation = await this.auditService.getUserAccountCreation(req.user);
|
||||
return this.formService.populateTemplate(req, {
|
||||
logins,
|
||||
creation,
|
||||
});
|
||||
|
@ -34,9 +34,8 @@ export class TwoFactorController {
|
||||
if (!twoFA) {
|
||||
const challengeString = req.query.challenge as string;
|
||||
if (challengeString) {
|
||||
const challenge = await this.token.decryptChallenge<ChallengeType>(
|
||||
challengeString,
|
||||
);
|
||||
const challenge =
|
||||
await this.token.decryptChallenge<ChallengeType>(challengeString);
|
||||
if (
|
||||
challenge.type === 'totp' &&
|
||||
challenge.user === req.user.uuid &&
|
||||
@ -87,9 +86,8 @@ export class TwoFactorController {
|
||||
throw new Error('Invalid request');
|
||||
}
|
||||
|
||||
const challenge = await this.token.decryptChallenge<ChallengeType>(
|
||||
challengeString,
|
||||
);
|
||||
const challenge =
|
||||
await this.token.decryptChallenge<ChallengeType>(challengeString);
|
||||
secret = challenge.secret;
|
||||
|
||||
if (
|
||||
@ -151,7 +149,7 @@ export class TwoFactorController {
|
||||
}
|
||||
|
||||
if (
|
||||
!(await this.user.comparePasswords(req.user.password, body.password))
|
||||
!(await this.token.comparePasswords(req.user.password, body.password))
|
||||
) {
|
||||
throw new Error('The entered password is invalid.');
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as crypto from 'crypto';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||
import { v4 } from 'uuid';
|
||||
import * as CSRF from 'csrf';
|
||||
import CSRF from 'csrf';
|
||||
import { Request } from 'express';
|
||||
|
||||
const IV_LENGTH = 16;
|
||||
@ -37,6 +38,18 @@ export class TokenService {
|
||||
return v4();
|
||||
}
|
||||
|
||||
public async comparePasswords(
|
||||
hash: string,
|
||||
password: string,
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
public async hashPassword(password: string): Promise<string> {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
return bcrypt.hash(password, salt);
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/q/52212430
|
||||
/**
|
||||
* Symmetric encryption function
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
Reference in New Issue
Block a user