feat(backend): handle username always in lowercase

This should make all usernames of new users into lowercase. Usernames are also searched in the DB as lowercase.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Philip Molares 2023-05-13 14:56:42 +02:00 committed by Tilman Vatteroth
parent 9625900d1c
commit 0a8945d934
23 changed files with 99 additions and 58 deletions

View file

@ -29,6 +29,7 @@ import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { SessionState } from '../../../session/session.service'; import { SessionState } from '../../../session/session.service';
import { User } from '../../../users/user.entity'; import { User } from '../../../users/user.entity';
import { UsersService } from '../../../users/users.service'; import { UsersService } from '../../../users/users.service';
import { makeUsernameLowercase } from '../../../utils/username';
import { LoginEnabledGuard } from '../../utils/login-enabled.guard'; import { LoginEnabledGuard } from '../../utils/login-enabled.guard';
import { OpenApi } from '../../utils/openapi.decorator'; import { OpenApi } from '../../utils/openapi.decorator';
import { RegistrationEnabledGuard } from '../../utils/registration-enabled.guard'; import { RegistrationEnabledGuard } from '../../utils/registration-enabled.guard';
@ -107,8 +108,8 @@ export class AuthController {
@Param('ldapIdentifier') ldapIdentifier: string, @Param('ldapIdentifier') ldapIdentifier: string,
@Body() loginDto: LdapLoginDto, @Body() loginDto: LdapLoginDto,
): void { ): void {
// There is no further testing needed as we only get to this point if LocalAuthGuard was successful // There is no further testing needed as we only get to this point if LdapAuthGuard was successful
request.session.username = loginDto.username; request.session.username = makeUsernameLowercase(loginDto.username);
request.session.authProvider = 'ldap'; request.session.authProvider = 'ldap';
} }

View file

@ -39,6 +39,7 @@ import { RevisionDto } from '../../../revisions/revision.dto';
import { RevisionsService } from '../../../revisions/revisions.service'; import { RevisionsService } from '../../../revisions/revisions.service';
import { User } from '../../../users/user.entity'; import { User } from '../../../users/user.entity';
import { UsersService } from '../../../users/users.service'; import { UsersService } from '../../../users/users.service';
import { Username } from '../../../utils/username';
import { GetNoteInterceptor } from '../../utils/get-note.interceptor'; import { GetNoteInterceptor } from '../../utils/get-note.interceptor';
import { MarkdownBody } from '../../utils/markdown-body.decorator'; import { MarkdownBody } from '../../utils/markdown-body.decorator';
import { OpenApi } from '../../utils/openapi.decorator'; import { OpenApi } from '../../utils/openapi.decorator';
@ -203,7 +204,7 @@ export class NotesController {
async setUserPermission( async setUserPermission(
@RequestUser() user: User, @RequestUser() user: User,
@RequestNote() note: Note, @RequestNote() note: Note,
@Param('userName') username: string, @Param('userName') username: Username,
@Body('canEdit') canEdit: boolean, @Body('canEdit') canEdit: boolean,
): Promise<NotePermissionsDto> { ): Promise<NotePermissionsDto> {
const permissionUser = await this.userService.getUserByUsername(username); const permissionUser = await this.userService.getUserByUsername(username);
@ -221,7 +222,7 @@ export class NotesController {
async removeUserPermission( async removeUserPermission(
@RequestUser() user: User, @RequestUser() user: User,
@RequestNote() note: Note, @RequestNote() note: Note,
@Param('userName') username: string, @Param('userName') username: Username,
): Promise<NotePermissionsDto> { ): Promise<NotePermissionsDto> {
try { try {
const permissionUser = await this.userService.getUserByUsername(username); const permissionUser = await this.userService.getUserByUsername(username);
@ -281,7 +282,7 @@ export class NotesController {
async changeOwner( async changeOwner(
@RequestUser() user: User, @RequestUser() user: User,
@RequestNote() note: Note, @RequestNote() note: Note,
@Body('newOwner') newOwner: string, @Body('newOwner') newOwner: Username,
): Promise<NoteDto> { ): Promise<NoteDto> {
const owner = await this.userService.getUserByUsername(newOwner); const owner = await this.userService.getUserByUsername(newOwner);
return await this.noteService.toNoteDto( return await this.noteService.toNoteDto(

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -9,6 +9,7 @@ import { ApiTags } from '@nestjs/swagger';
import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { UserInfoDto } from '../../../users/user-info.dto'; import { UserInfoDto } from '../../../users/user-info.dto';
import { UsersService } from '../../../users/users.service'; import { UsersService } from '../../../users/users.service';
import { Username } from '../../../utils/username';
import { OpenApi } from '../../utils/openapi.decorator'; import { OpenApi } from '../../utils/openapi.decorator';
@ApiTags('users') @ApiTags('users')
@ -23,7 +24,7 @@ export class UsersController {
@Get(':username') @Get(':username')
@OpenApi(200) @OpenApi(200)
async getUser(@Param('username') username: string): Promise<UserInfoDto> { async getUser(@Param('username') username: Username): Promise<UserInfoDto> {
return this.userService.toUserDto( return this.userService.toUserDto(
await this.userService.getUserByUsername(username), await this.userService.getUserByUsername(username),
); );

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -42,6 +42,7 @@ import { RevisionDto } from '../../../revisions/revision.dto';
import { RevisionsService } from '../../../revisions/revisions.service'; import { RevisionsService } from '../../../revisions/revisions.service';
import { User } from '../../../users/user.entity'; import { User } from '../../../users/user.entity';
import { UsersService } from '../../../users/users.service'; import { UsersService } from '../../../users/users.service';
import { Username } from '../../../utils/username';
import { GetNoteInterceptor } from '../../utils/get-note.interceptor'; import { GetNoteInterceptor } from '../../utils/get-note.interceptor';
import { MarkdownBody } from '../../utils/markdown-body.decorator'; import { MarkdownBody } from '../../utils/markdown-body.decorator';
import { OpenApi } from '../../utils/openapi.decorator'; import { OpenApi } from '../../utils/openapi.decorator';
@ -264,7 +265,7 @@ export class NotesController {
async setUserPermission( async setUserPermission(
@RequestUser() user: User, @RequestUser() user: User,
@RequestNote() note: Note, @RequestNote() note: Note,
@Param('userName') username: string, @Param('userName') username: Username,
@Body('canEdit') canEdit: boolean, @Body('canEdit') canEdit: boolean,
): Promise<NotePermissionsDto> { ): Promise<NotePermissionsDto> {
const permissionUser = await this.userService.getUserByUsername(username); const permissionUser = await this.userService.getUserByUsername(username);
@ -291,7 +292,7 @@ export class NotesController {
async removeUserPermission( async removeUserPermission(
@RequestUser() user: User, @RequestUser() user: User,
@RequestNote() note: Note, @RequestNote() note: Note,
@Param('userName') username: string, @Param('userName') username: Username,
): Promise<NotePermissionsDto> { ): Promise<NotePermissionsDto> {
try { try {
const permissionUser = await this.userService.getUserByUsername(username); const permissionUser = await this.userService.getUserByUsername(username);
@ -377,7 +378,7 @@ export class NotesController {
async changeOwner( async changeOwner(
@RequestUser() user: User, @RequestUser() user: User,
@RequestNote() note: Note, @RequestNote() note: Note,
@Body('newOwner') newOwner: string, @Body('newOwner') newOwner: Username,
): Promise<NoteDto> { ): Promise<NoteDto> {
const owner = await this.userService.getUserByUsername(newOwner); const owner = await this.userService.getUserByUsername(newOwner);
return await this.noteService.toNoteDto( return await this.noteService.toNoteDto(

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -7,7 +7,7 @@ import { IsString } from 'class-validator';
export class LdapLoginDto { export class LdapLoginDto {
@IsString() @IsString()
username: string; username: string; // This is not of type Username, because LDAP server may use mixed case usernames
@IsString() @IsString()
password: string; password: string;
} }

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -22,6 +22,7 @@ import authConfiguration, {
import { NotInDBError } from '../../errors/errors'; import { NotInDBError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { UsersService } from '../../users/users.service'; import { UsersService } from '../../users/users.service';
import { makeUsernameLowercase } from '../../utils/username';
import { Identity } from '../identity.entity'; import { Identity } from '../identity.entity';
import { IdentityService } from '../identity.service'; import { IdentityService } from '../identity.service';
import { ProviderType } from '../provider-type.enum'; import { ProviderType } from '../provider-type.enum';
@ -85,7 +86,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') {
*/ */
private loginWithLDAP( private loginWithLDAP(
ldapConfig: LDAPConfig, ldapConfig: LDAPConfig,
username: string, username: string, // This is not of type Username, because LDAP server may use mixed case usernames
password: string, password: string,
doneCallBack: VerifiedCallback, doneCallBack: VerifiedCallback,
): void { ): void {
@ -146,7 +147,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') {
userId: string, userId: string,
ldapConfig: LDAPConfig, ldapConfig: LDAPConfig,
user: Record<string, string>, user: Record<string, string>,
username: string, username: string, // This is not of type Username, because LDAP server may use mixed case usernames
): void { ): void {
this.identityService this.identityService
.getIdentityFromUserIdAndProviderType(userId, ProviderType.LDAP) .getIdentityFromUserIdAndProviderType(userId, ProviderType.LDAP)
@ -162,8 +163,9 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') {
.catch(async (error) => { .catch(async (error) => {
if (error instanceof NotInDBError) { if (error instanceof NotInDBError) {
// The user/identity does not yet exist // 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( const newUser = await this.usersService.createUser(
username, usernameLowercase,
// if there is no displayName we use the username // if there is no displayName we use the username
user[ldapConfig.displayNameField] ?? username, user[ldapConfig.displayNameField] ?? username,
); );

View file

@ -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 * 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 { UserRelationEnum } from '../../users/user-relation.enum';
import { User } from '../../users/user.entity'; import { User } from '../../users/user.entity';
import { UsersService } from '../../users/users.service'; import { UsersService } from '../../users/users.service';
import { Username } from '../../utils/username';
import { IdentityService } from '../identity.service'; import { IdentityService } from '../identity.service';
@Injectable() @Injectable()
@ -31,7 +32,7 @@ export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
logger.setContext(LocalStrategy.name); logger.setContext(LocalStrategy.name);
} }
async validate(username: string, password: string): Promise<User> { async validate(username: Username, password: string): Promise<User> {
try { try {
const user = await this.userService.getUserByUsername(username, [ const user = await this.userService.getUserByUsername(username, [
UserRelationEnum.IDENTITIES, UserRelationEnum.IDENTITIES,

View file

@ -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 * 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 { export class LoginDto {
@IsString() @IsString()
username: string; @IsLowercase()
username: Username;
@IsString() @IsString()
password: string; password: string;
} }

View file

@ -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 * 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 { export class RegisterDto {
@IsString() @IsString()
username: string; @IsLowercase()
username: Username;
@IsString() @IsString()
displayName: string; displayName: string;

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; 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 { BaseDto } from '../utils/base.dto.';
import { Username } from '../utils/username';
export class MediaUploadDto extends BaseDto { export class MediaUploadDto extends BaseDto {
/** /**
@ -41,6 +42,7 @@ export class MediaUploadDto extends BaseDto {
* @example "testuser5" * @example "testuser5"
*/ */
@IsString() @IsString()
@IsLowercase()
@ApiProperty() @ApiProperty()
username: string | null; username: Username | null;
} }

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -8,20 +8,23 @@ import { Type } from 'class-transformer';
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
IsLowercase,
IsOptional, IsOptional,
IsString, IsString,
ValidateNested, ValidateNested,
} from 'class-validator'; } from 'class-validator';
import { BaseDto } from '../utils/base.dto.'; import { BaseDto } from '../utils/base.dto.';
import { Username } from '../utils/username';
export class NoteUserPermissionEntryDto extends BaseDto { export class NoteUserPermissionEntryDto extends BaseDto {
/** /**
* Username of the User this permission applies to * Username of the User this permission applies to
*/ */
@IsString() @IsString()
@IsLowercase()
@ApiProperty() @ApiProperty()
username: string; username: Username;
/** /**
* True if the user is allowed to edit the note * True if the user is allowed to edit the note
@ -38,8 +41,9 @@ export class NoteUserPermissionUpdateDto {
* @example "john.smith" * @example "john.smith"
*/ */
@IsString() @IsString()
@IsLowercase()
@ApiProperty() @ApiProperty()
username: string; username: Username;
/** /**
* True if the user should be allowed to edit the note * True if the user should be allowed to edit the note

View file

@ -13,6 +13,7 @@ import { Mock } from 'ts-mockery';
import { Note } from '../../notes/note.entity'; import { Note } from '../../notes/note.entity';
import { User } from '../../users/user.entity'; import { User } from '../../users/user.entity';
import { Username } from '../../utils/username';
import * as NameRandomizerModule from './random-word-lists/name-randomizer'; import * as NameRandomizerModule from './random-word-lists/name-randomizer';
import { RealtimeConnection } from './realtime-connection'; import { RealtimeConnection } from './realtime-connection';
import { RealtimeNote } from './realtime-note'; import { RealtimeNote } from './realtime-note';
@ -39,7 +40,7 @@ describe('websocket connection', () => {
let mockedUser: User; let mockedUser: User;
let mockedMessageTransporter: MessageTransporter; let mockedMessageTransporter: MessageTransporter;
const mockedUserName = 'mockedUserName'; const mockedUserName: Username = 'mocked-user-name';
const mockedDisplayName = 'mockedDisplayName'; const mockedDisplayName = 'mockedDisplayName';
beforeEach(() => { beforeEach(() => {

View file

@ -40,9 +40,9 @@ describe('RealtimeNoteService', () => {
let clientWithoutReadWrite: RealtimeConnection; let clientWithoutReadWrite: RealtimeConnection;
let deleteIntervalSpy: jest.SpyInstance; let deleteIntervalSpy: jest.SpyInstance;
const readWriteUsername = 'canReadWriteUser'; const readWriteUsername = 'can-read-write-user';
const onlyReadUsername = 'canOnlyReadUser'; const onlyReadUsername = 'can-only-read-user';
const noAccessUsername = 'noReadWriteUser'; const noAccessUsername = 'no-read-write-user';
afterAll(() => { afterAll(() => {
jest.useRealTimers(); jest.useRealTimers();

View file

@ -11,6 +11,8 @@ import {
} from '@hedgedoc/commons'; } from '@hedgedoc/commons';
import { Listener } from 'eventemitter2'; import { Listener } from 'eventemitter2';
import { Username } from '../../utils/username';
export type OtherAdapterCollector = () => RealtimeUserStatusAdapter[]; export type OtherAdapterCollector = () => RealtimeUserStatusAdapter[];
/** /**
@ -20,7 +22,7 @@ export class RealtimeUserStatusAdapter {
private readonly realtimeUser: RealtimeUser; private readonly realtimeUser: RealtimeUser;
constructor( constructor(
private readonly username: string | null, private readonly username: Username | null,
private readonly displayName: string, private readonly displayName: string,
private collectOtherAdapters: OtherAdapterCollector, private collectOtherAdapters: OtherAdapterCollector,
private messageTransporter: MessageTransporter, private messageTransporter: MessageTransporter,

View file

@ -11,6 +11,7 @@ import {
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { User } from '../../../users/user.entity'; import { User } from '../../../users/user.entity';
import { Username } from '../../../utils/username';
import { RealtimeConnection } from '../realtime-connection'; import { RealtimeConnection } from '../realtime-connection';
import { RealtimeNote } from '../realtime-note'; import { RealtimeNote } from '../realtime-note';
import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter'; import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter';
@ -21,13 +22,13 @@ enum RealtimeUserState {
WITH_READONLY, WITH_READONLY,
} }
const MOCK_FALLBACK_USERNAME = 'mock'; const MOCK_FALLBACK_USERNAME: Username = 'mock';
/** /**
* Creates a mocked {@link RealtimeConnection realtime connection}. * Creates a mocked {@link RealtimeConnection realtime connection}.
*/ */
export class MockConnectionBuilder { export class MockConnectionBuilder {
private username: string | null; private username: Username | null;
private displayName: string | undefined; private displayName: string | undefined;
private includeRealtimeUserStatus: RealtimeUserState = private includeRealtimeUserStatus: RealtimeUserState =
RealtimeUserState.WITHOUT; 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}. * @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; const newUsername = username ?? MOCK_FALLBACK_USERNAME;
this.username = newUsername; this.username = newUsername;
this.displayName = newUsername; this.displayName = newUsername;

View file

@ -41,6 +41,7 @@ import { Session } from '../../users/session.entity';
import { User } from '../../users/user.entity'; import { User } from '../../users/user.entity';
import { UsersModule } from '../../users/users.module'; import { UsersModule } from '../../users/users.module';
import { UsersService } from '../../users/users.service'; import { UsersService } from '../../users/users.service';
import { Username } from '../../utils/username';
import * as websocketConnectionModule from '../realtime-note/realtime-connection'; import * as websocketConnectionModule from '../realtime-note/realtime-connection';
import { RealtimeConnection } from '../realtime-note/realtime-connection'; import { RealtimeConnection } from '../realtime-note/realtime-connection';
import { RealtimeNote } from '../realtime-note/realtime-note'; import { RealtimeNote } from '../realtime-note/realtime-note';
@ -165,7 +166,7 @@ describe('Websocket gateway', () => {
), ),
); );
const mockUsername = 'mockUsername'; const mockUsername: Username = 'mock-username';
jest jest
.spyOn(sessionService, 'fetchUsernameForSessionId') .spyOn(sessionService, 'fetchUsernameForSessionId')
.mockImplementation((sessionId: string) => .mockImplementation((sessionId: string) =>

View file

@ -28,7 +28,7 @@ describe('SessionService', () => {
let authConfigMock: AuthConfig; let authConfigMock: AuthConfig;
let typeormStoreConstructorMock: jest.SpyInstance; let typeormStoreConstructorMock: jest.SpyInstance;
const mockedExistingSessionId = 'mockedExistingSessionId'; const mockedExistingSessionId = 'mockedExistingSessionId';
const mockUsername = 'mockUser'; const mockUsername = 'mock-user';
const mockSecret = 'mockSecret'; const mockSecret = 'mockSecret';
let sessionService: SessionService; let sessionService: SessionService;

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -19,10 +19,11 @@ import databaseConfiguration, {
} from '../config/database.config'; } from '../config/database.config';
import { Session } from '../users/session.entity'; import { Session } from '../users/session.entity';
import { HEDGEDOC_SESSION } from '../utils/session'; import { HEDGEDOC_SESSION } from '../utils/session';
import { Username } from '../utils/username';
export interface SessionState { export interface SessionState {
cookie: unknown; cookie: unknown;
username?: string; username?: Username;
authProvider: string; authProvider: string;
} }
@ -58,10 +59,10 @@ export class SessionService {
* @param sessionId The session id for which the owning user should be found * @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 * @return A Promise that either resolves with the username or rejects with an error
*/ */
fetchUsernameForSessionId(sessionId: string): Promise<string | undefined> { fetchUsernameForSessionId(sessionId: string): Promise<Username | undefined> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.typeormStore.get(sessionId, (error?: Error, result?: SessionState) => this.typeormStore.get(sessionId, (error?: Error, result?: SessionState) =>
error || !result ? reject(error) : resolve(result.username), error || !result ? reject(error) : resolve(result.username as Username),
); );
}); });
} }

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator'; import { IsLowercase, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.'; import { BaseDto } from '../utils/base.dto.';
import { Username } from '../utils/username';
export class UserInfoDto extends BaseDto { export class UserInfoDto extends BaseDto {
/** /**
@ -14,8 +15,9 @@ export class UserInfoDto extends BaseDto {
* @example "john.smith" * @example "john.smith"
*/ */
@IsString() @IsString()
@IsLowercase()
@ApiProperty() @ApiProperty()
username: string; username: Username;
/** /**
* The display name * The display name

View file

@ -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 * 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 { Identity } from '../identity/identity.entity';
import { MediaUpload } from '../media/media-upload.entity'; import { MediaUpload } from '../media/media-upload.entity';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
import { Username } from '../utils/username';
@Entity() @Entity()
export class User { export class User {
@ -29,7 +30,7 @@ export class User {
@Column({ @Column({
unique: true, unique: true,
}) })
username: string; username: Username;
@Column() @Column()
displayName: string; displayName: string;
@ -77,7 +78,7 @@ export class User {
private constructor() {} private constructor() {}
public static create( public static create(
username: string, username: Username,
displayName: string, displayName: string,
): Omit<User, 'id' | 'createdAt' | 'updatedAt'> { ): Omit<User, 'id' | 'createdAt' | 'updatedAt'> {
const newUser = new User(); const newUser = new User();

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -9,6 +9,7 @@ import { Repository } from 'typeorm';
import { AlreadyInDBError, NotInDBError } from '../errors/errors'; import { AlreadyInDBError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { Username } from '../utils/username';
import { import {
FullUserInfoDto, FullUserInfoDto,
UserInfoDto, UserInfoDto,
@ -29,12 +30,12 @@ export class UsersService {
/** /**
* @async * @async
* Create a new user with a given username and displayName * Create a new user with a given username and displayName
* @param username - the username the new user shall have * @param {Username} username - the username the new user shall have
* @param displayName - the display name the new user shall have * @param {string} displayName - the display name the new user shall have
* @return {User} the user * @return {User} the user
* @throws {AlreadyInDBError} the username is already taken. * @throws {AlreadyInDBError} the username is already taken.
*/ */
async createUser(username: string, displayName: string): Promise<User> { async createUser(username: Username, displayName: string): Promise<User> {
const user = User.create(username, displayName); const user = User.create(username, displayName);
try { try {
return await this.userRepository.save(user); return await this.userRepository.save(user);
@ -77,12 +78,12 @@ export class UsersService {
/** /**
* @async * @async
* Get the user specified by the username * 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 * @param {UserRelationEnum[]} [withRelations=[]] if the returned user object should contain certain relations
* @return {User} the specified user * @return {User} the specified user
*/ */
async getUserByUsername( async getUserByUsername(
username: string, username: Username,
withRelations: UserRelationEnum[] = [], withRelations: UserRelationEnum[] = [],
): Promise<User> { ): Promise<User> {
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type Username = Lowercase<string>;
export function makeUsernameLowercase(username: string): Username {
return username.toLowerCase() as Username;
}

View file

@ -16,12 +16,13 @@ import { RegisterDto } from '../../src/identity/local/register.dto';
import { UpdatePasswordDto } from '../../src/identity/local/update-password.dto'; import { UpdatePasswordDto } from '../../src/identity/local/update-password.dto';
import { UserRelationEnum } from '../../src/users/user-relation.enum'; import { UserRelationEnum } from '../../src/users/user-relation.enum';
import { checkPassword } from '../../src/utils/password'; import { checkPassword } from '../../src/utils/password';
import { Username } from '../../src/utils/username';
import { TestSetup, TestSetupBuilder } from '../test-setup'; import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Auth', () => { describe('Auth', () => {
let testSetup: TestSetup; let testSetup: TestSetup;
let username: string; let username: Username;
let displayName: string; let displayName: string;
let password: string; let password: string;