feat(auth): add guest login

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 2025-05-28 21:53:35 +02:00
parent 04d19ebfbc
commit 167135a8d0
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
9 changed files with 87 additions and 19 deletions

View file

@ -40,7 +40,9 @@ export class ApiTokensController {
@Get()
@OpenApi(200)
async getUserTokens(@RequestUserId() userId: number): Promise<ApiTokenDto[]> {
async getUserTokens(
@RequestUserId({ forbidGuests: true }) userId: number,
): Promise<ApiTokenDto[]> {
return (await this.apiTokenService.getTokensOfUserById(userId)).map(
(token) => this.apiTokenService.toAuthTokenDto(token),
);
@ -50,7 +52,7 @@ export class ApiTokensController {
@OpenApi(201)
async postTokenRequest(
@Body() createDto: ApiTokenCreateDto,
@RequestUserId() userId: User[FieldNameUser.id],
@RequestUserId({ forbidGuests: true }) userId: User[FieldNameUser.id],
): Promise<ApiTokenWithSecretDto> {
return await this.apiTokenService.createToken(
userId,
@ -62,7 +64,7 @@ export class ApiTokensController {
@Delete('/:keyId')
@OpenApi(204, 404)
async deleteToken(
@RequestUserId() userId: number,
@RequestUserId({ forbidGuests: true }) userId: number,
@Param('keyId') keyId: string,
): Promise<void> {
await this.apiTokenService.removeToken(keyId, userId);

View file

@ -66,7 +66,7 @@ export class MeController {
@Put('profile')
@OpenApi(200)
async updateProfile(
@RequestUserId() userId: number,
@RequestUserId({ forbidGuests: true }) userId: number,
@Body('displayName') newDisplayName: string,
): Promise<void> {
await this.userService.updateUser(

View file

@ -32,7 +32,7 @@ import {
import { ApiTags } from '@nestjs/swagger';
import { SessionGuard } from '../../../auth/session.guard';
import { NotInDBError } from '../../../errors/errors';
import { NotInDBError, PermissionError } from '../../../errors/errors';
import { GroupsService } from '../../../groups/groups.service';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaService } from '../../../media/media.service';
@ -70,10 +70,7 @@ export class NotesController {
@OpenApi(200)
@RequirePermission(RequiredPermission.READ)
@UseInterceptors(GetNoteIdInterceptor)
async getNote(
@RequestUserId({ guestsAllowed: true }) userId: number,
@RequestNoteId() noteId: number,
): Promise<NoteDto> {
async getNote(@RequestNoteId() noteId: number): Promise<NoteDto> {
return await this.noteService.toNoteDto(noteId);
}
@ -92,7 +89,7 @@ export class NotesController {
@OpenApi(201, 413)
@RequirePermission(RequiredPermission.CREATE)
async createNote(
@RequestUserId({ guestsAllowed: true }) userId: number,
@RequestUserId() userId: number,
@MarkdownBody() text: string,
): Promise<NoteDto> {
const createdNoteId = await this.noteService.createNote(text, userId);
@ -103,7 +100,7 @@ export class NotesController {
@OpenApi(201, 400, 404, 409, 413)
@RequirePermission(RequiredPermission.CREATE)
async createNamedNote(
@RequestUserId({ guestsAllowed: true }) userId: number,
@RequestUserId() userId: number,
@Param('noteAlias') noteAlias: string,
@MarkdownBody() text: string,
): Promise<NoteDto> {
@ -124,6 +121,12 @@ export class NotesController {
@RequestNoteId() noteId: number,
@Body() noteMediaDeletionDto: NoteMediaDeletionDto,
): Promise<void> {
const isOwner = await this.permissionService.isOwner(userId, noteId);
if (!isOwner) {
throw new PermissionError(
'You do not have the permission to delete this note.',
);
}
const mediaUploads =
await this.mediaService.getMediaUploadUuidsByNoteId(noteId);
for (const mediaUpload of mediaUploads) {
@ -141,7 +144,6 @@ export class NotesController {
@RequirePermission(RequiredPermission.READ)
@Get(':noteAlias/metadata')
async getNoteMetadata(
@RequestUserId({ guestsAllowed: true }) userId: number,
@RequestNoteId() noteId: number,
): Promise<NoteMetadataDto> {
return await this.noteService.toNoteMetadataDto(noteId);
@ -152,7 +154,6 @@ export class NotesController {
@RequirePermission(RequiredPermission.READ)
@UseInterceptors(GetNoteIdInterceptor)
async getNoteRevisions(
@RequestUserId({ guestsAllowed: true }) userId: number,
@RequestNoteId() noteId: number,
): Promise<RevisionMetadataDto[]> {
return await this.revisionsService.getAllRevisionMetadataDto(noteId);

View file

@ -13,7 +13,7 @@ import {
import { CompleteRequest } from '../request.type';
type RequestUserIdParameter = {
guestsAllowed: boolean;
forbidGuests: boolean;
};
/**
@ -26,14 +26,13 @@ type RequestUserIdParameter = {
// eslint-disable-next-line @typescript-eslint/naming-convention
export const RequestUserId = createParamDecorator(
(
data: RequestUserIdParameter = { guestsAllowed: false },
data: RequestUserIdParameter = { forbidGuests: false },
ctx: ExecutionContext,
) => {
const request: CompleteRequest = ctx.switchToHttp().getRequest();
if (
!request.authProviderType ||
(request.authProviderType === AuthProviderType.GUEST &&
!data.guestsAllowed)
(request.authProviderType === AuthProviderType.GUEST && data.forbidGuests)
) {
throw new UnauthorizedException("You're not logged in");
}

View file

@ -19,7 +19,7 @@ export const forbiddenDescription =
'Access to the requested resource is not permitted';
export const notFoundDescription = 'The requested resource was not found';
export const successfullyDeletedDescription =
'The requested resource was sucessfully deleted';
'The requested resource was successfully deleted';
export const unprocessableEntityDescription =
"The request change can't be processed";
export const conflictDescription =

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { GuestLoginDto } from '@hedgedoc/commons'
import type { GuestRegistrationResponseDto } from '@hedgedoc/commons'
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
/**
* Logs in a guest user identified by a uuid
*
* @param uuid The uuid of the guest user
*/
export const logInGuest = async (uuid: string): Promise<void> => {
await new PostApiRequestBuilder<void, GuestLoginDto>('auth/guest/login').withJsonBody({ uuid }).sendRequest()
}
/**
* Registers a new guest user
*
* @return The uuid of the newly created guest user
*/
export const registerGuest = async (): Promise<GuestRegistrationResponseDto> => {
const response = await new PostApiRequestBuilder<GuestRegistrationResponseDto, void>(
`auth/guest/register`
).sendRequest()
return await response.asParsedJsonObject()
}

View file

@ -9,6 +9,7 @@ import { loadDarkMode } from './load-dark-mode'
import { setUpI18n } from './setupI18n'
import { loadFromLocalStorage } from '../../../redux/editor-config/methods'
import { fetchAndSetUser } from '../../login-page/utils/fetch-and-set-user'
import { loginOrRegisterGuest } from './login-or-register-guest'
const logger = new Logger('Application Loader')
@ -65,6 +66,10 @@ export const createSetUpTaskList = (): InitTask[] => {
name: 'Fetch user information',
task: fetchUserInformation
},
{
name: 'Register or login guest user',
task: loginOrRegisterGuest
},
{
name: 'Load preferences',
task: loadFromLocalStorageAsync

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { logInGuest, registerGuest } from '../../../api/auth/guest'
import { store } from '../../../redux'
import { fetchAndSetUser } from '../../login-page/utils/fetch-and-set-user'
/**
* Handles the auth process towards the backend for guests
* If a user is already logged in, nothing happens.
* If there is a guest uuid in local storage, the guest with that uuid is logged in.
* If there is no guest uuid in local storage, a new guest is registered and logged in.
* The uuid is stored in local storage afterward.
*/
export const loginOrRegisterGuest = async (): Promise<void> => {
const userState = store.getState().user
if (userState !== null) {
return
}
const guestUuid = window.localStorage.getItem('guestUuid')
if (guestUuid === null) {
const { uuid } = await registerGuest()
window.localStorage.setItem('guestUuid', uuid)
return
}
await logInGuest(guestUuid)
await fetchAndSetUser()
}

View file

@ -4,11 +4,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from './use-application-state'
import { AuthProviderType } from '@hedgedoc/commons'
/**
* Hook to check if currently a user is logged in.
* @return True, if a user is logged in. False otherwise.
*/
export const useIsLoggedIn = () => {
return useApplicationState((state) => !!state.user)
return useApplicationState((state) => state.user !== null && state.user.authProvider !== AuthProviderType.GUEST)
}