From 167135a8d0bcf523b7355d420ec892562be0db46 Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Wed, 28 May 2025 21:53:35 +0200 Subject: [PATCH] feat(auth): add guest login Co-authored-by: Philip Molares Signed-off-by: Philip Molares Signed-off-by: Erik Michelson --- .../api-tokens/api-tokens.controller.ts | 8 +++-- backend/src/api/private/me/me.controller.ts | 2 +- .../src/api/private/notes/notes.controller.ts | 19 ++++++------ .../decorators/request-user-id.decorator.ts | 7 ++--- backend/src/api/utils/descriptions.ts | 2 +- frontend/src/api/auth/guest.ts | 29 +++++++++++++++++ .../application-loader/initializers/index.ts | 5 +++ .../initializers/login-or-register-guest.ts | 31 +++++++++++++++++++ frontend/src/hooks/common/use-is-logged-in.ts | 3 +- 9 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 frontend/src/api/auth/guest.ts create mode 100644 frontend/src/components/application-loader/initializers/login-or-register-guest.ts diff --git a/backend/src/api/private/api-tokens/api-tokens.controller.ts b/backend/src/api/private/api-tokens/api-tokens.controller.ts index d858cf072..98c0e5d34 100644 --- a/backend/src/api/private/api-tokens/api-tokens.controller.ts +++ b/backend/src/api/private/api-tokens/api-tokens.controller.ts @@ -40,7 +40,9 @@ export class ApiTokensController { @Get() @OpenApi(200) - async getUserTokens(@RequestUserId() userId: number): Promise { + async getUserTokens( + @RequestUserId({ forbidGuests: true }) userId: number, + ): Promise { 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 { 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 { await this.apiTokenService.removeToken(keyId, userId); diff --git a/backend/src/api/private/me/me.controller.ts b/backend/src/api/private/me/me.controller.ts index 9e750a251..ba6862c99 100644 --- a/backend/src/api/private/me/me.controller.ts +++ b/backend/src/api/private/me/me.controller.ts @@ -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 { await this.userService.updateUser( diff --git a/backend/src/api/private/notes/notes.controller.ts b/backend/src/api/private/notes/notes.controller.ts index 60e15d5f1..5a2801cb8 100644 --- a/backend/src/api/private/notes/notes.controller.ts +++ b/backend/src/api/private/notes/notes.controller.ts @@ -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 { + async getNote(@RequestNoteId() noteId: number): Promise { 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 { 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 { @@ -124,6 +121,12 @@ export class NotesController { @RequestNoteId() noteId: number, @Body() noteMediaDeletionDto: NoteMediaDeletionDto, ): Promise { + 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 { 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 { return await this.revisionsService.getAllRevisionMetadataDto(noteId); diff --git a/backend/src/api/utils/decorators/request-user-id.decorator.ts b/backend/src/api/utils/decorators/request-user-id.decorator.ts index e7e6cc35a..2796183a5 100644 --- a/backend/src/api/utils/decorators/request-user-id.decorator.ts +++ b/backend/src/api/utils/decorators/request-user-id.decorator.ts @@ -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"); } diff --git a/backend/src/api/utils/descriptions.ts b/backend/src/api/utils/descriptions.ts index 65ec311ea..7af78a9b0 100644 --- a/backend/src/api/utils/descriptions.ts +++ b/backend/src/api/utils/descriptions.ts @@ -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 = diff --git a/frontend/src/api/auth/guest.ts b/frontend/src/api/auth/guest.ts new file mode 100644 index 000000000..aaca9de3b --- /dev/null +++ b/frontend/src/api/auth/guest.ts @@ -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 => { + await new PostApiRequestBuilder('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 => { + const response = await new PostApiRequestBuilder( + `auth/guest/register` + ).sendRequest() + return await response.asParsedJsonObject() +} diff --git a/frontend/src/components/application-loader/initializers/index.ts b/frontend/src/components/application-loader/initializers/index.ts index 5d3a96184..ca7e7bf88 100644 --- a/frontend/src/components/application-loader/initializers/index.ts +++ b/frontend/src/components/application-loader/initializers/index.ts @@ -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 diff --git a/frontend/src/components/application-loader/initializers/login-or-register-guest.ts b/frontend/src/components/application-loader/initializers/login-or-register-guest.ts new file mode 100644 index 000000000..6c2a14552 --- /dev/null +++ b/frontend/src/components/application-loader/initializers/login-or-register-guest.ts @@ -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 => { + 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() +} diff --git a/frontend/src/hooks/common/use-is-logged-in.ts b/frontend/src/hooks/common/use-is-logged-in.ts index c11a046ed..2b7f817ba 100644 --- a/frontend/src/hooks/common/use-is-logged-in.ts +++ b/frontend/src/hooks/common/use-is-logged-in.ts @@ -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) }