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