final commit
This commit is contained in:
parent
ba96b8fbee
commit
bc570074b0
@ -1,63 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
CanActivate,
|
|
||||||
ExecutionContext,
|
|
||||||
HttpException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Request } from 'express';
|
|
||||||
import { IPLimitService } from 'src/modules/iplimit/iplimit.service';
|
|
||||||
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
|
||||||
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class LoginAntispamGuard implements CanActivate {
|
|
||||||
constructor(
|
|
||||||
private iplimit: IPLimitService,
|
|
||||||
private audit: AuditService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
||||||
const request = context.switchToHttp().getRequest<Request>();
|
|
||||||
|
|
||||||
if (['GET', 'OPTIONS'].includes(request.method)) return true;
|
|
||||||
|
|
||||||
const known = await this.iplimit.getAddressLimit(request.ip);
|
|
||||||
if (known && known.attempts > 10) {
|
|
||||||
if (known.attempts > 15) {
|
|
||||||
let reported = false;
|
|
||||||
if (!known.reported) {
|
|
||||||
reported = true;
|
|
||||||
await this.audit.insertAudit(
|
|
||||||
AuditAction.THROTTLE,
|
|
||||||
`antispam-guard ${known.attempts} attempts`,
|
|
||||||
undefined,
|
|
||||||
request.ip,
|
|
||||||
request.header('user-agent'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const limitMinutes = known.attempts > 15 ? 30 : 10; // Half-Hour
|
|
||||||
await this.iplimit.limitUntil(
|
|
||||||
request.ip,
|
|
||||||
limitMinutes * 60 * 1000,
|
|
||||||
reported,
|
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, known.attempts * 1000),
|
|
||||||
);
|
|
||||||
|
|
||||||
throw new HttpException(
|
|
||||||
`Too Many Requests. Try again in ${limitMinutes} minutes.`,
|
|
||||||
429,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.iplimit.limitUntil(request.ip, 30 * 1000); // 30 seconds
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,6 +9,7 @@ import { AdminApiModule } from './modules/api/admin/admin.module';
|
|||||||
import { OAuth2RouterModule } from './modules/ssr-front-end/oauth2-router/oauth2-router.module';
|
import { OAuth2RouterModule } from './modules/ssr-front-end/oauth2-router/oauth2-router.module';
|
||||||
import { ConfigurationService } from './modules/config/config.service';
|
import { ConfigurationService } from './modules/config/config.service';
|
||||||
import { ApiModule } from './modules/api/api.module';
|
import { ApiModule } from './modules/api/api.module';
|
||||||
|
import { AccountApiModule } from './modules/api/account/account.module';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ async function bootstrap() {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
const document = SwaggerModule.createDocument(app, docBuilder, {
|
const document = SwaggerModule.createDocument(app, docBuilder, {
|
||||||
include: [ApiModule, AdminApiModule, OAuth2RouterModule],
|
include: [ApiModule, AdminApiModule, OAuth2RouterModule, AccountApiModule],
|
||||||
});
|
});
|
||||||
|
|
||||||
SwaggerModule.setup('api/openapi', app, document);
|
SwaggerModule.setup('api/openapi', app, document);
|
||||||
|
34
src/modules/api/account/account.module.ts
Normal file
34
src/modules/api/account/account.module.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||||
|
import * as cors from 'cors';
|
||||||
|
import { ConfigurationModule } from 'src/modules/config/config.module';
|
||||||
|
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||||
|
import { JWTModule } from 'src/modules/jwt/jwt.module';
|
||||||
|
import { OAuth2Module } from 'src/modules/oauth2/oauth2.module';
|
||||||
|
import { ObjectsModule } from 'src/modules/objects/objects.module';
|
||||||
|
import { AccountApiController } from './controllers/account.controller';
|
||||||
|
import { AccountApiService } from './services/account.service';
|
||||||
|
import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AccountApiController],
|
||||||
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
JWTModule,
|
||||||
|
ObjectsModule,
|
||||||
|
UserTokenModule,
|
||||||
|
OAuth2Module,
|
||||||
|
AccountApiModule,
|
||||||
|
],
|
||||||
|
providers: [AccountApiService],
|
||||||
|
})
|
||||||
|
export class AccountApiModule implements NestModule {
|
||||||
|
constructor(private readonly config: ConfigurationService) {}
|
||||||
|
|
||||||
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
const corsOpts = cors({
|
||||||
|
origin: [this.config.get('app.base_url'), this.config.get('app.fe_url')],
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
consumer.apply(corsOpts).forRoutes(AccountApiController);
|
||||||
|
}
|
||||||
|
}
|
45
src/modules/api/account/controllers/account.controller.ts
Normal file
45
src/modules/api/account/controllers/account.controller.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
ClassSerializerInterceptor,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
|
UsePipes,
|
||||||
|
ValidationPipe,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiTags,
|
||||||
|
ApiOAuth2,
|
||||||
|
ApiOperation,
|
||||||
|
ApiOkResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { Scopes } from 'src/decorators/scopes.decorator';
|
||||||
|
import { OAuth2Guard } from 'src/guards/oauth2.guard';
|
||||||
|
import { PrivilegesGuard } from 'src/guards/privileges.guard';
|
||||||
|
import { ScopesGuard } from 'src/guards/scopes.guard';
|
||||||
|
import { AccountApiService } from '../services/account.service';
|
||||||
|
import { CurrentUser } from 'src/decorators/user.decorator';
|
||||||
|
import { User } from 'src/modules/objects/user/user.entity';
|
||||||
|
import { AccountResponseDto } from '../../dtos/account.dto';
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: '/api/account',
|
||||||
|
})
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiTags('account')
|
||||||
|
@ApiOAuth2(['account'])
|
||||||
|
@Scopes('account')
|
||||||
|
@UseInterceptors(ClassSerializerInterceptor)
|
||||||
|
@UsePipes(new ValidationPipe({ whitelist: true }))
|
||||||
|
@UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard)
|
||||||
|
export class AccountApiController {
|
||||||
|
constructor(private readonly service: AccountApiService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'Get account details' })
|
||||||
|
@ApiOkResponse({ type: AccountResponseDto })
|
||||||
|
async getAccount(@CurrentUser() user: User) {
|
||||||
|
return this.service.getAccountInfo(user);
|
||||||
|
}
|
||||||
|
}
|
29
src/modules/api/account/services/account.service.ts
Normal file
29
src/modules/api/account/services/account.service.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service';
|
||||||
|
import { User } from 'src/modules/objects/user/user.entity';
|
||||||
|
import { UserService } from 'src/modules/objects/user/user.service';
|
||||||
|
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||||
|
import { AccountResponseDto } from '../../dtos/account.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccountApiService {
|
||||||
|
constructor(
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly userTotp: UserTOTPService,
|
||||||
|
private readonly form: FormUtilityService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getAccountInfo(user: User) {
|
||||||
|
const fullInfo = await this.userService.getByUUID(user.uuid, [
|
||||||
|
'picture',
|
||||||
|
'privileges',
|
||||||
|
]);
|
||||||
|
const hasTotp = await this.userTotp.userHasTOTP(fullInfo);
|
||||||
|
|
||||||
|
return plainToInstance(AccountResponseDto, {
|
||||||
|
...fullInfo,
|
||||||
|
totp_enabled: hasTotp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -5,12 +5,12 @@ import * as mime from 'mime-types';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { OAuth2Module } from 'src/modules/oauth2/oauth2.module';
|
import { OAuth2Module } from 'src/modules/oauth2/oauth2.module';
|
||||||
import { ObjectsModule } from 'src/modules/objects/objects.module';
|
import { ObjectsModule } from 'src/modules/objects/objects.module';
|
||||||
import { OAuth2AdminController } from './oauth2-admin.controller';
|
import { OAuth2AdminController } from './controllers/oauth2-admin.controller';
|
||||||
import { PrivilegeAdminController } from './privilege-admin.controller';
|
import { PrivilegeAdminController } from './controllers/privilege-admin.controller';
|
||||||
import { UserAdminController } from './user-admin.controller';
|
import { UserAdminController } from './controllers/user-admin.controller';
|
||||||
import { ConfigurationModule } from 'src/modules/config/config.module';
|
import { ConfigurationModule } from 'src/modules/config/config.module';
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './services/admin.service';
|
||||||
import { AuditAdminController } from './audit-admin.controller';
|
import { AuditAdminController } from './controllers/audit-admin.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [
|
controllers: [
|
||||||
|
@ -36,7 +36,7 @@ import { FormUtilityService } from 'src/modules/utility/services/form-utility.se
|
|||||||
import { PaginationService } from 'src/modules/utility/services/paginate.service';
|
import { PaginationService } from 'src/modules/utility/services/paginate.service';
|
||||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||||
import { PageOptions } from 'src/types/pagination.interfaces';
|
import { PageOptions } from 'src/types/pagination.interfaces';
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from '../services/admin.service';
|
||||||
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||||
|
|
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { OAuth2Client } from 'src/modules/objects/oauth2-client/oauth2-client.entity';
|
import { OAuth2Client } from 'src/modules/objects/oauth2-client/oauth2-client.entity';
|
||||||
import { User } from 'src/modules/objects/user/user.entity';
|
import { User } from 'src/modules/objects/user/user.entity';
|
||||||
|
|
||||||
const UNPRIVILEGED_STRIP = ['id_token', 'management', 'implicit'];
|
const UNPRIVILEGED_STRIP = ['id_token', 'management', 'implicit', 'account'];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
@ -7,6 +7,7 @@ import { OAuth2Service } from '../oauth2/oauth2.service';
|
|||||||
import { ObjectsModule } from '../objects/objects.module';
|
import { ObjectsModule } from '../objects/objects.module';
|
||||||
import { AdminApiModule } from './admin/admin.module';
|
import { AdminApiModule } from './admin/admin.module';
|
||||||
import { ApiController } from './api.controller';
|
import { ApiController } from './api.controller';
|
||||||
|
import { AccountApiModule } from './account/account.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [ApiController],
|
controllers: [ApiController],
|
||||||
@ -14,8 +15,9 @@ import { ApiController } from './api.controller';
|
|||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
JWTModule,
|
JWTModule,
|
||||||
ObjectsModule,
|
ObjectsModule,
|
||||||
AdminApiModule,
|
|
||||||
OAuth2Module,
|
OAuth2Module,
|
||||||
|
AdminApiModule,
|
||||||
|
AccountApiModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ApiModule implements NestModule {
|
export class ApiModule implements NestModule {
|
||||||
|
7
src/modules/api/dtos/account.dto.ts
Normal file
7
src/modules/api/dtos/account.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { ApiProperty, OmitType } from '@nestjs/swagger';
|
||||||
|
import { User } from 'src/modules/objects/user/user.entity';
|
||||||
|
|
||||||
|
export class AccountResponseDto extends OmitType(User, ['password']) {
|
||||||
|
@ApiProperty()
|
||||||
|
totp_enabled: boolean;
|
||||||
|
}
|
@ -17,6 +17,7 @@ export const configProviders = [
|
|||||||
useValue: {
|
useValue: {
|
||||||
app: {
|
app: {
|
||||||
base_url: 'http://localhost:3000',
|
base_url: 'http://localhost:3000',
|
||||||
|
fe_url: 'http://localhost:5173',
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 3000,
|
port: 3000,
|
||||||
session_name: '__sid',
|
session_name: '__sid',
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { IPLimitService } from './iplimit.service';
|
|
||||||
import { CommonCacheModule } from '../cache/cache.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [CommonCacheModule],
|
|
||||||
providers: [IPLimitService],
|
|
||||||
exports: [IPLimitService],
|
|
||||||
})
|
|
||||||
export class IPLimitModule {}
|
|
@ -1,47 +0,0 @@
|
|||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { Cache } from 'cache-manager';
|
|
||||||
import { TokenService } from '../utility/services/token.service';
|
|
||||||
|
|
||||||
export interface IPLimit {
|
|
||||||
ip: string;
|
|
||||||
attempts: number;
|
|
||||||
reported: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class IPLimitService {
|
|
||||||
constructor(
|
|
||||||
@Inject(CACHE_MANAGER)
|
|
||||||
private readonly cache: Cache,
|
|
||||||
private readonly token: TokenService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async getAddressLimit(ip: string) {
|
|
||||||
const ipHash = this.token.insecureHash(ip);
|
|
||||||
const entry = await this.cache.get<IPLimit>(`iplimit-${ipHash}`);
|
|
||||||
if (!entry) return null;
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async limitUntil(ip: string, expires: number, reported = false) {
|
|
||||||
const ipHash = this.token.insecureHash(ip);
|
|
||||||
const existing = await this.cache.get<IPLimit>(`iplimit-${ipHash}`);
|
|
||||||
if (existing) {
|
|
||||||
existing.attempts++;
|
|
||||||
if (reported) existing.reported = true;
|
|
||||||
await this.cache.set(`iplimit-${ipHash}`, existing, expires + Date.now());
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newObj = {
|
|
||||||
ip,
|
|
||||||
attempts: 0,
|
|
||||||
reported,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.cache.set(`iplimit-${ipHash}`, newObj, expires + Date.now());
|
|
||||||
|
|
||||||
return newObj;
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,6 +11,7 @@ import { UserAdapter } from './adapter/user.adapter';
|
|||||||
const SCOPE_DESCRIPTION: Record<string, string> = {
|
const SCOPE_DESCRIPTION: Record<string, string> = {
|
||||||
email: 'Email address',
|
email: 'Email address',
|
||||||
picture: 'Profile picture',
|
picture: 'Profile picture',
|
||||||
|
account: 'Password and other account settings',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ALWAYS_AVAILABLE = ['Username and display name'];
|
const ALWAYS_AVAILABLE = ['Username and display name'];
|
||||||
@ -39,6 +40,10 @@ export class OAuth2Service implements OAuth2AdapterModel {
|
|||||||
disallowedScopes = null;
|
disallowedScopes = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scope.includes('account')) {
|
||||||
|
disallowedScopes = null;
|
||||||
|
}
|
||||||
|
|
||||||
res.render('authorize', {
|
res.render('authorize', {
|
||||||
csrf: req.csrfToken(),
|
csrf: req.csrfToken(),
|
||||||
user: req.user,
|
user: req.user,
|
||||||
|
@ -26,6 +26,7 @@ export class OAuth2ClientService {
|
|||||||
'email',
|
'email',
|
||||||
'privileges',
|
'privileges',
|
||||||
'management',
|
'management',
|
||||||
|
'account',
|
||||||
'openid',
|
'openid',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Privilege {
|
export class Privilege {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@ApiProperty()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: false })
|
@Column({ type: 'text', nullable: false })
|
||||||
|
@ApiProperty()
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,19 +7,26 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from '../user/user.entity';
|
import { User } from '../user/user.entity';
|
||||||
|
import { Exclude, Expose } from 'class-transformer';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@Expose()
|
||||||
export class Upload {
|
export class Upload {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@ApiProperty()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
|
@ApiProperty()
|
||||||
original_name: string;
|
original_name: string;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
|
@ApiProperty()
|
||||||
mimetype: string;
|
mimetype: string;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
|
@ApiProperty()
|
||||||
file: string;
|
file: string;
|
||||||
|
|
||||||
@ManyToOne(() => User, {
|
@ManyToOne(() => User, {
|
||||||
@ -27,11 +34,14 @@ export class Upload {
|
|||||||
onDelete: 'SET NULL',
|
onDelete: 'SET NULL',
|
||||||
onUpdate: 'CASCADE',
|
onUpdate: 'CASCADE',
|
||||||
})
|
})
|
||||||
|
@Exclude()
|
||||||
uploader: User;
|
uploader: User;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
|
@ApiProperty()
|
||||||
public created_at: Date;
|
public created_at: Date;
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
|
@ApiProperty()
|
||||||
public updated_at: Date;
|
public updated_at: Date;
|
||||||
}
|
}
|
||||||
|
@ -10,39 +10,53 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Privilege } from '../privilege/privilege.entity';
|
import { Privilege } from '../privilege/privilege.entity';
|
||||||
import { Upload } from '../upload/upload.entity';
|
import { Upload } from '../upload/upload.entity';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Exclude, Expose } from 'class-transformer';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@Expose()
|
||||||
export class User {
|
export class User {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@ApiProperty()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column({ type: 'uuid', length: 36, nullable: false, unique: true })
|
@Column({ type: 'uuid', length: 36, nullable: false, unique: true })
|
||||||
|
@ApiProperty()
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
||||||
@Column({ length: 26, nullable: false, unique: true })
|
@Column({ length: 26, nullable: false, unique: true })
|
||||||
|
@ApiProperty()
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
@Column({ nullable: false, unique: true })
|
@Column({ nullable: false, unique: true })
|
||||||
|
@ApiProperty()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@Column({ length: 32, nullable: false })
|
@Column({ length: 32, nullable: false })
|
||||||
|
@ApiProperty()
|
||||||
display_name: string;
|
display_name: string;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
|
@Exclude()
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
|
@ApiProperty()
|
||||||
activated: boolean;
|
activated: boolean;
|
||||||
|
|
||||||
@Column({ type: 'timestamp' })
|
@Column({ type: 'timestamp' })
|
||||||
|
@ApiProperty()
|
||||||
public activity_at: Date;
|
public activity_at: Date;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
|
@ApiProperty()
|
||||||
public created_at: Date;
|
public created_at: Date;
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
|
@ApiProperty()
|
||||||
public updated_at: Date;
|
public updated_at: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ type: () => Upload })
|
||||||
@ManyToOne(() => Upload, {
|
@ManyToOne(() => Upload, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
onDelete: 'SET NULL',
|
onDelete: 'SET NULL',
|
||||||
@ -50,6 +64,7 @@ export class User {
|
|||||||
})
|
})
|
||||||
public picture: Upload;
|
public picture: Upload;
|
||||||
|
|
||||||
|
@ApiProperty({ type: Privilege, isArray: true })
|
||||||
@ManyToMany(() => Privilege)
|
@ManyToMany(() => Privilege)
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
public privileges: Privilege[];
|
public privileges: Privilege[];
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { SessionData } from 'express-session';
|
import { SessionData } from 'express-session';
|
||||||
import { LoginAntispamGuard } from 'src/guards/login-antispam.guard';
|
|
||||||
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||||
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
||||||
import {
|
import {
|
||||||
@ -35,7 +34,6 @@ interface VerifyChallenge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Controller('/login')
|
@Controller('/login')
|
||||||
@UseGuards(LoginAntispamGuard)
|
|
||||||
export class LoginController {
|
export class LoginController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
@ -60,6 +58,8 @@ export class LoginController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||||
|
@UseGuards(ThrottlerGuard)
|
||||||
public async loginRequest(
|
public async loginRequest(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
|
@ -9,16 +9,9 @@ import { UserTokenModule } from 'src/modules/objects/user-token/user-token.modul
|
|||||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||||
import { SessionModule } from '../session/session.module';
|
import { SessionModule } from '../session/session.module';
|
||||||
import { LoginController } from './login.controller';
|
import { LoginController } from './login.controller';
|
||||||
import { IPLimitModule } from 'src/modules/iplimit/iplimit.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [UserModule, UserTokenModule, AuditModule, SessionModule],
|
||||||
UserModule,
|
|
||||||
UserTokenModule,
|
|
||||||
AuditModule,
|
|
||||||
SessionModule,
|
|
||||||
IPLimitModule,
|
|
||||||
],
|
|
||||||
controllers: [LoginController],
|
controllers: [LoginController],
|
||||||
})
|
})
|
||||||
export class LoginModule implements NestModule {
|
export class LoginModule implements NestModule {
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { LoginAntispamGuard } from 'src/guards/login-antispam.guard';
|
|
||||||
import { ConfigurationService } from 'src/modules/config/config.service';
|
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||||
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||||
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
||||||
@ -20,7 +19,6 @@ import { FormUtilityService } from 'src/modules/utility/services/form-utility.se
|
|||||||
import { RegisterDto } from './register.interfaces';
|
import { RegisterDto } from './register.interfaces';
|
||||||
|
|
||||||
@Controller('/register')
|
@Controller('/register')
|
||||||
@UseGuards(LoginAntispamGuard)
|
|
||||||
export class RegisterController {
|
export class RegisterController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
@ -67,7 +65,7 @@ export class RegisterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@Throttle({ default: { limit: 3, ttl: 10000 } })
|
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||||
@UseGuards(ThrottlerGuard)
|
@UseGuards(ThrottlerGuard)
|
||||||
public async registerRequest(
|
public async registerRequest(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
|
@ -8,10 +8,9 @@ import { AuditModule } from 'src/modules/objects/audit/audit.module';
|
|||||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||||
import { SessionModule } from '../session/session.module';
|
import { SessionModule } from '../session/session.module';
|
||||||
import { RegisterController } from './register.controller';
|
import { RegisterController } from './register.controller';
|
||||||
import { IPLimitModule } from 'src/modules/iplimit/iplimit.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserModule, AuditModule, SessionModule, IPLimitModule],
|
imports: [UserModule, AuditModule, SessionModule],
|
||||||
controllers: [RegisterController],
|
controllers: [RegisterController],
|
||||||
})
|
})
|
||||||
export class RegisterModule implements NestModule {
|
export class RegisterModule implements NestModule {
|
||||||
|
@ -15,9 +15,24 @@ export class WellKnownController {
|
|||||||
@Get('security.txt')
|
@Get('security.txt')
|
||||||
securityTXT(@Res({ passthrough: true }) res: Response) {
|
securityTXT(@Res({ passthrough: true }) res: Response) {
|
||||||
res.set('content-type', 'text/plain');
|
res.set('content-type', 'text/plain');
|
||||||
|
const date = new Date();
|
||||||
|
date.setMonth(date.getMonth() + 6);
|
||||||
|
date.setDate(1);
|
||||||
|
date.setHours(0);
|
||||||
|
date.setMinutes(0);
|
||||||
|
date.setSeconds(0);
|
||||||
|
date.setMilliseconds(0);
|
||||||
return `# If you would like to report a security issue
|
return `# If you would like to report a security issue
|
||||||
# you may report it to:
|
# you may report it to:
|
||||||
Contact: mailto:evert@lunasqu.ee
|
Contact: mailto:evert@lunasqu.ee
|
||||||
|
|
||||||
|
# GnuPG public key
|
||||||
|
Encryption: https://lunasqu.ee/public/keys/pgp/Evert%20Prants.pub
|
||||||
|
|
||||||
|
# English and Estonian
|
||||||
|
Preferred-Languages: en, et
|
||||||
|
|
||||||
|
Expires: ${date.toISOString()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,11 +20,27 @@ block body
|
|||||||
div.form-container
|
div.form-container
|
||||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||||
label.form-label(for="username") Username
|
label.form-label(for="username") Username
|
||||||
input.form-control#username(type="text", name="username", placeholder="Username", autofocus, value=form.username)
|
input.form-control#username(
|
||||||
|
type="text",
|
||||||
|
name="username",
|
||||||
|
autocomplete="username",
|
||||||
|
placeholder="Username",
|
||||||
|
autofocus,
|
||||||
|
value=form.username
|
||||||
|
)
|
||||||
label.form-label(for="password") Password
|
label.form-label(for="password") Password
|
||||||
input.form-control#password(type="password", name="password", placeholder="Password")
|
input.form-control#password(
|
||||||
|
type="password",
|
||||||
|
name="password",
|
||||||
|
autocomplete="current-password",
|
||||||
|
placeholder="Password"
|
||||||
|
)
|
||||||
div.form-checkbox
|
div.form-checkbox
|
||||||
input.form-control#remember(type="checkbox", name="remember", checked=form.remember)
|
input.form-control#remember(
|
||||||
|
type="checkbox",
|
||||||
|
name="remember",
|
||||||
|
checked=form.remember
|
||||||
|
)
|
||||||
label(for="remember") Remember me
|
label(for="remember") Remember me
|
||||||
button.btn.btn-primary(type="submit") Log in
|
button.btn.btn-primary(type="submit") Log in
|
||||||
div.btn-group.align-self-end
|
div.btn-group.align-self-end
|
||||||
|
@ -21,18 +21,35 @@ block body
|
|||||||
div.form-container
|
div.form-container
|
||||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||||
label.form-label(for="password") New password
|
label.form-label(for="password") New password
|
||||||
input.form-control#password(type="password", name="password", autofocus, placeholder="Password")
|
input.form-control#password(
|
||||||
|
type="password",
|
||||||
|
name="password",
|
||||||
|
autocomplete="new-password",
|
||||||
|
autofocus,
|
||||||
|
placeholder="Password"
|
||||||
|
)
|
||||||
small.form-hint Must be at least 8 characters long, contain a capital and lowercase letter and a number.
|
small.form-hint Must be at least 8 characters long, contain a capital and lowercase letter and a number.
|
||||||
|
|
||||||
label.form-label(for="password_repeat") Repeat new password
|
label.form-label(for="password_repeat") Repeat new password
|
||||||
input.form-control#password_repeat(type="password", name="password_repeat", placeholder="Password")
|
input.form-control#password_repeat(
|
||||||
|
type="password",
|
||||||
|
name="password_repeat",
|
||||||
|
autocomplete="new-password",
|
||||||
|
placeholder="Password"
|
||||||
|
)
|
||||||
button.btn.btn-primary(type="submit") Set password
|
button.btn.btn-primary(type="submit") Set password
|
||||||
else
|
else
|
||||||
h1 Reset password
|
h1 Reset password
|
||||||
p If you have forgotten your password, please enter your accounts email address and we will send you a link to recover it.
|
p If you have forgotten your password, please enter your account email address and we will send you a link to recover it.
|
||||||
form(method="post")
|
form(method="post")
|
||||||
div.form-container
|
div.form-container
|
||||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||||
label.form-label(for="email") Email address
|
label.form-label(for="email") Email address
|
||||||
input.form-control#email(type="email", name="email", autofocus, placeholder="Email addres")
|
input.form-control#email(
|
||||||
|
type="email",
|
||||||
|
name="email",
|
||||||
|
autofocus,
|
||||||
|
placeholder="Email address"
|
||||||
|
)
|
||||||
button.btn.btn-primary(type="submit") Send recovery email
|
button.btn.btn-primary(type="submit") Send recovery email
|
||||||
a.btn.btn-link.align-self-end(type="button" href="/login") Log in instead
|
a.btn.btn-link.align-self-end(type="button" href="/login") Log in instead
|
||||||
|
@ -19,6 +19,19 @@ block body
|
|||||||
form(method="post")
|
form(method="post")
|
||||||
div.form-container
|
div.form-container
|
||||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||||
|
input#username(
|
||||||
|
type="text",
|
||||||
|
name="username",
|
||||||
|
value=user.username,
|
||||||
|
autocomplete="username",
|
||||||
|
style="display: none"
|
||||||
|
)
|
||||||
|
|
||||||
label.form-label(for="password") Password
|
label.form-label(for="password") Password
|
||||||
input.form-control#password(type="password", name="password", autofocus)
|
input.form-control#password(
|
||||||
|
type="password",
|
||||||
|
name="password",
|
||||||
|
autofocus,
|
||||||
|
autocomplete="current-password"
|
||||||
|
)
|
||||||
button.btn.btn-primary(type="submit") Submit
|
button.btn.btn-primary(type="submit") Submit
|
||||||
|
@ -22,23 +22,51 @@ block body
|
|||||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||||
|
|
||||||
label.form-label(for="username") Username
|
label.form-label(for="username") Username
|
||||||
input.form-control#username(type="text", name="username", placeholder="Username", autofocus, value=form.username)
|
input.form-control#username(
|
||||||
|
type="text",
|
||||||
|
name="username",
|
||||||
|
placeholder="Username",
|
||||||
|
autocomplete="username",
|
||||||
|
autofocus,
|
||||||
|
value=form.username
|
||||||
|
)
|
||||||
small.form-hint Between 3 and 26 English alphanumeric characters and .-_ only.
|
small.form-hint Between 3 and 26 English alphanumeric characters and .-_ only.
|
||||||
|
|
||||||
label.form-label(for="display_name") Display name
|
label.form-label(for="display_name") Display name
|
||||||
input.form-control#display_name(type="text", name="display_name", placeholder="Display name", value=form.display_name)
|
input.form-control#display_name(
|
||||||
|
type="text",
|
||||||
|
name="display_name",
|
||||||
|
placeholder="Display name",
|
||||||
|
value=form.display_name
|
||||||
|
)
|
||||||
small.form-hint Maximum length is 32.
|
small.form-hint Maximum length is 32.
|
||||||
|
|
||||||
label.form-label(for="email") Email address
|
label.form-label(for="email") Email address
|
||||||
input.form-control#email(type="email", name="email", placeholder="Email address", value=form.email)
|
input.form-control#email(
|
||||||
|
type="email",
|
||||||
|
name="email",
|
||||||
|
placeholder="Email address",
|
||||||
|
value=form.email
|
||||||
|
)
|
||||||
small.form-hint You will need to verify your email address before you can log in.
|
small.form-hint You will need to verify your email address before you can log in.
|
||||||
|
|
||||||
label.form-label(for="password") Password
|
label.form-label(for="password") Password
|
||||||
input.form-control#password(type="password", name="password", placeholder="Password", value=form.password)
|
input.form-control#password(
|
||||||
|
type="password",
|
||||||
|
name="password",
|
||||||
|
autocomplete="new-password",
|
||||||
|
placeholder="Password",
|
||||||
|
value=form.password
|
||||||
|
)
|
||||||
small.form-hint Must be at least 8 characters long, contain a capital and lowercase letter and a number.
|
small.form-hint Must be at least 8 characters long, contain a capital and lowercase letter and a number.
|
||||||
|
|
||||||
label.form-label(for="password_repeat") Confirm password
|
label.form-label(for="password_repeat") Confirm password
|
||||||
input.form-control#password_repeat(type="password", name="password_repeat", placeholder="Confirm password")
|
input.form-control#password_repeat(
|
||||||
|
type="password",
|
||||||
|
name="password_repeat",
|
||||||
|
autocomplete="new-password",
|
||||||
|
placeholder="Confirm password"
|
||||||
|
)
|
||||||
|
|
||||||
button.btn.btn-primary(type="submit") Create a new account
|
button.btn.btn-primary(type="submit") Create a new account
|
||||||
a.btn.btn-link.align-self-end(type="button" href="/login") Log in instead
|
a.btn.btn-link.align-self-end(type="button" href="/login") Log in instead
|
||||||
|
@ -18,21 +18,23 @@ block settings
|
|||||||
form(method="post", action="/account/security/password", autocomplete="off")
|
form(method="post", action="/account/security/password", autocomplete="off")
|
||||||
div.form-container
|
div.form-container
|
||||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||||
|
input#username(type="text", name="username", value=user.username, autocomplete="username", style="display: none")
|
||||||
label.form-label(for="password") Current Password
|
label.form-label(for="password") Current Password
|
||||||
input.form-control#password(type="password", name="password")
|
input.form-control#password(type="password", name="password", autocomplete="current-password")
|
||||||
label.form-label(for="new_password") New Password
|
label.form-label(for="new_password") New Password
|
||||||
input.form-control#new_password(type="password", name="new_password", autocomplete="new-password")
|
input.form-control#new_password(type="password", name="new_password", autocomplete="new-password")
|
||||||
small.form-hint At least 8 characters, a capital letter and a number required.
|
small.form-hint At least 8 characters, a capital letter and a number required.
|
||||||
label.form-label(for="password_repeat") Repeat new password
|
label.form-label(for="password_repeat") Repeat new password
|
||||||
input.form-control#password_repeat(type="password", name="password_repeat")
|
input.form-control#password_repeat(type="password", name="password_repeat", autocomplete="new-password")
|
||||||
button.btn.btn-primary(type="submit") Change
|
button.btn.btn-primary(type="submit") Change
|
||||||
.col
|
.col
|
||||||
h2 Change Email Address
|
h2 Change Email Address
|
||||||
form(method="post", action="/account/security/email", autocomplete="off")
|
form(method="post", action="/account/security/email", autocomplete="off")
|
||||||
div.form-container
|
div.form-container
|
||||||
input(type="hidden", name="_csrf", value=csrf)
|
input(type="hidden", name="_csrf", value=csrf)
|
||||||
|
input#email_username(type="text", name="username", value=user.username, autocomplete="username", style="display: none")
|
||||||
label.form-label(for="current_password") Current Password
|
label.form-label(for="current_password") Current Password
|
||||||
input.form-control#current_password(type="password", name="current_password")
|
input.form-control#current_password(type="password", name="current_password", autocomplete="current-password")
|
||||||
label.form-label(for="current_email") Current Email Address
|
label.form-label(for="current_email") Current Email Address
|
||||||
input.form-control#current_email(type="email", name="current_email")
|
input.form-control#current_email(type="email", name="current_email")
|
||||||
small.form-hint Hint: #{emailHint}
|
small.form-hint Hint: #{emailHint}
|
||||||
|
Reference in New Issue
Block a user