diff --git a/src/guards/login-antispam.guard.ts b/src/guards/login-antispam.guard.ts deleted file mode 100644 index 2f179df..0000000 --- a/src/guards/login-antispam.guard.ts +++ /dev/null @@ -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 { - const request = context.switchToHttp().getRequest(); - - 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; - } -} diff --git a/src/main.ts b/src/main.ts index bb82f39..0341381 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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); diff --git a/src/modules/api/account/account.module.ts b/src/modules/api/account/account.module.ts new file mode 100644 index 0000000..bd5a35f --- /dev/null +++ b/src/modules/api/account/account.module.ts @@ -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); + } +} diff --git a/src/modules/api/account/controllers/account.controller.ts b/src/modules/api/account/controllers/account.controller.ts new file mode 100644 index 0000000..27f6f1b --- /dev/null +++ b/src/modules/api/account/controllers/account.controller.ts @@ -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); + } +} diff --git a/src/modules/api/account/services/account.service.ts b/src/modules/api/account/services/account.service.ts new file mode 100644 index 0000000..b2812c2 --- /dev/null +++ b/src/modules/api/account/services/account.service.ts @@ -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, + }); + } +} diff --git a/src/modules/api/admin/admin.module.ts b/src/modules/api/admin/admin.module.ts index 4459095..bb0cb07 100644 --- a/src/modules/api/admin/admin.module.ts +++ b/src/modules/api/admin/admin.module.ts @@ -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: [ diff --git a/src/modules/api/admin/audit-admin.controller.ts b/src/modules/api/admin/controllers/audit-admin.controller.ts similarity index 100% rename from src/modules/api/admin/audit-admin.controller.ts rename to src/modules/api/admin/controllers/audit-admin.controller.ts diff --git a/src/modules/api/admin/oauth2-admin.controller.ts b/src/modules/api/admin/controllers/oauth2-admin.controller.ts similarity index 99% rename from src/modules/api/admin/oauth2-admin.controller.ts rename to src/modules/api/admin/controllers/oauth2-admin.controller.ts index 5f0cbe2..ad4cfb8 100644 --- a/src/modules/api/admin/oauth2-admin.controller.ts +++ b/src/modules/api/admin/controllers/oauth2-admin.controller.ts @@ -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'; diff --git a/src/modules/api/admin/privilege-admin.controller.ts b/src/modules/api/admin/controllers/privilege-admin.controller.ts similarity index 100% rename from src/modules/api/admin/privilege-admin.controller.ts rename to src/modules/api/admin/controllers/privilege-admin.controller.ts diff --git a/src/modules/api/admin/user-admin.controller.ts b/src/modules/api/admin/controllers/user-admin.controller.ts similarity index 100% rename from src/modules/api/admin/user-admin.controller.ts rename to src/modules/api/admin/controllers/user-admin.controller.ts diff --git a/src/modules/api/admin/admin.service.ts b/src/modules/api/admin/services/admin.service.ts similarity index 98% rename from src/modules/api/admin/admin.service.ts rename to src/modules/api/admin/services/admin.service.ts index 7849de8..b6a1c98 100644 --- a/src/modules/api/admin/admin.service.ts +++ b/src/modules/api/admin/services/admin.service.ts @@ -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 { diff --git a/src/modules/api/api.module.ts b/src/modules/api/api.module.ts index 346726b..163d770 100644 --- a/src/modules/api/api.module.ts +++ b/src/modules/api/api.module.ts @@ -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 { diff --git a/src/modules/api/dtos/account.dto.ts b/src/modules/api/dtos/account.dto.ts new file mode 100644 index 0000000..86284a7 --- /dev/null +++ b/src/modules/api/dtos/account.dto.ts @@ -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; +} diff --git a/src/modules/config/config.providers.ts b/src/modules/config/config.providers.ts index 311f91e..e9e1e10 100644 --- a/src/modules/config/config.providers.ts +++ b/src/modules/config/config.providers.ts @@ -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', diff --git a/src/modules/iplimit/iplimit.module.ts b/src/modules/iplimit/iplimit.module.ts deleted file mode 100644 index e87a238..0000000 --- a/src/modules/iplimit/iplimit.module.ts +++ /dev/null @@ -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 {} diff --git a/src/modules/iplimit/iplimit.service.ts b/src/modules/iplimit/iplimit.service.ts deleted file mode 100644 index f2cb053..0000000 --- a/src/modules/iplimit/iplimit.service.ts +++ /dev/null @@ -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-${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-${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; - } -} diff --git a/src/modules/oauth2/oauth2.service.ts b/src/modules/oauth2/oauth2.service.ts index 64e9124..bdb6692 100644 --- a/src/modules/oauth2/oauth2.service.ts +++ b/src/modules/oauth2/oauth2.service.ts @@ -11,6 +11,7 @@ import { UserAdapter } from './adapter/user.adapter'; const SCOPE_DESCRIPTION: Record = { 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, diff --git a/src/modules/objects/oauth2-client/oauth2-client.service.ts b/src/modules/objects/oauth2-client/oauth2-client.service.ts index cde195a..87f0acc 100644 --- a/src/modules/objects/oauth2-client/oauth2-client.service.ts +++ b/src/modules/objects/oauth2-client/oauth2-client.service.ts @@ -26,6 +26,7 @@ export class OAuth2ClientService { 'email', 'privileges', 'management', + 'account', 'openid', ]; diff --git a/src/modules/objects/privilege/privilege.entity.ts b/src/modules/objects/privilege/privilege.entity.ts index 005fd32..e9d40a6 100644 --- a/src/modules/objects/privilege/privilege.entity.ts +++ b/src/modules/objects/privilege/privilege.entity.ts @@ -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; } diff --git a/src/modules/objects/upload/upload.entity.ts b/src/modules/objects/upload/upload.entity.ts index d589f8a..5300dfa 100644 --- a/src/modules/objects/upload/upload.entity.ts +++ b/src/modules/objects/upload/upload.entity.ts @@ -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; } diff --git a/src/modules/objects/user/user.entity.ts b/src/modules/objects/user/user.entity.ts index fd04751..bc44bd3 100644 --- a/src/modules/objects/user/user.entity.ts +++ b/src/modules/objects/user/user.entity.ts @@ -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[]; diff --git a/src/modules/ssr-front-end/login/login.controller.ts b/src/modules/ssr-front-end/login/login.controller.ts index e3289b2..c73d07a 100644 --- a/src/modules/ssr-front-end/login/login.controller.ts +++ b/src/modules/ssr-front-end/login/login.controller.ts @@ -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, diff --git a/src/modules/ssr-front-end/login/login.module.ts b/src/modules/ssr-front-end/login/login.module.ts index 2238e90..ba012a8 100644 --- a/src/modules/ssr-front-end/login/login.module.ts +++ b/src/modules/ssr-front-end/login/login.module.ts @@ -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 { diff --git a/src/modules/ssr-front-end/register/register.controller.ts b/src/modules/ssr-front-end/register/register.controller.ts index 824211b..57e7077 100644 --- a/src/modules/ssr-front-end/register/register.controller.ts +++ b/src/modules/ssr-front-end/register/register.controller.ts @@ -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, diff --git a/src/modules/ssr-front-end/register/register.module.ts b/src/modules/ssr-front-end/register/register.module.ts index 8ca8c1b..c1ae004 100644 --- a/src/modules/ssr-front-end/register/register.module.ts +++ b/src/modules/ssr-front-end/register/register.module.ts @@ -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 { diff --git a/src/modules/well-known/well-known.controller.ts b/src/modules/well-known/well-known.controller.ts index 007e5f8..956108b 100644 --- a/src/modules/well-known/well-known.controller.ts +++ b/src/modules/well-known/well-known.controller.ts @@ -15,10 +15,25 @@ 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()} +`; } @Get('dnt') diff --git a/views/login/login.pug b/views/login/login.pug index 019dedc..1b9170e 100644 --- a/views/login/login.pug +++ b/views/login/login.pug @@ -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 diff --git a/views/login/password.pug b/views/login/password.pug index 193758a..8073226 100644 --- a/views/login/password.pug +++ b/views/login/password.pug @@ -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 diff --git a/views/password.pug b/views/password.pug index c5d937d..fb475ce 100644 --- a/views/password.pug +++ b/views/password.pug @@ -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 diff --git a/views/register.pug b/views/register.pug index 57a5edf..1ed6c36 100644 --- a/views/register.pug +++ b/views/register.pug @@ -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 diff --git a/views/settings/security.pug b/views/settings/security.pug index fa820fc..3c23e49 100644 --- a/views/settings/security.pug +++ b/views/settings/security.pug @@ -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}