diff --git a/backend/src/api/private/auth/auth.controller.ts b/backend/src/api/private/auth/auth.controller.ts index f0e659b10..36055e08c 100644 --- a/backend/src/api/private/auth/auth.controller.ts +++ b/backend/src/api/private/auth/auth.controller.ts @@ -29,6 +29,7 @@ import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { SessionState } from '../../../session/session.service'; import { User } from '../../../users/user.entity'; import { UsersService } from '../../../users/users.service'; +import { makeUsernameLowercase } from '../../../utils/username'; import { LoginEnabledGuard } from '../../utils/login-enabled.guard'; import { OpenApi } from '../../utils/openapi.decorator'; import { RegistrationEnabledGuard } from '../../utils/registration-enabled.guard'; @@ -107,8 +108,8 @@ export class AuthController { @Param('ldapIdentifier') ldapIdentifier: string, @Body() loginDto: LdapLoginDto, ): void { - // There is no further testing needed as we only get to this point if LocalAuthGuard was successful - request.session.username = loginDto.username; + // There is no further testing needed as we only get to this point if LdapAuthGuard was successful + request.session.username = makeUsernameLowercase(loginDto.username); request.session.authProvider = 'ldap'; } diff --git a/backend/src/api/private/notes/notes.controller.ts b/backend/src/api/private/notes/notes.controller.ts index 63609375e..fa64c4f23 100644 --- a/backend/src/api/private/notes/notes.controller.ts +++ b/backend/src/api/private/notes/notes.controller.ts @@ -39,6 +39,7 @@ import { RevisionDto } from '../../../revisions/revision.dto'; import { RevisionsService } from '../../../revisions/revisions.service'; import { User } from '../../../users/user.entity'; import { UsersService } from '../../../users/users.service'; +import { Username } from '../../../utils/username'; import { GetNoteInterceptor } from '../../utils/get-note.interceptor'; import { MarkdownBody } from '../../utils/markdown-body.decorator'; import { OpenApi } from '../../utils/openapi.decorator'; @@ -203,7 +204,7 @@ export class NotesController { async setUserPermission( @RequestUser() user: User, @RequestNote() note: Note, - @Param('userName') username: string, + @Param('userName') username: Username, @Body('canEdit') canEdit: boolean, ): Promise { const permissionUser = await this.userService.getUserByUsername(username); @@ -221,7 +222,7 @@ export class NotesController { async removeUserPermission( @RequestUser() user: User, @RequestNote() note: Note, - @Param('userName') username: string, + @Param('userName') username: Username, ): Promise { try { const permissionUser = await this.userService.getUserByUsername(username); @@ -281,7 +282,7 @@ export class NotesController { async changeOwner( @RequestUser() user: User, @RequestNote() note: Note, - @Body('newOwner') newOwner: string, + @Body('newOwner') newOwner: Username, ): Promise { const owner = await this.userService.getUserByUsername(newOwner); return await this.noteService.toNoteDto( diff --git a/backend/src/api/private/users/users.controller.ts b/backend/src/api/private/users/users.controller.ts index b03798d1b..32133fc26 100644 --- a/backend/src/api/private/users/users.controller.ts +++ b/backend/src/api/private/users/users.controller.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -9,6 +9,7 @@ import { ApiTags } from '@nestjs/swagger'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { UserInfoDto } from '../../../users/user-info.dto'; import { UsersService } from '../../../users/users.service'; +import { Username } from '../../../utils/username'; import { OpenApi } from '../../utils/openapi.decorator'; @ApiTags('users') @@ -23,7 +24,7 @@ export class UsersController { @Get(':username') @OpenApi(200) - async getUser(@Param('username') username: string): Promise { + async getUser(@Param('username') username: Username): Promise { return this.userService.toUserDto( await this.userService.getUserByUsername(username), ); diff --git a/backend/src/api/public/notes/notes.controller.ts b/backend/src/api/public/notes/notes.controller.ts index 802d10584..b2aab7bee 100644 --- a/backend/src/api/public/notes/notes.controller.ts +++ b/backend/src/api/public/notes/notes.controller.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -42,6 +42,7 @@ import { RevisionDto } from '../../../revisions/revision.dto'; import { RevisionsService } from '../../../revisions/revisions.service'; import { User } from '../../../users/user.entity'; import { UsersService } from '../../../users/users.service'; +import { Username } from '../../../utils/username'; import { GetNoteInterceptor } from '../../utils/get-note.interceptor'; import { MarkdownBody } from '../../utils/markdown-body.decorator'; import { OpenApi } from '../../utils/openapi.decorator'; @@ -264,7 +265,7 @@ export class NotesController { async setUserPermission( @RequestUser() user: User, @RequestNote() note: Note, - @Param('userName') username: string, + @Param('userName') username: Username, @Body('canEdit') canEdit: boolean, ): Promise { const permissionUser = await this.userService.getUserByUsername(username); @@ -291,7 +292,7 @@ export class NotesController { async removeUserPermission( @RequestUser() user: User, @RequestNote() note: Note, - @Param('userName') username: string, + @Param('userName') username: Username, ): Promise { try { const permissionUser = await this.userService.getUserByUsername(username); @@ -377,7 +378,7 @@ export class NotesController { async changeOwner( @RequestUser() user: User, @RequestNote() note: Note, - @Body('newOwner') newOwner: string, + @Body('newOwner') newOwner: Username, ): Promise { const owner = await this.userService.getUserByUsername(newOwner); return await this.noteService.toNoteDto( diff --git a/backend/src/identity/ldap/ldap-login.dto.ts b/backend/src/identity/ldap/ldap-login.dto.ts index ebccf6d7a..6926921f7 100644 --- a/backend/src/identity/ldap/ldap-login.dto.ts +++ b/backend/src/identity/ldap/ldap-login.dto.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,7 @@ import { IsString } from 'class-validator'; export class LdapLoginDto { @IsString() - username: string; + username: string; // This is not of type Username, because LDAP server may use mixed case usernames @IsString() password: string; } diff --git a/backend/src/identity/ldap/ldap.strategy.ts b/backend/src/identity/ldap/ldap.strategy.ts index 0c0c12495..e8bc69401 100644 --- a/backend/src/identity/ldap/ldap.strategy.ts +++ b/backend/src/identity/ldap/ldap.strategy.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -22,6 +22,7 @@ import authConfiguration, { import { NotInDBError } from '../../errors/errors'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { UsersService } from '../../users/users.service'; +import { makeUsernameLowercase } from '../../utils/username'; import { Identity } from '../identity.entity'; import { IdentityService } from '../identity.service'; import { ProviderType } from '../provider-type.enum'; @@ -85,7 +86,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { */ private loginWithLDAP( ldapConfig: LDAPConfig, - username: string, + username: string, // This is not of type Username, because LDAP server may use mixed case usernames password: string, doneCallBack: VerifiedCallback, ): void { @@ -146,7 +147,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { userId: string, ldapConfig: LDAPConfig, user: Record, - username: string, + username: string, // This is not of type Username, because LDAP server may use mixed case usernames ): void { this.identityService .getIdentityFromUserIdAndProviderType(userId, ProviderType.LDAP) @@ -162,8 +163,9 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { .catch(async (error) => { if (error instanceof NotInDBError) { // The user/identity does not yet exist + const usernameLowercase = makeUsernameLowercase(username); // This ensures ldap user can be given permission via usernames const newUser = await this.usersService.createUser( - username, + usernameLowercase, // if there is no displayName we use the username user[ldapConfig.displayNameField] ?? username, ); diff --git a/backend/src/identity/local/local.strategy.ts b/backend/src/identity/local/local.strategy.ts index 59bfc5900..a70850ca6 100644 --- a/backend/src/identity/local/local.strategy.ts +++ b/backend/src/identity/local/local.strategy.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -15,6 +15,7 @@ import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { UserRelationEnum } from '../../users/user-relation.enum'; import { User } from '../../users/user.entity'; import { UsersService } from '../../users/users.service'; +import { Username } from '../../utils/username'; import { IdentityService } from '../identity.service'; @Injectable() @@ -31,7 +32,7 @@ export class LocalStrategy extends PassportStrategy(Strategy, 'local') { logger.setContext(LocalStrategy.name); } - async validate(username: string, password: string): Promise { + async validate(username: Username, password: string): Promise { try { const user = await this.userService.getUserByUsername(username, [ UserRelationEnum.IDENTITIES, diff --git a/backend/src/identity/local/login.dto.ts b/backend/src/identity/local/login.dto.ts index 290c52456..2e4d91a88 100644 --- a/backend/src/identity/local/login.dto.ts +++ b/backend/src/identity/local/login.dto.ts @@ -1,13 +1,16 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import { IsString } from 'class-validator'; +import { IsLowercase, IsString } from 'class-validator'; + +import { Username } from '../../utils/username'; export class LoginDto { @IsString() - username: string; + @IsLowercase() + username: Username; @IsString() password: string; } diff --git a/backend/src/identity/local/register.dto.ts b/backend/src/identity/local/register.dto.ts index 8ce73a52f..54bc61cf9 100644 --- a/backend/src/identity/local/register.dto.ts +++ b/backend/src/identity/local/register.dto.ts @@ -1,13 +1,16 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import { IsString } from 'class-validator'; +import { IsLowercase, IsString } from 'class-validator'; + +import { Username } from '../../utils/username'; export class RegisterDto { @IsString() - username: string; + @IsLowercase() + username: Username; @IsString() displayName: string; diff --git a/backend/src/media/media-upload.dto.ts b/backend/src/media/media-upload.dto.ts index b3d3fe2c0..bb4044920 100644 --- a/backend/src/media/media-upload.dto.ts +++ b/backend/src/media/media-upload.dto.ts @@ -1,13 +1,14 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsDate, IsOptional, IsString } from 'class-validator'; +import { IsDate, IsLowercase, IsOptional, IsString } from 'class-validator'; import { BaseDto } from '../utils/base.dto.'; +import { Username } from '../utils/username'; export class MediaUploadDto extends BaseDto { /** @@ -41,6 +42,7 @@ export class MediaUploadDto extends BaseDto { * @example "testuser5" */ @IsString() + @IsLowercase() @ApiProperty() - username: string | null; + username: Username | null; } diff --git a/backend/src/notes/note-permissions.dto.ts b/backend/src/notes/note-permissions.dto.ts index 56424f68a..30372010e 100644 --- a/backend/src/notes/note-permissions.dto.ts +++ b/backend/src/notes/note-permissions.dto.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,20 +8,23 @@ import { Type } from 'class-transformer'; import { IsArray, IsBoolean, + IsLowercase, IsOptional, IsString, ValidateNested, } from 'class-validator'; import { BaseDto } from '../utils/base.dto.'; +import { Username } from '../utils/username'; export class NoteUserPermissionEntryDto extends BaseDto { /** * Username of the User this permission applies to */ @IsString() + @IsLowercase() @ApiProperty() - username: string; + username: Username; /** * True if the user is allowed to edit the note @@ -38,8 +41,9 @@ export class NoteUserPermissionUpdateDto { * @example "john.smith" */ @IsString() + @IsLowercase() @ApiProperty() - username: string; + username: Username; /** * True if the user should be allowed to edit the note diff --git a/backend/src/realtime/realtime-note/realtime-connection.spec.ts b/backend/src/realtime/realtime-note/realtime-connection.spec.ts index b72e759bb..d8a520bff 100644 --- a/backend/src/realtime/realtime-note/realtime-connection.spec.ts +++ b/backend/src/realtime/realtime-note/realtime-connection.spec.ts @@ -13,6 +13,7 @@ import { Mock } from 'ts-mockery'; import { Note } from '../../notes/note.entity'; import { User } from '../../users/user.entity'; +import { Username } from '../../utils/username'; import * as NameRandomizerModule from './random-word-lists/name-randomizer'; import { RealtimeConnection } from './realtime-connection'; import { RealtimeNote } from './realtime-note'; @@ -39,7 +40,7 @@ describe('websocket connection', () => { let mockedUser: User; let mockedMessageTransporter: MessageTransporter; - const mockedUserName = 'mockedUserName'; + const mockedUserName: Username = 'mocked-user-name'; const mockedDisplayName = 'mockedDisplayName'; beforeEach(() => { diff --git a/backend/src/realtime/realtime-note/realtime-note.service.spec.ts b/backend/src/realtime/realtime-note/realtime-note.service.spec.ts index 2595bb55d..35f60e3d4 100644 --- a/backend/src/realtime/realtime-note/realtime-note.service.spec.ts +++ b/backend/src/realtime/realtime-note/realtime-note.service.spec.ts @@ -40,9 +40,9 @@ describe('RealtimeNoteService', () => { let clientWithoutReadWrite: RealtimeConnection; let deleteIntervalSpy: jest.SpyInstance; - const readWriteUsername = 'canReadWriteUser'; - const onlyReadUsername = 'canOnlyReadUser'; - const noAccessUsername = 'noReadWriteUser'; + const readWriteUsername = 'can-read-write-user'; + const onlyReadUsername = 'can-only-read-user'; + const noAccessUsername = 'no-read-write-user'; afterAll(() => { jest.useRealTimers(); diff --git a/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts b/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts index ff8a9eccf..fef3f7983 100644 --- a/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts +++ b/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts @@ -11,6 +11,8 @@ import { } from '@hedgedoc/commons'; import { Listener } from 'eventemitter2'; +import { Username } from '../../utils/username'; + export type OtherAdapterCollector = () => RealtimeUserStatusAdapter[]; /** @@ -20,7 +22,7 @@ export class RealtimeUserStatusAdapter { private readonly realtimeUser: RealtimeUser; constructor( - private readonly username: string | null, + private readonly username: Username | null, private readonly displayName: string, private collectOtherAdapters: OtherAdapterCollector, private messageTransporter: MessageTransporter, diff --git a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts index 9ffb1660c..d0a264af1 100644 --- a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts +++ b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts @@ -11,6 +11,7 @@ import { import { Mock } from 'ts-mockery'; import { User } from '../../../users/user.entity'; +import { Username } from '../../../utils/username'; import { RealtimeConnection } from '../realtime-connection'; import { RealtimeNote } from '../realtime-note'; import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter'; @@ -21,13 +22,13 @@ enum RealtimeUserState { WITH_READONLY, } -const MOCK_FALLBACK_USERNAME = 'mock'; +const MOCK_FALLBACK_USERNAME: Username = 'mock'; /** * Creates a mocked {@link RealtimeConnection realtime connection}. */ export class MockConnectionBuilder { - private username: string | null; + private username: Username | null; private displayName: string | undefined; private includeRealtimeUserStatus: RealtimeUserState = RealtimeUserState.WITHOUT; @@ -50,7 +51,7 @@ export class MockConnectionBuilder { * * @param username the username of the mocked user. If this value is omitted then the builder will user a {@link MOCK_FALLBACK_USERNAME fallback}. */ - public withLoggedInUser(username?: string): this { + public withLoggedInUser(username?: Username): this { const newUsername = username ?? MOCK_FALLBACK_USERNAME; this.username = newUsername; this.displayName = newUsername; diff --git a/backend/src/realtime/websocket/websocket.gateway.spec.ts b/backend/src/realtime/websocket/websocket.gateway.spec.ts index 9545e22c3..1912cc958 100644 --- a/backend/src/realtime/websocket/websocket.gateway.spec.ts +++ b/backend/src/realtime/websocket/websocket.gateway.spec.ts @@ -41,6 +41,7 @@ import { Session } from '../../users/session.entity'; import { User } from '../../users/user.entity'; import { UsersModule } from '../../users/users.module'; import { UsersService } from '../../users/users.service'; +import { Username } from '../../utils/username'; import * as websocketConnectionModule from '../realtime-note/realtime-connection'; import { RealtimeConnection } from '../realtime-note/realtime-connection'; import { RealtimeNote } from '../realtime-note/realtime-note'; @@ -165,7 +166,7 @@ describe('Websocket gateway', () => { ), ); - const mockUsername = 'mockUsername'; + const mockUsername: Username = 'mock-username'; jest .spyOn(sessionService, 'fetchUsernameForSessionId') .mockImplementation((sessionId: string) => diff --git a/backend/src/session/session.service.spec.ts b/backend/src/session/session.service.spec.ts index cf803f43c..844178fd9 100644 --- a/backend/src/session/session.service.spec.ts +++ b/backend/src/session/session.service.spec.ts @@ -28,7 +28,7 @@ describe('SessionService', () => { let authConfigMock: AuthConfig; let typeormStoreConstructorMock: jest.SpyInstance; const mockedExistingSessionId = 'mockedExistingSessionId'; - const mockUsername = 'mockUser'; + const mockUsername = 'mock-user'; const mockSecret = 'mockSecret'; let sessionService: SessionService; diff --git a/backend/src/session/session.service.ts b/backend/src/session/session.service.ts index ca5c6c51f..27676ad22 100644 --- a/backend/src/session/session.service.ts +++ b/backend/src/session/session.service.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -19,10 +19,11 @@ import databaseConfiguration, { } from '../config/database.config'; import { Session } from '../users/session.entity'; import { HEDGEDOC_SESSION } from '../utils/session'; +import { Username } from '../utils/username'; export interface SessionState { cookie: unknown; - username?: string; + username?: Username; authProvider: string; } @@ -58,10 +59,10 @@ export class SessionService { * @param sessionId The session id for which the owning user should be found * @return A Promise that either resolves with the username or rejects with an error */ - fetchUsernameForSessionId(sessionId: string): Promise { + fetchUsernameForSessionId(sessionId: string): Promise { return new Promise((resolve, reject) => { this.typeormStore.get(sessionId, (error?: Error, result?: SessionState) => - error || !result ? reject(error) : resolve(result.username), + error || !result ? reject(error) : resolve(result.username as Username), ); }); } diff --git a/backend/src/users/user-info.dto.ts b/backend/src/users/user-info.dto.ts index 29027128d..d26937da8 100644 --- a/backend/src/users/user-info.dto.ts +++ b/backend/src/users/user-info.dto.ts @@ -1,12 +1,13 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { IsLowercase, IsString } from 'class-validator'; import { BaseDto } from '../utils/base.dto.'; +import { Username } from '../utils/username'; export class UserInfoDto extends BaseDto { /** @@ -14,8 +15,9 @@ export class UserInfoDto extends BaseDto { * @example "john.smith" */ @IsString() + @IsLowercase() @ApiProperty() - username: string; + username: Username; /** * The display name diff --git a/backend/src/users/user.entity.ts b/backend/src/users/user.entity.ts index 601dac0c7..85a2d15c3 100644 --- a/backend/src/users/user.entity.ts +++ b/backend/src/users/user.entity.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -20,6 +20,7 @@ import { HistoryEntry } from '../history/history-entry.entity'; import { Identity } from '../identity/identity.entity'; import { MediaUpload } from '../media/media-upload.entity'; import { Note } from '../notes/note.entity'; +import { Username } from '../utils/username'; @Entity() export class User { @@ -29,7 +30,7 @@ export class User { @Column({ unique: true, }) - username: string; + username: Username; @Column() displayName: string; @@ -77,7 +78,7 @@ export class User { private constructor() {} public static create( - username: string, + username: Username, displayName: string, ): Omit { const newUser = new User(); diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 3664e75bf..2712d3102 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -9,6 +9,7 @@ import { Repository } from 'typeorm'; import { AlreadyInDBError, NotInDBError } from '../errors/errors'; import { ConsoleLoggerService } from '../logger/console-logger.service'; +import { Username } from '../utils/username'; import { FullUserInfoDto, UserInfoDto, @@ -29,12 +30,12 @@ export class UsersService { /** * @async * Create a new user with a given username and displayName - * @param username - the username the new user shall have - * @param displayName - the display name the new user shall have + * @param {Username} username - the username the new user shall have + * @param {string} displayName - the display name the new user shall have * @return {User} the user * @throws {AlreadyInDBError} the username is already taken. */ - async createUser(username: string, displayName: string): Promise { + async createUser(username: Username, displayName: string): Promise { const user = User.create(username, displayName); try { return await this.userRepository.save(user); @@ -77,12 +78,12 @@ export class UsersService { /** * @async * Get the user specified by the username - * @param {string} username the username by which the user is specified + * @param {Username} username the username by which the user is specified * @param {UserRelationEnum[]} [withRelations=[]] if the returned user object should contain certain relations * @return {User} the specified user */ async getUserByUsername( - username: string, + username: Username, withRelations: UserRelationEnum[] = [], ): Promise { const user = await this.userRepository.findOne({ diff --git a/backend/src/utils/username.ts b/backend/src/utils/username.ts new file mode 100644 index 000000000..4ea20fc70 --- /dev/null +++ b/backend/src/utils/username.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type Username = Lowercase; + +export function makeUsernameLowercase(username: string): Username { + return username.toLowerCase() as Username; +} diff --git a/backend/test/private-api/auth.e2e-spec.ts b/backend/test/private-api/auth.e2e-spec.ts index ac682d6b5..d77b8f6d4 100644 --- a/backend/test/private-api/auth.e2e-spec.ts +++ b/backend/test/private-api/auth.e2e-spec.ts @@ -16,12 +16,13 @@ import { RegisterDto } from '../../src/identity/local/register.dto'; import { UpdatePasswordDto } from '../../src/identity/local/update-password.dto'; import { UserRelationEnum } from '../../src/users/user-relation.enum'; import { checkPassword } from '../../src/utils/password'; +import { Username } from '../../src/utils/username'; import { TestSetup, TestSetupBuilder } from '../test-setup'; describe('Auth', () => { let testSetup: TestSetup; - let username: string; + let username: Username; let displayName: string; let password: string;