mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-06-06 01:21:39 -04:00
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:
parent
04d19ebfbc
commit
167135a8d0
9 changed files with 87 additions and 19 deletions
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
29
frontend/src/api/auth/guest.ts
Normal file
29
frontend/src/api/auth/guest.ts
Normal 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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue