diff --git a/backend/knexfile.ts b/backend/knexfile.ts index 03b1e733a..100629cc7 100644 --- a/backend/knexfile.ts +++ b/backend/knexfile.ts @@ -5,7 +5,7 @@ */ import type { Knex } from 'knex'; -/** This is used for the Knex CLI to create migrations during development */ +/** This is used for the Knex CLI to create migrations and a seeded database during development */ const config: { [key: string]: Knex.Config } = { development: { client: 'better-sqlite3', @@ -15,6 +15,9 @@ const config: { [key: string]: Knex.Config } = { migrations: { directory: './src/database/migrations', }, + seeds: { + directory: './src/database/seeds', + }, }, }; diff --git a/backend/src/database/seeds/01_user.ts b/backend/src/database/seeds/01_user.ts new file mode 100644 index 000000000..80c23e5f2 --- /dev/null +++ b/backend/src/database/seeds/01_user.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { Knex } from 'knex'; + +import { ProviderType } from '../../auth/provider-type.enum'; +import { hashPassword } from '../../utils/password'; +import { + FieldNameIdentity, + FieldNameUser, + TableIdentity, + TableUser, +} from '../types'; + +export async function seed(knex: Knex): Promise<void> { + // Clear tables beforehand + await knex(TableUser).del(); + await knex(TableIdentity).del(); + + // Insert user accounts and identities + await knex(TableUser).insert([ + { + [FieldNameUser.username]: null, + [FieldNameUser.guestUuid]: '55b4618a-d5f3-4320-93d3-f3501c73d72b', + [FieldNameUser.displayName]: null, + [FieldNameUser.photoUrl]: null, + [FieldNameUser.email]: null, + [FieldNameUser.authorStyle]: 1, + }, + { + [FieldNameUser.username]: 'test', + [FieldNameUser.guestUuid]: null, + [FieldNameUser.displayName]: 'Local Test User', + [FieldNameUser.photoUrl]: null, + [FieldNameUser.email]: null, + [FieldNameUser.authorStyle]: 2, + }, + ]); + await knex(TableIdentity).insert({ + [FieldNameIdentity.userId]: 2, + [FieldNameIdentity.providerType]: ProviderType.LOCAL, + [FieldNameIdentity.providerIdentifier]: null, + [FieldNameIdentity.providerUserId]: null, + [FieldNameIdentity.passwordHash]: await hashPassword('test123'), + }); +} diff --git a/backend/src/database/seeds/02_api_token.ts b/backend/src/database/seeds/02_api_token.ts new file mode 100644 index 000000000..435e0ecb4 --- /dev/null +++ b/backend/src/database/seeds/02_api_token.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { createHash } from 'crypto'; +import { Knex } from 'knex'; + +import { FieldNameApiToken, TableApiToken } from '../types'; + +export async function seed(knex: Knex): Promise<void> { + // Clear table beforehand + await knex(TableApiToken).del(); + + // Insert an api token + const apiToken = + 'LaD52wgw7pi5zVitv4gR5lxoUa6ncTQGASPmXDSdppB9xcd9kCtqjlrdQ8OOfmG9DNXGvfkIwaOCAv8nRp8IoQ'; + await knex(TableApiToken).insert({ + [FieldNameApiToken.id]: 'pA4mOf51bpY', + [FieldNameApiToken.userId]: 2, + [FieldNameApiToken.label]: 'Local Test User API Token', + [FieldNameApiToken.secretHash]: createHash('sha512') + .update(apiToken) + .digest('hex'), + // Token is valid for 2 years + [FieldNameApiToken.validUntil]: new Date( + new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000, + ), + }); +} diff --git a/backend/src/database/seeds/03_note.ts b/backend/src/database/seeds/03_note.ts new file mode 100644 index 000000000..28074b357 --- /dev/null +++ b/backend/src/database/seeds/03_note.ts @@ -0,0 +1,199 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { NoteType } from '@hedgedoc/commons'; +import { createPatch } from 'diff'; +import { readFileSync } from 'fs'; +import { Knex } from 'knex'; + +import { extractRevisionMetadataFromContent } from '../../revisions/utils/extract-revision-metadata-from-content'; +import { + FieldNameAlias, + FieldNameAuthorshipInfo, + FieldNameNote, + FieldNameNoteGroupPermission, + FieldNameNoteUserPermission, + FieldNameRevision, + FieldNameRevisionTag, + TableAlias, + TableAuthorshipInfo, + TableNote, + TableNoteGroupPermission, + TableNoteUserPermission, + TableRevision, + TableRevisionTag, +} from '../types'; + +export async function seed(knex: Knex): Promise<void> { + // Clear tables beforehand + await knex(TableNote).del(); + await knex(TableAlias).del(); + await knex(TableRevision).del(); + await knex(TableRevisionTag).del(); + await knex(TableAuthorshipInfo).del(); + await knex(TableNoteGroupPermission).del(); + await knex(TableNoteUserPermission).del(); + + const guestNoteAlias = 'guest-note'; + const userNoteAlias = 'user-note'; + const userSlideAlias = 'user-slide'; + + const guestNoteContent = readFileSync('./notes/guest_note.md', 'utf-8'); + const userNoteContent = readFileSync('./notes/local_user_note.md', 'utf-8'); + const userSlideContent = readFileSync('./notes/local_user_slide.md', 'utf-8'); + + const { + title: guestNoteTitle, + description: guestNoteDescription, + tags: guestNoteTags, + } = extractRevisionMetadataFromContent(guestNoteContent); + const { + title: userNoteTitle, + description: userNoteDescription, + tags: userNoteTags, + } = extractRevisionMetadataFromContent(userNoteContent); + const { + title: userSlideTitle, + description: userSlideDescription, + tags: userSlideTags, + } = extractRevisionMetadataFromContent(userSlideContent); + + // Insert a few notes and revisions + await knex(TableNote).insert([ + { + [FieldNameNote.ownerId]: 1, + [FieldNameNote.version]: 2, + }, + { + [FieldNameNote.ownerId]: 2, + [FieldNameNote.version]: 2, + }, + { + [FieldNameNote.ownerId]: 2, + [FieldNameNote.version]: 2, + }, + ]); + await knex(TableAlias).insert([ + { + [FieldNameAlias.noteId]: 1, + [FieldNameAlias.alias]: guestNoteAlias, + [FieldNameAlias.isPrimary]: true, + }, + { + [FieldNameAlias.noteId]: 2, + [FieldNameAlias.alias]: userNoteAlias, + [FieldNameAlias.isPrimary]: true, + }, + { + [FieldNameAlias.noteId]: 1, + [FieldNameAlias.alias]: userSlideAlias, + [FieldNameAlias.isPrimary]: true, + }, + ]); + await knex(TableRevision).insert([ + { + [FieldNameRevision.noteId]: 1, + [FieldNameRevision.patch]: createPatch( + guestNoteAlias, + '', + guestNoteContent, + ), + [FieldNameRevision.content]: guestNoteContent, + [FieldNameRevision.yjsStateVector]: null, + [FieldNameRevision.noteType]: NoteType.DOCUMENT, + [FieldNameRevision.title]: guestNoteTitle, + [FieldNameRevision.description]: guestNoteDescription, + }, + { + [FieldNameRevision.noteId]: 1, + [FieldNameRevision.patch]: createPatch( + userNoteAlias, + '', + userNoteContent, + ), + [FieldNameRevision.content]: userNoteContent, + [FieldNameRevision.yjsStateVector]: null, + [FieldNameRevision.noteType]: NoteType.DOCUMENT, + [FieldNameRevision.title]: userNoteTitle, + [FieldNameRevision.description]: userNoteDescription, + }, + { + [FieldNameRevision.noteId]: 1, + [FieldNameRevision.patch]: createPatch( + userSlideAlias, + '', + userSlideContent, + ), + [FieldNameRevision.content]: userSlideContent, + [FieldNameRevision.yjsStateVector]: null, + [FieldNameRevision.noteType]: NoteType.SLIDE, + [FieldNameRevision.title]: userSlideTitle, + [FieldNameRevision.description]: userSlideDescription, + }, + ]); + await knex(TableRevisionTag).insert([ + ...guestNoteTags.map((tag) => ({ + [FieldNameRevisionTag.revisionId]: 1, + [FieldNameRevisionTag.tag]: tag, + })), + ...userNoteTags.map((tag) => ({ + [FieldNameRevisionTag.revisionId]: 2, + [FieldNameRevisionTag.tag]: tag, + })), + ...userSlideTags.map((tag) => ({ + [FieldNameRevisionTag.revisionId]: 3, + [FieldNameRevisionTag.tag]: tag, + })), + ]); + await knex(TableAuthorshipInfo).insert([ + { + [FieldNameAuthorshipInfo.revisionId]: 1, + [FieldNameAuthorshipInfo.authorId]: 1, + [FieldNameAuthorshipInfo.startPosition]: 0, + [FieldNameAuthorshipInfo.endPosition]: guestNoteContent.length, + }, + { + [FieldNameAuthorshipInfo.revisionId]: 2, + [FieldNameAuthorshipInfo.authorId]: 2, + [FieldNameAuthorshipInfo.startPosition]: 0, + [FieldNameAuthorshipInfo.endPosition]: userNoteContent.length, + }, + { + [FieldNameAuthorshipInfo.revisionId]: 3, + [FieldNameAuthorshipInfo.authorId]: 2, + [FieldNameAuthorshipInfo.startPosition]: 0, + [FieldNameAuthorshipInfo.endPosition]: userSlideContent.length, + }, + ]); + await knex(TableNoteGroupPermission).insert([ + { + [FieldNameNoteGroupPermission.noteId]: 1, + [FieldNameNoteGroupPermission.groupId]: 1, + [FieldNameNoteGroupPermission.canEdit]: true, + }, + { + [FieldNameNoteGroupPermission.noteId]: 2, + [FieldNameNoteGroupPermission.groupId]: 1, + [FieldNameNoteGroupPermission.canEdit]: false, + }, + { + [FieldNameNoteGroupPermission.noteId]: 3, + [FieldNameNoteGroupPermission.groupId]: 1, + [FieldNameNoteGroupPermission.canEdit]: false, + }, + ]); + await knex(TableNoteUserPermission).insert([ + { + [FieldNameNoteUserPermission.noteId]: 2, + [FieldNameNoteUserPermission.userId]: 2, + [FieldNameNoteUserPermission.canEdit]: true, + }, + { + [FieldNameNoteUserPermission.noteId]: 3, + [FieldNameNoteUserPermission.userId]: 2, + [FieldNameNoteUserPermission.canEdit]: true, + }, + ]); +} diff --git a/backend/src/database/seeds/notes/guest_note.md b/backend/src/database/seeds/notes/guest_note.md new file mode 100644 index 000000000..b81ebaf15 --- /dev/null +++ b/backend/src/database/seeds/notes/guest_note.md @@ -0,0 +1,9 @@ +--- +title: Guest User Note +description: A test note for the guest user +tags: + - guest + - note +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`; diff --git a/backend/src/database/seeds/notes/local_user_note.md b/backend/src/database/seeds/notes/local_user_note.md new file mode 100644 index 000000000..2e0cbaf3e --- /dev/null +++ b/backend/src/database/seeds/notes/local_user_note.md @@ -0,0 +1,14 @@ +--- +title: Local Test User Note 1 +description: A test note for the local test user +tags: + - user + - note +--- + +# Title + +Some Text + +- [ ] Write A ToDo-List +- [ ] Check some items diff --git a/backend/src/database/seeds/notes/local_user_slide.md b/backend/src/database/seeds/notes/local_user_slide.md new file mode 100644 index 000000000..dd0280494 --- /dev/null +++ b/backend/src/database/seeds/notes/local_user_slide.md @@ -0,0 +1,29 @@ +--- +title: Local Test User SlideDeck +description: A test SlideDeck for the local test user +tags: + - user + - slide +--- + +## First slide + +\`---\` + +Is the divider of slides + +---- + +### First branch of first the slide + +\`----\` + +Is the divider of branches + +Use the *Space* key to navigate through all slides. + +--- + +## Point of View + +Press **ESC** to enter the slide overview. diff --git a/backend/src/ormconfig.ts b/backend/src/ormconfig.ts deleted file mode 100644 index 9ef531b20..000000000 --- a/backend/src/ormconfig.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { NestFactory } from '@nestjs/core'; -import { NestExpressApplication } from '@nestjs/platform-express'; -import { DataSource } from 'typeorm'; - -import { AppModule } from './app.module'; -import { AppConfig } from './config/app.config'; -import { Loglevel } from './config/loglevel.enum'; -import { ConsoleLoggerService } from './logger/console-logger.service'; - -async function buildDataSource(): Promise<DataSource> { - // We create a new app instance to let it discover entities - const app = await NestFactory.create<NestExpressApplication>(AppModule, { - logger: new ConsoleLoggerService({ loglevel: Loglevel.TRACE } as AppConfig), - }); - const dataSource = app.get(DataSource); - - // The migration CLI does not want an existing connection - await dataSource.destroy(); - - return dataSource; -} - -export default buildDataSource(); diff --git a/backend/src/seed.ts b/backend/src/seed.ts deleted file mode 100644 index 07c3020a0..000000000 --- a/backend/src/seed.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { ProviderType } from '@hedgedoc/commons'; -import { DataSource } from 'typeorm'; - -import { ApiToken } from './api-token/api-token.entity'; -import { Identity } from './auth/identity.entity'; -import { Author } from './authors/author.entity'; -import { User } from './database/user.entity'; -import { Group } from './groups/group.entity'; -import { HistoryEntry } from './history/history-entry.entity'; -import { MediaUpload } from './media/media-upload.entity'; -import { Alias } from './notes/alias.entity'; -import { Note } from './notes/note.entity'; -import { Tag } from './notes/tag.entity'; -import { NoteGroupPermission } from './permissions/note-group-permission.entity'; -import { NoteUserPermission } from './permissions/note-user-permission.entity'; -import { Edit } from './revisions/edit.entity'; -import { Revision } from './revisions/revision.entity'; -import { Session } from './sessions/session.entity'; -import { hashPassword } from './utils/password'; - -/** - * This function creates and populates a sqlite db for manual testing - */ -const dataSource = new DataSource({ - type: 'sqlite', - database: './hedgedoc.sqlite', - entities: [ - User, - Note, - Revision, - Edit, - NoteGroupPermission, - NoteUserPermission, - Group, - HistoryEntry, - MediaUpload, - Tag, - ApiToken, - Identity, - Author, - Session, - Alias, - ], - synchronize: true, - logging: false, - dropSchema: true, -}); - -dataSource - .initialize() - .then(async () => { - const password = 'test_password'; - const users = []; - users.push(User.create('hardcoded', 'Test User 1')); - users.push(User.create('hardcoded_2', 'Test User 2')); - users.push(User.create('hardcoded_3', 'Test User 3')); - const notes: Note[] = []; - notes.push(Note.create(null, 'test') as Note); - notes.push(Note.create(null, 'test2') as Note); - notes.push(Note.create(null, 'test3') as Note); - - for (let i = 0; i < 3; i++) { - const author = (await dataSource.manager.save( - Author.create(1), - )) as Author; - const user = (await dataSource.manager.save(users[i])) as User; - const identity = Identity.create(user, ProviderType.LOCAL, null); - identity.passwordHash = await hashPassword(password); - dataSource.manager.create(Identity, identity); - author.user = dataSource.manager.save(user); - const revision = Revision.create( - 'This is a test note', - 'This is a test note', - notes[i], - null, - 'Test note', - '', - [], - ) as Revision; - const edit = Edit.create(author, 1, 42) as Edit; - revision.edits = Promise.resolve([edit]); - notes[i].revisions = Promise.all([revision]); - notes[i].userPermissions = Promise.resolve([]); - notes[i].groupPermissions = Promise.resolve([]); - user.ownedNotes = Promise.resolve([notes[i]]); - await dataSource.manager.save([ - notes[i], - user, - revision, - edit, - author, - identity, - ]); - } - const createdUsers = await dataSource.manager.find(User); - const groupEveryone = Group.create('_EVERYONE', 'Everyone', true) as Group; - const groupLoggedIn = Group.create( - '_LOGGED_IN', - 'Logged-in users', - true, - ) as Group; - await dataSource.manager.save([groupEveryone, groupLoggedIn]); - - for (let i = 0; i < 3; i++) { - if (i === 0) { - const permission1 = NoteUserPermission.create( - createdUsers[0], - notes[i], - true, - ); - const permission2 = NoteUserPermission.create( - createdUsers[1], - notes[i], - false, - ); - notes[i].userPermissions = Promise.resolve([permission1, permission2]); - notes[i].groupPermissions = Promise.resolve([]); - await dataSource.manager.save([notes[i], permission1, permission2]); - } - - if (i === 1) { - const readPermission = NoteGroupPermission.create( - groupEveryone, - notes[i], - false, - ); - notes[i].userPermissions = Promise.resolve([]); - notes[i].groupPermissions = Promise.resolve([readPermission]); - await dataSource.manager.save([notes[i], readPermission]); - } - - if (i === 2) { - notes[i].owner = Promise.resolve(createdUsers[0]); - await dataSource.manager.save([notes[i]]); - } - } - - const foundUsers = await dataSource.manager.find(User); - if (!foundUsers) { - throw new Error('Could not find freshly seeded users. Aborting.'); - } - const foundNotes = await dataSource.manager.find(Note, { - relations: ['aliases'], - }); - if (!foundNotes) { - throw new Error('Could not find freshly seeded notes. Aborting.'); - } - for (const note of foundNotes) { - if (!(await note.aliases)[0]) { - throw new Error( - 'Could not find alias of freshly seeded notes. Aborting.', - ); - } - } - for (const user of foundUsers) { - console.log( - `Created User '${user.username}' with password '${password}'`, - ); - } - for (const note of foundNotes) { - console.log(`Created Note '${(await note.aliases)[0].name ?? ''}'`); - } - for (const user of foundUsers) { - for (const note of foundNotes) { - const historyEntry = HistoryEntry.create(user, note); - await dataSource.manager.save(historyEntry); - console.log( - `Created HistoryEntry for user '${user.username}' and note '${ - (await note.aliases)[0].name ?? '' - }'`, - ); - } - } - }) - .catch((error) => console.log(error));