feat(commons): add DTOs

Moving the DTOs to commons so frontend and backend use the same types.
Also introducing zod for validation.

Co-authored-by: Erik Michelson <github@erik.michelson.eu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2025-03-21 21:14:29 +01:00
parent aa87ff35b3
commit 4b5bf870f2
65 changed files with 1211 additions and 48 deletions

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const AliasCreateSchema = z
.object({
noteIdOrAlias: z
.string()
.describe(
'The note id, which identifies the note the alias should be added to',
),
newAlias: z.string().describe('The new alias'),
})
.describe('DTO for creating a new alias')
export type AliasCreateDto = z.infer<typeof AliasCreateSchema>

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const AliasUpdateSchema = z
.object({
primaryAlias: z
.literal(true)
.describe('Whether the alias should become the primary alias or not'),
})
.describe('DTO for making one alias primary')
export type AliasUpdateDto = z.infer<typeof AliasUpdateSchema>

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const AliasSchema = z
.object({
name: z.string().describe('The name of the alias'),
primaryAlias: z.boolean().describe('Is the alias the primary alias or not'),
noteId: z
.string()
.describe('The public id of the note the alias is associated with'),
})
.describe(
'The alias of a note. A note can have multiple of these. Only one can be the primary alias.',
)
export type AliasDto = z.infer<typeof AliasSchema>

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './alias.dto.js'
export * from './alias-create.dto.js'
export * from './alias-update.dto.js'

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
const nowPlusTwoYears = (): Date => {
const date = new Date()
date.setFullYear(date.getFullYear() + 2)
return date
}
export const ApiTokenCreateSchema = z
.object({
label: z.string().describe('Label for the new token'),
validUntil: z.coerce
.date()
.max(nowPlusTwoYears())
.describe(
'Expiry date for the new token. Should be at max two years in the future.',
),
})
.describe('DTO for creating a new API access token')
export type ApiTokenCreateDto = z.infer<typeof ApiTokenCreateSchema>

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { ApiTokenSchema } from './api-token.dto.js'
export const ApiTokenWithSecretSchema = ApiTokenSchema.merge(
z.object({
secret: z.string().describe('The secret part of the API token'),
}),
).describe(
'This is returned once after an api token is created to let the user know what their token is.',
)
export type ApiTokenWithSecretDto = z.infer<typeof ApiTokenWithSecretSchema>

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const ApiTokenSchema = z
.object({
label: z.string().describe('The label of the token'),
keyId: z.string().describe('The id of the token'),
createdAt: z.string().datetime().describe('When this token was created'),
validUntil: z
.string()
.datetime()
.describe('How long this token is valid fro'),
lastUsedAt: z
.string()
.datetime()
.nullable()
.describe('When this token was last used'),
})
.describe(
'Represents an access token for the public API. Each API token is bound to a user account. A user can have multiple API tokens.',
)
export type ApiTokenDto = z.infer<typeof ApiTokenSchema>

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './api-token.dto.js'
export * from './api-token-create.dto.js'
export * from './api-token-with-secret.dto.js'

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './ldap-login.dto.js'
export * from './ldap-login-response.dto.js'
export * from './login.dto.js'
export * from './logout-response.dto.js'
export * from './pending-user-confirmation.dto.js'
export * from './provider-type.enum.js'
export * from './register.dto.js'
export * from './update-password.dto.js'
export * from './username-check.dto.js'
export * from './username-check-reponse.dto.js'

View file

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const LdapLoginResponseSchema = z
.object({
newUser: z.boolean().describe('If the LDAP user was newly created.'),
})
.describe('DTO to login via a LDAP server.')
export type LdapLoginResponseDto = z.infer<typeof LdapLoginResponseSchema>

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const LdapLoginSchema = z
.object({
username: z.string().describe('The username to log in at the LDAP server'),
password: z.string().describe('The password to log in at the LDAP server'),
})
.describe('DTO to login via a LDAP server.')
export type LdapLoginDto = z.infer<typeof LdapLoginSchema>

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const LoginSchema = z
.object({
username: z
.string()
.toLowerCase()
.describe('The username to log in with local authentication'),
password: z
.string()
.describe('The password to log in with local authentication'),
})
.describe('DTO for the login form of local accounts')
export type LoginDto = z.infer<typeof LoginSchema>

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const LogoutResponseSchema = z
.object({
redirect: z
.string()
.url()
.describe('Where the user shall be redirected to after the logout.'),
})
.describe('Information the user gets after logging out.')
export type LogoutResponseDto = z.infer<typeof LogoutResponseSchema>

View file

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const PendingUserConfirmationSchema = z
.object({
username: z
.string()
.min(3)
.max(64)
.toLowerCase()
.describe('The chosen new username for the pending user'),
displayName: z
.string()
.describe('The new display name for the pending user'),
profilePicture: z
.string()
.url()
.nullable()
.describe(
'The URL to the chosen profile picture or null to use the auto-generated one',
),
})
.describe(
'DTO for the confirmation of a new user account. When a new user is created through OIDC login, they get asked to choose some details for their new account.',
)
export type PendingUserConfirmationDto = z.infer<
typeof PendingUserConfirmationSchema
>

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum ProviderType {
GUEST = 'guest',
LOCAL = 'local',
LDAP = 'ldap',
OIDC = 'oidc',
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const RegisterSchema = z
.object({
username: z
.string()
.min(3)
.max(64)
.toLowerCase()
.describe('The new username for local account registration'),
displayName: z.string().describe('The display name of the new user'),
password: z
.string()
.min(6)
.describe('The new password for the local account'),
})
.describe('DTO to register a local user account')
export type RegisterDto = z.infer<typeof RegisterSchema>

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const UpdatePasswordSchema = z
.object({
currentPassword: z
.string()
.min(6)
.describe('The current password of the user'),
newPassword: z.string().min(6).describe('The new password of the user'),
})
.describe('DTO to update the password of a local user account')
export type UpdatePasswordDto = z.infer<typeof UpdatePasswordSchema>

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const UsernameCheckResponseSchema = z
.object({
usernameAvailable: z
.boolean()
.describe('Whether the chosen username is available or not'),
})
.describe('Response to the username check on the register forms')
export type UsernameCheckResponseDto = z.infer<
typeof UsernameCheckResponseSchema
>

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const UsernameCheckSchema = z
.object({
username: z
.string()
.toLowerCase()
.describe("The username the user want's to register"),
})
.describe('DTO to check if a username is available')
export type UsernameCheckDto = z.infer<typeof UsernameCheckSchema>

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const EditSchema = z
.object({
username: z
.string()
.nullable()
.describe('The username who changed this section of the note'),
startPosition: z
.number()
.positive()
.describe('The offset where the change starts in the note'),
endPosition: z
.number()
.positive()
.describe('The offset where the change ends in the note'),
createdAt: z.string().datetime().describe('When this edit happened'),
updatedAt: z.string().datetime().describe('When this edit was updated?'),
})
.describe('A edit in a note by username from startPosition to endPosition.')
export type EditDto = z.infer<typeof EditSchema>

View file

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './edit.dto.js'

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { ProviderType } from '../auth/index.js'
export const AuthProviderWithCustomNameSchema = z
.object({
type: z
.literal(ProviderType.LDAP)
.or(z.literal(ProviderType.OIDC))
.describe('The type of the auth provider'),
identifier: z
.string()
.describe('The identifier with which the auth provider can be called'),
providerName: z.string().describe('The name given to the auth provider'),
theme: z
.string()
.nullable()
.describe('The theme to apply for the login button.'),
})
.describe(
'The configuration for an auth provider with a custom name. So you can have multiple of the same kind.',
)
export type AuthProviderWithCustomNameDto = z.infer<
typeof AuthProviderWithCustomNameSchema
>

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { ProviderType } from '../auth/index.js'
export const AuthProviderWithoutCustomNameSchema = z
.object({
type: z
.literal(ProviderType.LOCAL)
.describe('The type of the auth provider'),
})
.describe('Represents the local authentication provider')
export type AuthProviderWithoutCustomNameDto = z.infer<
typeof AuthProviderWithoutCustomNameSchema
>

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AuthProviderWithCustomNameSchema } from './auth-provider-with-custom-name.dto.js'
import { AuthProviderWithoutCustomNameSchema } from './auth-provider-without-custom-name.dto.js'
import { z } from 'zod'
export const AuthProviderSchema = z
.union([
AuthProviderWithoutCustomNameSchema,
AuthProviderWithCustomNameSchema,
])
.describe('A general type for all auth providers')
export type AuthProviderDto = z.infer<typeof AuthProviderSchema>

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const BrandingSchema = z
.object({
name: z
.string()
.nullable()
.describe('The name to be displayed next to the HedgeDoc logo'),
logo: z
.string()
.url()
.nullable()
.describe('URL to the logo to be displayed next to the HedgeDoc logo'),
})
.describe('The configuration for branding of the HedgeDoc instance.')
export type BrandingDto = z.infer<typeof BrandingSchema>

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { GuestAccess } from '../permissions/index.js'
import { ServerVersionSchema } from '../monitoring/index.js'
import { BrandingSchema } from './branding.dto.js'
import { SpecialUrlSchema } from './special-urls.dto.js'
import { AuthProviderSchema } from './auth-provider.dto.js'
export const FrontendConfigSchema = z
.object({
guestAccess: z
.nativeEnum(GuestAccess)
.describe('Maximum access level for guest users'),
allowRegister: z
.boolean()
.describe('Are users allowed to register on this instance?'),
allowProfileEdits: z
.boolean()
.describe('Are users allowed to edit their profile information?'),
allowChooseUsername: z
.boolean()
.describe(
'Are users allowed to choose their username when signing up via OIDC?',
),
authProviders: z
.array(AuthProviderSchema)
.describe(
'Which auth providers are enabled and how are they configured?',
),
branding: BrandingSchema.describe('Individual branding information'),
useImageProxy: z.boolean().describe('Is an image proxy enabled?'),
specialUrls: SpecialUrlSchema.describe('Links to some special pages'),
version: ServerVersionSchema.describe('The version of HedgeDoc'),
plantUmlServer: z
.string()
.url()
.nullable()
.describe('The PlantUML server that should be used to render.'),
maxDocumentLength: z
.number()
.positive()
.describe('The maximal length of each document'),
})
.describe(
'Config properties that are received by the frontend to adjust its own behaviour',
)
export type FrontendConfigDto = z.infer<typeof FrontendConfigSchema>

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './auth-provider.dto.js'
export * from './auth-provider-with-custom-name.dto.js'
export * from './auth-provider-without-custom-name.dto.js'
export * from './branding.dto.js'
export * from './frontend-config.dto.js'
export * from './special-urls.dto.js'

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const SpecialUrlSchema = z
.object({
privacy: z
.string()
.url()
.nullable()
.describe('A link to the privacy notice'),
termsOfUse: z
.string()
.url()
.nullable()
.describe('A link to the privacy notice'),
imprint: z
.string()
.url()
.nullable()
.describe('A link to the imprint notice'),
})
.describe('The special urls an HedgeDoc instance can link to.')
export type SpecialUrlDto = z.infer<typeof SpecialUrlSchema>

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const GroupInfoSchema = z
.object({
name: z.string().describe('Name of the group'),
displayName: z
.string()
.describe(
'Display name of this group. This is used in the UI, when the group is mentioned.',
),
special: z.boolean().describe('Is this group special?'),
})
.describe('DTO that contains the information about a group.')
export type GroupInfoDto = z.infer<typeof GroupInfoSchema>

View file

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './group-info.dto.js'
export * from './special-group.enum.js'

View file

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum SpecialGroup {
EVERYONE = '_EVERYONE',
LOGGED_IN = '_LOGGED_IN',
}

17
commons/src/dtos/index.ts Normal file
View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './alias/index.js'
export * from './api-token/index.js'
export * from './auth/index.js'
export * from './edit/index.js'
export * from './frontend-config/index.js'
export * from './group/index.js'
export * from './media/index.js'
export * from './monitoring/index.js'
export * from './note/index.js'
export * from './permissions/index.js'
export * from './revision/index.js'
export * from './user/index.js'

View file

@ -0,0 +1,6 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './media-upload.dto.js'

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const MediaUploadSchema = z
.object({
uuid: z.string().uuid().describe('The uuid of the media file'),
fileName: z.string().describe('The original filename of the media upload'),
noteId: z
.string()
.nullable()
.describe('The note id to which the uploaded file is linked to'),
createdAt: z
.string()
.datetime()
.describe('The dater when the upload was created'),
username: z
.string()
.nullable()
.describe('The username which uploaded the file'),
})
.describe('Metadata for an uploaded file')
export type MediaUploadDto = z.infer<typeof MediaUploadSchema>

View file

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './server-status.dto.js'
export * from './server-version.dto.js'

View file

@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { ServerVersionSchema } from './server-version.dto.js'
export const ServerStatusSchema = z
.object({
serverVersion: ServerVersionSchema,
onlineNotes: z
.number()
.positive()
.describe('Number of notes that are currently being worked on'),
onlineUsers: z
.number()
.positive()
.describe('Number of user that are currently working on notes'),
distinctOnlineUsers: z
.number()
.positive()
.describe(
'Number of user that are currently working on notes. Each user only counts only once.',
),
notesCount: z
.number()
.positive()
.describe('Number of notes on the instance'),
registeredUsers: z
.number()
.positive()
.describe('Number of user that are currently registered'),
onlineRegisteredUsers: z
.number()
.positive()
.describe('Number of user that are currently registered and online'),
distinctOnlineRegisteredUsers: z
.number()
.positive()
.describe(
'Number of user that are currently registered and online. Each user only counts only once.',
),
isConnectionBusy: z
.boolean()
.describe('If the connection is currently busy'),
connectionSocketQueueLength: z
.number()
.positive()
.describe('Number of connections in the queue'),
isDisconnectBusy: z
.boolean()
.describe('If the connection is currently busy'),
disconnectSocketQueueLength: z
.number()
.positive()
.describe('Number of disconnections in the queue'),
})
.describe('The server status of the HedgeDoc instance.')
export type ServerStatusDto = z.infer<typeof ServerStatusSchema>

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const ServerVersionSchema = z
.object({
major: z.number().positive().describe('The major version of the server'),
minor: z.number().positive().describe('The minor version of the server'),
patch: z.number().positive().describe('The patch version of the server'),
preRelease: z
.string()
.optional()
.describe('The pre release text of the server'),
commit: z.string().optional().describe('The commit of the server'),
fullString: z.string().describe('The full version string of the server'),
})
.describe('The version of the HedgeDoc server.')
export type ServerVersionDto = z.infer<typeof ServerVersionSchema>

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './note.dto.js'
export * from './note.media-deletion.dto.js'
export * from './note-metadata.dto.js'
export * from './note-metadata-update.dto.js'

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const NoteMetadataUpdateSchema = z
.object({
title: z
.string()
.describe(
'The new title of the note. Can not contain any markup and might be empty',
),
description: z
.string()
.describe(
'The new description of the note. Can not contain any markup but might be empty.',
),
tags: z.array(z.string()).describe('The new tags for this note.'),
})
.describe('DTO for updating the note metadata')
export type NoteMetadataUpdate = z.infer<typeof NoteMetadataUpdateSchema>

View file

@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { AliasSchema } from '../alias/index.js'
import { NotePermissionsSchema } from '../permissions/index.js'
export const NoteMetadataSchema = z
.object({
id: z.string().describe('The id of the note'),
aliases: z.array(AliasSchema).describe('All aliases of the note'),
primaryAddress: z
.string()
.describe(
'The primary address/alias of the note. If at least one alias is set, this is the primary alias.',
),
title: z
.string()
.describe(
'The title of the note. Does not contain any markup but might be empty.',
),
description: z
.string()
.describe(
'The description of the note. Does not contain any markup but might be empty.',
),
tags: z.array(z.string()).describe('List of tags assigned to this note'),
version: z
.number()
.describe('The HedgeDoc version this note was created in'),
updatedAt: z
.string()
.datetime()
.describe('The timestamp when the note was last updated'),
updateUsername: z
.string()
.nullable()
.describe('The user that last updated the note'),
viewCount: z
.number()
.describe('Counts how many times the note has been viewed'),
createdAt: z
.string()
.datetime()
.describe('Timestamp when the note was created'),
editedBy: z.array(z.string()).describe('List of users who edited the note'),
permissions: NotePermissionsSchema.describe(
'The permissions of the current note',
),
})
.describe('The metadata of a note')
export type NoteMetadataDto = z.infer<typeof NoteMetadataSchema>

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { EditSchema } from '../edit/edit.dto.js'
import { NoteMetadataSchema } from './note-metadata.dto.js'
export const NoteSchema = z
.object({
content: z.string().describe('The markdown content of the note'),
metadata: NoteMetadataSchema,
editedByAtPosition: z.array(EditSchema).describe('The edit information '),
})
.describe('DTO representing a note')
export type NoteDto = z.infer<typeof NoteSchema>

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const NoteMediaDeletionSchema = z
.object({
keepMedia: z
.boolean()
.describe(
'Indicates whether existing media uploads for the note should be kept',
),
})
.describe(
'DTO for deleting a note with the option to remove associated uploads as well',
)
export type NoteMediaDeletionDto = z.infer<typeof NoteMediaDeletionSchema>

View file

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const ChangeNoteOwnerSchema = z
.object({
owner: z.string().describe('The username of the new owner.'),
})
.describe('DTO to change the owner of a note.')
export type ChangeNoteOwnerDto = z.infer<typeof ChangeNoteOwnerSchema>

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum GuestAccess {
DENY = 'deny',
READ = 'read',
WRITE = 'write',
CREATE = 'create',
}
export const getGuestAccessOrdinal = (guestAccess: GuestAccess): number => {
switch (guestAccess) {
case GuestAccess.DENY:
return 0
case GuestAccess.READ:
return 1
case GuestAccess.WRITE:
return 2
case GuestAccess.CREATE:
return 3
default:
throw Error('Unknown permission')
}
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './change-note-owner.dto.js'
export * from './guest-access.enum.js'
export * from './note-group-permission-entry.dto.js'
export * from './note-group-permission-update.dto.js'
export * from './note-permissions-update.dto.js'
export * from './note-permissions.dto.js'
export * from './note-user-permission-entry.dto.js'
export * from './note-user-permission-update.dto.js'

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const NoteGroupPermissionEntrySchema = z
.object({
groupName: z.string().describe('The name of the group'),
canEdit: z.boolean().describe('If the group can edit or only read'),
})
.describe('DTO for the permission a group has.')
export type NoteGroupPermissionEntryDto = z.infer<
typeof NoteGroupPermissionEntrySchema
>

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const NoteGroupPermissionUpdateSchema = z
.object({
groupName: z.string().describe('The name of the group'),
canEdit: z.boolean().describe('If the group can edit or only read'),
})
.describe('DTO to update the permission of a group.')
export type NoteGroupPermissionUpdateDto = z.infer<
typeof NoteGroupPermissionUpdateSchema
>

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { NoteUserPermissionUpdateSchema } from './note-user-permission-update.dto.js'
import { NoteGroupPermissionUpdateSchema } from './note-group-permission-update.dto.js'
export const NotePermissionsUpdateSchema = z
.object({
sharedToUsers: z
.array(NoteUserPermissionUpdateSchema)
.describe('List of users the note is shared with'),
sharedToGroups: z
.array(NoteGroupPermissionUpdateSchema)
.describe('List of groups that the note is shared with'),
})
.describe('DTO to update the permissions of a note.')
export type NotePermissionsUpdateDto = z.infer<
typeof NotePermissionsUpdateSchema
>

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { NoteUserPermissionEntrySchema } from './note-user-permission-entry.dto.js'
import { NoteGroupPermissionEntrySchema } from './note-group-permission-entry.dto.js'
export const NotePermissionsSchema = z
.object({
owner: z.string().nullable().describe('Username of the owner of the note'),
sharedToUsers: z
.array(NoteUserPermissionEntrySchema)
.describe('List of users the note is shared with'),
sharedToGroups: z
.array(NoteGroupPermissionEntrySchema)
.describe('List of groups that the note is shared with'),
})
.describe('Represents the permissions of a note')
export type NotePermissionsDto = z.infer<typeof NotePermissionsSchema>

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const NoteUserPermissionEntrySchema = z
.object({
username: z.string().describe('The name of the user'),
canEdit: z.boolean().describe('If the group can edit or only read'),
})
.describe('DTO for the permission a group has.')
export type NoteUserPermissionEntryDto = z.infer<
typeof NoteUserPermissionEntrySchema
>

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const NoteUserPermissionUpdateSchema = z
.object({
username: z.string().toLowerCase().describe('The name of the user'),
canEdit: z.boolean().describe('If the group can edit or only read'),
})
.describe('DTO to update the permission of a user.')
export type NoteUserPermissionUpdateDto = z.infer<
typeof NoteUserPermissionUpdateSchema
>

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './revision.dto.js'
export * from './revision-metadata.dto.js'

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const RevisionMetadataSchema = z
.object({
id: z.number().describe('The id of the revision.'),
createdAt: z.string().datetime().describe('When the revision was created.'),
length: z
.number()
.positive()
.describe('The length of the content of the revision.'),
authorUsernames: z
.array(z.string().toLowerCase())
.describe(
'A list of all usernames of the users that worked on the revision.',
),
anonymousAuthorCount: z
.number()
.positive()
.describe('Number of anonymous users that worked on the revision.'),
title: z
.string()
.describe(
'The title of the revision. Does not contain any markup but might be empty.',
),
description: z
.string()
.describe(
'The description of the revision. Does not contain any markup but might be empty.',
),
tags: z
.array(z.string())
.describe('List of tags assigned to this revision'),
})
.describe('DTO that describes the metadata of a revision.')
export type RevisionMetadataDto = z.infer<typeof RevisionMetadataSchema>

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { EditSchema } from '../edit/edit.dto.js'
import { RevisionMetadataSchema } from './revision-metadata.dto.js'
export const RevisionSchema = RevisionMetadataSchema.merge(
z.object({
content: z.string().describe('The content of the revision'),
patch: z.string().describe('The patch or diff to the previous revision'),
edits: z
.array(EditSchema)
.describe('A list of users, who created this revision'),
}),
).describe(
'A revision is the state of a note content at a specific time. This is used to go back to previous version of a note.',
)
export type RevisionDto = z.infer<typeof RevisionSchema>

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { FullUserInfoSchema } from './full-user-info.dto.js'
export const FullUserInfoWithIdSchema = FullUserInfoSchema.merge(
z.object({
id: z.string().describe('The id from the LDAP server'),
}),
).describe(
'The full user information with id is only used during the LDAP login process',
)
export type FullUserInfoWithIdDto = z.infer<typeof FullUserInfoWithIdSchema>

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { UserInfoSchema } from './user-info.dto.js'
export const FullUserInfoSchema = UserInfoSchema.merge(
z.object({
email: z
.string()
.email()
.nullable()
.describe('The email address of the user if known'),
}),
).describe(
'The full user information is only presented to the logged in user itself. For privacy reasons the email address is only here',
)
export type FullUserInfoDto = z.infer<typeof FullUserInfoSchema>

View file

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './full-user-info.dto.js'
export * from './full-user-info-with-id.dto.js'
export * from './login-user-info.dto.js'
export * from './update-user-info.dto.js'
export * from './user-info.dto.js'

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
import { ProviderType } from '../auth/index.js'
import { FullUserInfoSchema } from './full-user-info.dto.js'
export const LoginUserInfoSchema = FullUserInfoSchema.merge(
z.object({
authProvider: z
.nativeEnum(ProviderType)
.describe('The type of login provider used for the current session'),
}),
).describe(
'Information about the user and their auth method for the current session',
)
export type LoginUserInfoDto = z.infer<typeof LoginUserInfoSchema>

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const UpdateUserInfoSchema = z
.object({
displayName: z
.string()
.nullable()
.describe('The new display name of the user.'),
email: z.string().email().nullable().describe('The new email of the user.'),
})
.describe('The update of a user profile.')
export type UpdateUserInfoDto = z.infer<typeof UpdateUserInfoSchema>

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { z } from 'zod'
export const UserInfoSchema = z
.object({
username: z.string().describe("The user's username"),
displayName: z.string().describe('The display name of the user'),
photoUrl: z
.string()
.url()
.nullable()
.describe('The URL to the profile picture of the user'),
})
.describe('Represents the public information about a user')
export type UserInfoDto = z.infer<typeof UserInfoSchema>

View file

@ -1,9 +1,9 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './dtos/index.js'
export * from './frontmatter-extractor/index.js'
export * from './message-transporters/index.js'
export * from './note-frontmatter/index.js'

View file

@ -1,8 +1,7 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './permissions.js'
export * from './permissions.types.js'

View file

@ -4,11 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { userCanEdit, userIsOwner } from './permissions.js'
import { NotePermissions, SpecialGroup } from './permissions.types.js'
import { describe, expect, it } from '@jest/globals'
import { NotePermissionsDto, SpecialGroup } from '../dtos/index.js'
describe('Permissions', () => {
const testPermissions: NotePermissions = {
const testPermissions: NotePermissionsDto = {
owner: 'owner',
sharedToUsers: [
{

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NotePermissions, SpecialGroup } from './permissions.types.js'
import { NotePermissionsDto, SpecialGroup } from '../dtos/index.js'
/**
* Checks if the given user is the owner of a note.
@ -13,7 +13,7 @@ import { NotePermissions, SpecialGroup } from './permissions.types.js'
* @return True if the user is the owner of the note
*/
export const userIsOwner = (
permissions: NotePermissions,
permissions: NotePermissionsDto,
user?: string,
): boolean => {
return !!user && permissions.owner === user
@ -27,7 +27,7 @@ export const userIsOwner = (
* @return True if the user has the permission to edit the note
*/
export const userCanEdit = (
permissions: NotePermissions,
permissions: NotePermissionsDto,
user?: string,
): boolean => {
const isOwner = userIsOwner(permissions, user)

View file

@ -1,31 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface NotePermissions {
owner: string | null
sharedToUsers: NoteUserPermissionEntry[]
sharedToGroups: NoteGroupPermissionEntry[]
}
export interface NoteUserPermissionEntry {
username: string
canEdit: boolean
}
export interface NoteGroupPermissionEntry {
groupName: string
canEdit: boolean
}
export enum AccessLevel {
NONE,
READ_ONLY,
WRITEABLE,
}
export enum SpecialGroup {
EVERYONE = '_EVERYONE',
LOGGED_IN = '_LOGGED_IN',
}