feat(auth): refactor auth, add oidc
Some checks are pending
Docker / build-and-push (frontend) (push) Waiting to run
Docker / build-and-push (backend) (push) Waiting to run
Deploy HD2 docs to Netlify / Deploys to netlify (push) Waiting to run
E2E Tests / backend-sqlite (push) Waiting to run
E2E Tests / backend-mariadb (push) Waiting to run
E2E Tests / backend-postgres (push) Waiting to run
E2E Tests / Build test build of frontend (push) Waiting to run
E2E Tests / frontend-cypress (1) (push) Blocked by required conditions
E2E Tests / frontend-cypress (2) (push) Blocked by required conditions
E2E Tests / frontend-cypress (3) (push) Blocked by required conditions
Lint and check format / Lint files and check formatting (push) Waiting to run
REUSE Compliance Check / reuse (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Static Analysis / Njsscan code scanning (push) Waiting to run
Static Analysis / CodeQL analysis (push) Waiting to run
Run tests & build / Test and build with NodeJS 20 (push) Waiting to run

Thanks to all HedgeDoc team members for the time discussing,
helping with weird Nest issues, providing feedback
and suggestions!

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2024-03-23 02:10:25 +01:00
parent 1609f3e01f
commit 7f665fae4b
109 changed files with 2927 additions and 1700 deletions

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsLowercase, IsString } from 'class-validator';
import { IsLowercase, IsOptional, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
import { Username } from '../utils/username';
@ -33,11 +33,12 @@ export class UserInfoDto extends BaseDto {
* URL of the profile picture
* @example "https://hedgedoc.example.com/uploads/johnsmith.png"
*/
@ApiProperty({
@ApiPropertyOptional({
format: 'uri',
})
@IsOptional()
@IsString()
photoUrl: string;
photoUrl?: string;
}
/**
@ -49,11 +50,21 @@ export class FullUserInfoDto extends UserInfoDto {
* Email address of the user
* @example "john.smith@example.com"
*/
@ApiProperty({
@ApiPropertyOptional({
format: 'email',
})
@IsOptional()
@IsString()
email: string;
email?: string;
}
export class FullUserInfoWithIdDto extends FullUserInfoDto {
/**
* The user's ID
* @example 42
*/
@IsString()
id: string;
}
export class UserLoginInfoDto extends UserInfoDto {

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -80,12 +80,14 @@ export class User {
public static create(
username: Username,
displayName: string,
email?: string,
photoUrl?: string,
): Omit<User, 'id' | 'createdAt' | 'updatedAt'> {
const newUser = new User();
newUser.username = username;
newUser.displayName = displayName;
newUser.photo = null;
newUser.email = null;
newUser.photo = photoUrl ?? null;
newUser.email = email ?? null;
newUser.ownedNotes = Promise.resolve([]);
newUser.publicAuthTokens = Promise.resolve([]);
newUser.identities = Promise.resolve([]);

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsBoolean, IsLowercase, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
import { Username } from '../utils/username';
export class UsernameCheckDto extends BaseDto {
// eslint-disable-next-line @darraghor/nestjs-typed/validated-non-primitive-property-needs-type-decorator
@IsString()
@IsLowercase()
username: Username;
}
export class UsernameCheckResponseDto extends BaseDto {
@IsBoolean()
usernameAvailable: boolean;
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -9,6 +9,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import appConfigMock from '../config/mock/app.config.mock';
import authConfigMock from '../config/mock/auth.config.mock';
import { AlreadyInDBError, NotInDBError } from '../errors/errors';
import { LoggerModule } from '../logger/logger.module';
import { User } from './user.entity';
@ -30,7 +31,7 @@ describe('UsersService', () => {
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [appConfigMock],
load: [appConfigMock, authConfigMock],
}),
LoggerModule,
],
@ -100,7 +101,7 @@ describe('UsersService', () => {
return user;
},
);
await service.changeDisplayName(user, newDisplayName);
await service.updateUser(user, newDisplayName, undefined, undefined);
});
});

View file

@ -1,12 +1,14 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { REGEX_USERNAME } from '@hedgedoc/commons';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import AuthConfiguration, { AuthConfig } from '../config/auth.config';
import { AlreadyInDBError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { Username } from '../utils/username';
@ -22,6 +24,8 @@ import { User } from './user.entity';
export class UsersService {
constructor(
private readonly logger: ConsoleLoggerService,
@Inject(AuthConfiguration.KEY)
private authConfig: AuthConfig,
@InjectRepository(User) private userRepository: Repository<User>,
) {
this.logger.setContext(UsersService.name);
@ -32,11 +36,24 @@ export class UsersService {
* Create a new user with a given username and displayName
* @param {Username} username - the username the new user shall have
* @param {string} displayName - the display name the new user shall have
* @param {string} [email] - the email the new user shall have
* @param {string} [photoUrl] - the photoUrl the new user shall have
* @return {User} the user
* @throws {BadRequestException} if the username contains invalid characters or is too short
* @throws {AlreadyInDBError} the username is already taken.
*/
async createUser(username: Username, displayName: string): Promise<User> {
const user = User.create(username, displayName);
async createUser(
username: Username,
displayName: string,
email?: string,
photoUrl?: string,
): Promise<User> {
if (!REGEX_USERNAME.test(username)) {
throw new BadRequestException(
`The username '${username}' is not a valid username.`,
);
}
const user = User.create(username, displayName, email, photoUrl);
try {
return await this.userRepository.save(user);
} catch {
@ -66,13 +83,51 @@ export class UsersService {
/**
* @async
* Change the displayName of the specified user
* @param {User} user - the user to be changed
* @param displayName - the new displayName
* Update the given User with the given information.
* Use {@code null} to clear the stored value (email or profilePicture).
* Use {@code undefined} to keep the stored value.
* @param {User} user - the User to update
* @param {string | undefined} displayName - the displayName to update the user with
* @param {string | null | undefined} email - the email to update the user with
* @param {string | null | undefined} profilePicture - the profilePicture to update the user with
*/
async changeDisplayName(user: User, displayName: string): Promise<void> {
user.displayName = displayName;
await this.userRepository.save(user);
async updateUser(
user: User,
displayName?: string,
email?: string | null,
profilePicture?: string | null,
): Promise<User> {
let shouldSave = false;
if (displayName !== undefined) {
user.displayName = displayName;
shouldSave = true;
}
if (email !== undefined) {
user.email = email;
shouldSave = true;
}
if (profilePicture !== undefined) {
user.photo = profilePicture;
shouldSave = true;
// ToDo: handle LDAP images (https://github.com/hedgedoc/hedgedoc/issues/5032)
}
if (shouldSave) {
return await this.userRepository.save(user);
}
return user;
}
/**
* @async
* Checks if the user with the specified username exists
* @param username - the username to check
* @return {boolean} true if the user exists, false otherwise
*/
async checkIfUserExists(username: Username): Promise<boolean> {
const user = await this.userRepository.findOne({
where: { username: username },
});
return user !== null;
}
/**