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 { ConfigurationService } from './modules/config/config.service';
|
||||
import { ApiModule } from './modules/api/api.module';
|
||||
import { AccountApiModule } from './modules/api/account/account.module';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@ -44,7 +45,7 @@ async function bootstrap() {
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, docBuilder, {
|
||||
include: [ApiModule, AdminApiModule, OAuth2RouterModule],
|
||||
include: [ApiModule, AdminApiModule, OAuth2RouterModule, AccountApiModule],
|
||||
});
|
||||
|
||||
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 { OAuth2Module } from 'src/modules/oauth2/oauth2.module';
|
||||
import { ObjectsModule } from 'src/modules/objects/objects.module';
|
||||
import { OAuth2AdminController } from './oauth2-admin.controller';
|
||||
import { PrivilegeAdminController } from './privilege-admin.controller';
|
||||
import { UserAdminController } from './user-admin.controller';
|
||||
import { OAuth2AdminController } from './controllers/oauth2-admin.controller';
|
||||
import { PrivilegeAdminController } from './controllers/privilege-admin.controller';
|
||||
import { UserAdminController } from './controllers/user-admin.controller';
|
||||
import { ConfigurationModule } from 'src/modules/config/config.module';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AuditAdminController } from './audit-admin.controller';
|
||||
import { AdminService } from './services/admin.service';
|
||||
import { AuditAdminController } from './controllers/audit-admin.controller';
|
||||
|
||||
@Module({
|
||||
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 { TokenService } from 'src/modules/utility/services/token.service';
|
||||
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 { 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 { User } from 'src/modules/objects/user/user.entity';
|
||||
|
||||
const UNPRIVILEGED_STRIP = ['id_token', 'management', 'implicit'];
|
||||
const UNPRIVILEGED_STRIP = ['id_token', 'management', 'implicit', 'account'];
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
@ -7,6 +7,7 @@ import { OAuth2Service } from '../oauth2/oauth2.service';
|
||||
import { ObjectsModule } from '../objects/objects.module';
|
||||
import { AdminApiModule } from './admin/admin.module';
|
||||
import { ApiController } from './api.controller';
|
||||
import { AccountApiModule } from './account/account.module';
|
||||
|
||||
@Module({
|
||||
controllers: [ApiController],
|
||||
@ -14,8 +15,9 @@ import { ApiController } from './api.controller';
|
||||
ConfigurationModule,
|
||||
JWTModule,
|
||||
ObjectsModule,
|
||||
AdminApiModule,
|
||||
OAuth2Module,
|
||||
AdminApiModule,
|
||||
AccountApiModule,
|
||||
],
|
||||
})
|
||||
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: {
|
||||
app: {
|
||||
base_url: 'http://localhost:3000',
|
||||
fe_url: 'http://localhost:5173',
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
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> = {
|
||||
email: 'Email address',
|
||||
picture: 'Profile picture',
|
||||
account: 'Password and other account settings',
|
||||
};
|
||||
|
||||
const ALWAYS_AVAILABLE = ['Username and display name'];
|
||||
@ -39,6 +40,10 @@ export class OAuth2Service implements OAuth2AdapterModel {
|
||||
disallowedScopes = null;
|
||||
}
|
||||
|
||||
if (scope.includes('account')) {
|
||||
disallowedScopes = null;
|
||||
}
|
||||
|
||||
res.render('authorize', {
|
||||
csrf: req.csrfToken(),
|
||||
user: req.user,
|
||||
|
@ -26,6 +26,7 @@ export class OAuth2ClientService {
|
||||
'email',
|
||||
'privileges',
|
||||
'management',
|
||||
'account',
|
||||
'openid',
|
||||
];
|
||||
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Privilege {
|
||||
@PrimaryGeneratedColumn()
|
||||
@ApiProperty()
|
||||
id: number;
|
||||
|
||||
@Column({ type: 'text', nullable: false })
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
}
|
||||
|
||||
|
@ -7,19 +7,26 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../user/user.entity';
|
||||
import { Exclude, Expose } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
@Entity()
|
||||
@Expose()
|
||||
export class Upload {
|
||||
@PrimaryGeneratedColumn()
|
||||
@ApiProperty()
|
||||
id: number;
|
||||
|
||||
@Column({ nullable: false })
|
||||
@ApiProperty()
|
||||
original_name: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
@ApiProperty()
|
||||
mimetype: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
@ApiProperty()
|
||||
file: string;
|
||||
|
||||
@ManyToOne(() => User, {
|
||||
@ -27,11 +34,14 @@ export class Upload {
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
})
|
||||
@Exclude()
|
||||
uploader: User;
|
||||
|
||||
@CreateDateColumn()
|
||||
@ApiProperty()
|
||||
public created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
@ApiProperty()
|
||||
public updated_at: Date;
|
||||
}
|
||||
|
@ -10,39 +10,53 @@ import {
|
||||
} from 'typeorm';
|
||||
import { Privilege } from '../privilege/privilege.entity';
|
||||
import { Upload } from '../upload/upload.entity';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Exclude, Expose } from 'class-transformer';
|
||||
|
||||
@Entity()
|
||||
@Expose()
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
@ApiProperty()
|
||||
id: number;
|
||||
|
||||
@Column({ type: 'uuid', length: 36, nullable: false, unique: true })
|
||||
@ApiProperty()
|
||||
uuid: string;
|
||||
|
||||
@Column({ length: 26, nullable: false, unique: true })
|
||||
@ApiProperty()
|
||||
username: string;
|
||||
|
||||
@Column({ nullable: false, unique: true })
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
|
||||
@Column({ length: 32, nullable: false })
|
||||
@ApiProperty()
|
||||
display_name: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
@Exclude()
|
||||
password: string;
|
||||
|
||||
@Column({ default: false })
|
||||
@ApiProperty()
|
||||
activated: boolean;
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
@ApiProperty()
|
||||
public activity_at: Date;
|
||||
|
||||
@CreateDateColumn()
|
||||
@ApiProperty()
|
||||
public created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
@ApiProperty()
|
||||
public updated_at: Date;
|
||||
|
||||
@ApiProperty({ type: () => Upload })
|
||||
@ManyToOne(() => Upload, {
|
||||
nullable: true,
|
||||
onDelete: 'SET NULL',
|
||||
@ -50,6 +64,7 @@ export class User {
|
||||
})
|
||||
public picture: Upload;
|
||||
|
||||
@ApiProperty({ type: Privilege, isArray: true })
|
||||
@ManyToMany(() => Privilege)
|
||||
@JoinTable()
|
||||
public privileges: Privilege[];
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { Request, Response } from 'express';
|
||||
import { SessionData } from 'express-session';
|
||||
import { LoginAntispamGuard } from 'src/guards/login-antispam.guard';
|
||||
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
||||
import {
|
||||
@ -35,7 +34,6 @@ interface VerifyChallenge {
|
||||
}
|
||||
|
||||
@Controller('/login')
|
||||
@UseGuards(LoginAntispamGuard)
|
||||
export class LoginController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
@ -60,6 +58,8 @@ export class LoginController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||
@UseGuards(ThrottlerGuard)
|
||||
public async loginRequest(
|
||||
@Req() req: Request,
|
||||
@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 { SessionModule } from '../session/session.module';
|
||||
import { LoginController } from './login.controller';
|
||||
import { IPLimitModule } from 'src/modules/iplimit/iplimit.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
UserTokenModule,
|
||||
AuditModule,
|
||||
SessionModule,
|
||||
IPLimitModule,
|
||||
],
|
||||
imports: [UserModule, UserTokenModule, AuditModule, SessionModule],
|
||||
controllers: [LoginController],
|
||||
})
|
||||
export class LoginModule implements NestModule {
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { Request, Response } from 'express';
|
||||
import { LoginAntispamGuard } from 'src/guards/login-antispam.guard';
|
||||
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||
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';
|
||||
|
||||
@Controller('/register')
|
||||
@UseGuards(LoginAntispamGuard)
|
||||
export class RegisterController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
@ -67,7 +65,7 @@ export class RegisterController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Throttle({ default: { limit: 3, ttl: 10000 } })
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(ThrottlerGuard)
|
||||
public async registerRequest(
|
||||
@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 { SessionModule } from '../session/session.module';
|
||||
import { RegisterController } from './register.controller';
|
||||
import { IPLimitModule } from 'src/modules/iplimit/iplimit.module';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule, AuditModule, SessionModule, IPLimitModule],
|
||||
imports: [UserModule, AuditModule, SessionModule],
|
||||
controllers: [RegisterController],
|
||||
})
|
||||
export class RegisterModule implements NestModule {
|
||||
|
@ -15,9 +15,24 @@ export class WellKnownController {
|
||||
@Get('security.txt')
|
||||
securityTXT(@Res({ passthrough: true }) res: Response) {
|
||||
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
|
||||
# you may report it to:
|
||||
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
|
||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||
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
|
||||
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
|
||||
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
|
||||
button.btn.btn-primary(type="submit") Log in
|
||||
div.btn-group.align-self-end
|
||||
|
@ -21,18 +21,35 @@ block body
|
||||
div.form-container
|
||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||
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.
|
||||
|
||||
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
|
||||
else
|
||||
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")
|
||||
div.form-container
|
||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||
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
|
||||
a.btn.btn-link.align-self-end(type="button" href="/login") Log in instead
|
||||
|
@ -19,6 +19,19 @@ block body
|
||||
form(method="post")
|
||||
div.form-container
|
||||
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
|
||||
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
|
||||
|
@ -22,23 +22,51 @@ block body
|
||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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")
|
||||
div.form-container
|
||||
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
|
||||
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
|
||||
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.
|
||||
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
|
||||
.col
|
||||
h2 Change Email Address
|
||||
form(method="post", action="/account/security/email", autocomplete="off")
|
||||
div.form-container
|
||||
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
|
||||
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
|
||||
input.form-control#current_email(type="email", name="current_email")
|
||||
small.form-hint Hint: #{emailHint}
|
||||
|
Reference in New Issue
Block a user