diff --git a/app/Controllers/Http/Admin/AdminuserController.ts b/app/Controllers/Http/Admin/AdminuserController.ts index 05f25a4..2962cbd 100644 --- a/app/Controllers/Http/Admin/AdminuserController.ts +++ b/app/Controllers/Http/Admin/AdminuserController.ts @@ -85,9 +85,9 @@ export default class AdminuserController { // return response.badRequest(error.messages); throw error; } - - const input: Record = request.only(['login', 'email', 'first_name', 'last_name']); - input.password = request.input('new_password'); + + const input: Record = request.only(['login', 'email','first_name', 'last_name']); + input.password = request.input('new_password'); const user = await User.create(input); if (request.input('roles')) { const roles: Array = request.input('roles'); diff --git a/app/Controllers/Http/Admin/MimetypeController.ts b/app/Controllers/Http/Admin/MimetypeController.ts index 932fb55..561c0ad 100644 --- a/app/Controllers/Http/Admin/MimetypeController.ts +++ b/app/Controllers/Http/Admin/MimetypeController.ts @@ -33,7 +33,7 @@ export default class MimetypeController { // Step 2 - Validate request body against the schema // await request.validate({ schema: newDatasetSchema, messages: this.messages }); const validator = vine.compile(newDatasetSchema); - validator.messagesProvider = new SimpleMessagesProvider(this.messages); + validator.messagesProvider = new SimpleMessagesProvider(this.messages); await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) }); } catch (error) { // Step 3 - Handle errors @@ -168,7 +168,7 @@ export default class MimetypeController { mimetype, }); } catch (error) { - if (error.code === 'E_ROW_NOT_FOUND') { + if (error.code == 'E_ROW_NOT_FOUND') { session.flash({ warning: 'Mimetype is not found in database' }); } else { session.flash({ warning: 'general error occured, you cannot delete the mimetype' }); diff --git a/app/Controllers/Http/Admin/mailsettings_controller.ts b/app/Controllers/Http/Admin/mailsettings_controller.ts index 332fd05..628a9fc 100644 --- a/app/Controllers/Http/Admin/mailsettings_controller.ts +++ b/app/Controllers/Http/Admin/mailsettings_controller.ts @@ -85,14 +85,15 @@ export default class MailSettingsController { } try { - await mail.send((message) => { - message - // .from(Config.get('mail.from.address')) - .from('tethys@geosphere.at') - .to(userEmail) - .subject('Test Email') - .html('

If you received this email, the email configuration seems to be correct.

'); - }); + await mail.send( + (message) => { + message + // .from(Config.get('mail.from.address')) + .from('tethys@geosphere.at') + .to(userEmail) + .subject('Test Email') + .html('

If you received this email, the email configuration seems to be correct.

'); + }); return response.json({ success: true, message: 'Test email sent successfully' }); // return response.flash('Test email sent successfully!', 'message').redirect().back(); diff --git a/app/Controllers/Http/Api/DatasetController.ts b/app/Controllers/Http/Api/DatasetController.ts index 8cb17b6..9106521 100644 --- a/app/Controllers/Http/Api/DatasetController.ts +++ b/app/Controllers/Http/Api/DatasetController.ts @@ -210,13 +210,13 @@ export default class DatasetController { */ private async buildVersionChain(dataset: Dataset) { const versionChain = { - // current: { - // id: dataset.id, - // publish_id: dataset.publish_id, - // doi: dataset.identifier?.value || null, - // main_title: dataset.mainTitle || null, - // server_date_published: dataset.server_date_published, - // }, + current: { + id: dataset.id, + publish_id: dataset.publish_id, + doi: dataset.identifier?.value || null, + main_title: dataset.mainTitle || null, + server_date_published: dataset.server_date_published, + }, previousVersions: [] as any[], newerVersions: [] as any[], }; @@ -233,181 +233,92 @@ export default class DatasetController { /** * Recursively get all previous versions */ - // private async getPreviousVersions(datasetId: number, visited: Set = new Set()): Promise { - // // Prevent infinite loops - // if (visited.has(datasetId)) { - // return []; - // } - // visited.add(datasetId); - - // const previousVersions: any[] = []; - - // // Find references where this dataset "IsNewVersionOf" another dataset - // const previousRefs = await DatasetReference.query() - // .where('document_id', datasetId) - // .where('relation', 'IsNewVersionOf') - // .whereNotNull('related_document_id'); - - // for (const ref of previousRefs) { - // if (!ref.related_document_id) continue; - - // const previousDataset = await Dataset.query() - // .where('id', ref.related_document_id) - // .preload('identifier') - // .preload('titles') - // .first(); - - // if (previousDataset) { - // const versionInfo = { - // id: previousDataset.id, - // publish_id: previousDataset.publish_id, - // doi: previousDataset.identifier?.value || null, - // main_title: previousDataset.mainTitle || null, - // server_date_published: previousDataset.server_date_published, - // relation: 'IsPreviousVersionOf', // From perspective of current dataset - // }; - - // previousVersions.push(versionInfo); - - // // Recursively get even older versions - // const olderVersions = await this.getPreviousVersions(previousDataset.id, visited); - // previousVersions.push(...olderVersions); - // } - // } - - // return previousVersions; - // } - private async getPreviousVersions(datasetId: number, visited: Set = new Set()): Promise { - if (visited.has(datasetId)) return []; + // Prevent infinite loops + if (visited.has(datasetId)) { + return []; + } visited.add(datasetId); - const result: any[] = []; + const previousVersions: any[] = []; - // A dataset points to its OLDER version via relation 'IsNewVersionOf' - const refs = await DatasetReference.query() + // Find references where this dataset "IsNewVersionOf" another dataset + const previousRefs = await DatasetReference.query() .where('document_id', datasetId) - .where('relation', 'IsNewVersionOf'); // ← removed .whereNotNull('related_document_id') + .where('relation', 'IsNewVersionOf') + .whereNotNull('related_document_id'); - for (const ref of refs) { - const related = await this.resolveReferencedDataset(ref, datasetId); - if (!related) continue; + for (const ref of previousRefs) { + if (!ref.related_document_id) continue; - result.push({ - id: related.id, - publish_id: related.publish_id, - doi: related.identifier?.value || null, - main_title: related.mainTitle || null, - server_date_published: related.server_date_published, - relation: 'IsPreviousVersionOf', - }); + const previousDataset = await Dataset.query() + .where('id', ref.related_document_id) + .preload('identifier') + .preload('titles') + .first(); - result.push(...(await this.getPreviousVersions(related.id, visited))); + if (previousDataset) { + const versionInfo = { + id: previousDataset.id, + publish_id: previousDataset.publish_id, + doi: previousDataset.identifier?.value || null, + main_title: previousDataset.mainTitle || null, + server_date_published: previousDataset.server_date_published, + relation: 'IsPreviousVersionOf', // From perspective of current dataset + }; + + previousVersions.push(versionInfo); + + // Recursively get even older versions + const olderVersions = await this.getPreviousVersions(previousDataset.id, visited); + previousVersions.push(...olderVersions); + } } - return result; + return previousVersions; } /** * Recursively get all newer versions */ - // private async getNewerVersions(datasetId: number, visited: Set = new Set()): Promise { - // // Prevent infinite loops - // if (visited.has(datasetId)) { - // return []; - // } - // visited.add(datasetId); - - // const newerVersions: any[] = []; - - // // Find references where this dataset "IsPreviousVersionOf" another dataset - // const newerRefs = await DatasetReference.query() - // .where('document_id', datasetId) - // .where('relation', 'IsPreviousVersionOf') - // .whereNotNull('related_document_id'); - - // for (const ref of newerRefs) { - // if (!ref.related_document_id) continue; - - // const newerDataset = await Dataset.query().where('id', ref.related_document_id).preload('identifier').preload('titles').first(); - - // if (newerDataset) { - // const versionInfo = { - // id: newerDataset.id, - // publish_id: newerDataset.publish_id, - // doi: newerDataset.identifier?.value || null, - // main_title: newerDataset.mainTitle || null, - // server_date_published: newerDataset.server_date_published, - // relation: 'IsNewVersionOf', // From perspective of current dataset - // }; - - // newerVersions.push(versionInfo); - - // // Recursively get even newer versions - // const evenNewerVersions = await this.getNewerVersions(newerDataset.id, visited); - // newerVersions.push(...evenNewerVersions); - // } - // } - - // return newerVersions; - // } private async getNewerVersions(datasetId: number, visited: Set = new Set()): Promise { - if (visited.has(datasetId)) return []; + // Prevent infinite loops + if (visited.has(datasetId)) { + return []; + } visited.add(datasetId); - const result: any[] = []; + const newerVersions: any[] = []; - // A dataset points to its NEWER version via relation 'IsPreviousVersionOf' - const refs = await DatasetReference.query() + // Find references where this dataset "IsPreviousVersionOf" another dataset + const newerRefs = await DatasetReference.query() .where('document_id', datasetId) - .where('relation', 'IsPreviousVersionOf'); // ← removed .whereNotNull(...) + .where('relation', 'IsPreviousVersionOf') + .whereNotNull('related_document_id'); - for (const ref of refs) { - const related = await this.resolveReferencedDataset(ref, datasetId); - if (!related) continue; + for (const ref of newerRefs) { + if (!ref.related_document_id) continue; - result.push({ - id: related.id, - publish_id: related.publish_id, - doi: related.identifier?.value || null, - main_title: related.mainTitle || null, - server_date_published: related.server_date_published, - relation: 'IsNewVersionOf', - }); + const newerDataset = await Dataset.query().where('id', ref.related_document_id).preload('identifier').preload('titles').first(); - result.push(...(await this.getNewerVersions(related.id, visited))); + if (newerDataset) { + const versionInfo = { + id: newerDataset.id, + publish_id: newerDataset.publish_id, + doi: newerDataset.identifier?.value || null, + main_title: newerDataset.mainTitle || null, + server_date_published: newerDataset.server_date_published, + relation: 'IsNewVersionOf', // From perspective of current dataset + }; + + newerVersions.push(versionInfo); + + // Recursively get even newer versions + const evenNewerVersions = await this.getNewerVersions(newerDataset.id, visited); + newerVersions.push(...evenNewerVersions); + } } - return result; - } - - private async resolveReferencedDataset(ref: DatasetReference, currentDatasetId: number) { - const doi = this.normalizeDoi(ref.value); - - if (doi) { - const byDoi = await Dataset.query() - .whereHas('identifier', (q) => q.where('value', doi)) - .preload('identifier') - .preload('titles') // needed so mainTitle computes - .first(); - if (byDoi) return byDoi; - } - - if (ref.related_document_id && ref.related_document_id !== currentDatasetId) { - return await Dataset.query() - .where('id', ref.related_document_id) - .preload('identifier') - .preload('titles') - .first(); - } - - return null; - } - private normalizeDoi(value: string | null): string | null { - if (!value) return null; - return value - .trim() - .replace(/^https?:\/\/(dx\.)?doi\.org\//i, '') - .replace(/^doi:/i, ''); + return newerVersions; } } diff --git a/app/Controllers/Http/Auth/AuthController.ts b/app/Controllers/Http/Auth/AuthController.ts index 90a02be..08b1c05 100644 --- a/app/Controllers/Http/Auth/AuthController.ts +++ b/app/Controllers/Http/Auth/AuthController.ts @@ -1,68 +1,100 @@ import type { HttpContext } from '@adonisjs/core/http'; import User from '#models/user'; import BackupCode from '#models/backup_code'; +// import Hash from '@ioc:Adonis/Core/Hash'; +// import InvalidCredentialException from 'App/Exceptions/InvalidCredentialException'; import { authValidator } from '#validators/auth'; import hash from '@adonisjs/core/services/hash'; import db from '@adonisjs/lucid/services/db'; import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider'; -import ActivityLogger from '#services/activity_logger'; -import logger from '@adonisjs/core/services/logger'; +// import { Authenticator } from '@adonisjs/auth'; +// import { LoginState } from 'Contracts/enums'; +// import { StatusCodes } from 'http-status-codes'; +// interface MyHttpsContext extends HttpContext { +// auth: Authenticator +// } export default class AuthController { + // login function{ request, auth, response }:HttpContext public async login({ request, response, auth, session }: HttpContext) { + // console.log({ + // registerBody: request.body(), + // }); + // await request.validate(AuthValidator); await request.validateUsing(authValidator); + // const plainPassword = await request.input('password'); + // const email = await request.input('email'); + // grab uid and password values off request body const { email, password } = request.only(['email', 'password']); try { - await db.connection().rawQuery('SELECT 1'); + + await db.connection().rawQuery('SELECT 1') + + + // // attempt to verify credential and login user + // await auth.use('web').attempt(email, plainPassword); + // const user = await auth.use('web').verifyCredentials(email, password); const user = await User.verifyCredentials(email, password); if (user.isTwoFactorEnabled) { - // Noch KEIN abgeschlossenes Login -> nicht loggen. + // session.put("login.id", user.id); + // return view.render("pages/two-factor-challenge"); + session.flash('user_id', user.id); return response.redirect().back(); + + // let state = LoginState.STATE_VALIDATED; + // return response.status(StatusCodes.OK).json({ + // state: state, + // new_user_id: user.id, + // }); } await auth.use('web').login(user); - this.recordAuthEvent('auth.login', { user, ip: request.ip() }); - } catch (error: any) { - // DB nicht erreichbar -> kein fehlgeschlagener Login-Versuch, weiterwerfen + } catch (error) { if (error.code === 'ECONNREFUSED') { - throw error; + throw error } - - // Echter Credential-Fehler -> als fehlgeschlagenen Versuch protokollieren - this.recordAuthEvent('auth.login_failed', { email, ip: request.ip() }); - + // if login fails, return vague form message and redirect back session.flash('message', 'Your username, email, or password is incorrect'); return response.redirect().back(); } - return response.redirect('/apps/dashboard'); + // otherwise, redirect todashboard + response.redirect('/apps/dashboard'); } public async twoFactorChallenge({ request, session, auth, response }: HttpContext) { - const { code, backup_code, login_id } = request.only(['code', 'backup_code', 'login_id']); + const { code, backup_code, login_id } = request.only(['code', 'backup_code', 'login_id']); const user = await User.query().where('id', login_id).firstOrFail(); if (code) { const isValid = await TwoFactorAuthProvider.validate(user, code); if (isValid) { + // login user and redirect to dashboard await auth.use('web').login(user); - this.recordAuthEvent('auth.login', { user, email: user.email, ip: request.ip(), method: '2fa_totp' }); - return response.redirect('/apps/dashboard'); - } - - session.flash('message', 'Your two-factor code is incorrect'); + response.redirect('/apps/dashboard'); + } else { + session.flash('message', 'Your two-factor code is incorrect'); return response.redirect().back(); - } - - if (backup_code) { + } + } else if (backup_code) { const codes: BackupCode[] = await user.getBackupCodes(); + + // const verifiedBackupCodes = await Promise.all( + // codes.map(async (backupCode) => { + // let isVerified = await hash.verify(backupCode.code, backup_code); + // if (isVerified) { + // return backupCode; + // } + // }), + // ); + // const backupCodeToDelete = verifiedBackupCodes.find(Boolean); - let backupCodeToDelete: BackupCode | null = null; + let backupCodeToDelete = null; for (const backupCode of codes) { const isVerified = await hash.verify(backupCode.code, backup_code); if (isVerified) { @@ -71,68 +103,29 @@ export default class AuthController { } } - if (!backupCodeToDelete) { + if (backupCodeToDelete) { + if (backupCodeToDelete.used === false) { + backupCodeToDelete.used = true; + await backupCodeToDelete.save(); + console.log(`BackupCode with id ${backupCodeToDelete.id} has been marked as used.`); + await auth.use('web').login(user); + response.redirect('/apps/dashboard'); + } else { + session.flash('message', 'BackupCode already used'); + return response.redirect().back(); + } + } else { session.flash('message', 'BackupCode not found'); return response.redirect().back(); - } - - if (backupCodeToDelete.used) { - session.flash('message', 'BackupCode already used'); - return response.redirect().back(); - } - - backupCodeToDelete.used = true; - await backupCodeToDelete.save(); - - await auth.use('web').login(user); - this.recordAuthEvent('auth.login', { user, email: user.email, ip: request.ip(), method: '2fa_backup_code' }); - - return response.redirect('/apps/dashboard'); + } } - - // Weder code noch backup_code übergeben - session.flash('message', 'No verification code provided'); - return response.redirect().back(); } - public async logout({ auth, request, response }: HttpContext) { - // Session auflösen -> füllt auth.user, falls eingeloggt - await auth.use('web').check(); - const user = auth.use('web').user; - + // logout function + public async logout({ auth, response }: HttpContext) { + // await auth.logout(); await auth.use('web').logout(); - - if (user) { - this.recordAuthEvent('auth.logout', { user, email: user.email, ip: request.ip() }); - } - return response.redirect('/app/login'); - } - - /** - * Zentraler Audit-Logger für Auth-Events. - * Fire-and-forget: ein Fehler beim Schreiben darf Login/Logout nie blockieren. - */ - private recordAuthEvent( - type: 'auth.login' | 'auth.logout' | 'auth.login_failed', - opts: { user?: User; email?: string; ip: string; method?: string }, - ) { - const { user, email, ip, method } = opts; - - const description = - type === 'auth.login' - ? `${user!.firstName} ${user!.lastName} signed in` - : type === 'auth.logout' - ? `${user!.firstName} ${user!.lastName} signed out` - : `Failed login attempt for ${email ?? 'unknown'}`; - - void ActivityLogger.log({ - type, - description, - userId: user?.id ?? null, - subjectType: user ? 'User' : null, - subjectId: user?.id ?? null, - properties: { ip, ...(method ? { method } : {}) }, - }).catch((err) => logger.error({ err }, `failed to record ${type} activity`)); + // return response.status(200); } } diff --git a/app/Controllers/Http/Submitter/DatasetController.ts b/app/Controllers/Http/Submitter/DatasetController.ts index a5b484c..23789b9 100644 --- a/app/Controllers/Http/Submitter/DatasetController.ts +++ b/app/Controllers/Http/Submitter/DatasetController.ts @@ -40,8 +40,6 @@ import type { Multipart } from '@adonisjs/bodyparser'; import * as fs from 'fs'; import { parseBytesSize, getConfigFor, getTmpPath, formatBytes, errorMessage } from '#app/utils/utility-functions'; import validation from '#services/validation_service'; -import ActivityLogger from '#services/activity_logger'; -import logger from '@adonisjs/core/services/logger'; interface Dictionary { [index: string]: string; @@ -471,7 +469,9 @@ export default class DatasetController { console.error('Error cleaning up temporary file:', cleanupError); } }); - request.multipart.abort(validation.make('files', `Upload limit of ${formatBytes(aggregatedLimit)} exceeded.`, 'limit')); + request.multipart.abort( + validation.make('files', `Upload limit of ${formatBytes(aggregatedLimit)} exceeded.`, 'limit') + ); } }); @@ -513,21 +513,10 @@ export default class DatasetController { trx = await db.transaction(); const user = (await User.find(auth.user?.id)) as User; - // await this.createDatasetAndAssociations(user, request, trx); - const { dataset, mainTitle } = await this.createDatasetAndAssociations(user, request, trx); + await this.createDatasetAndAssociations(user, request, trx); await trx.commit(); console.log('Dataset and related models created successfully'); - - // NACH dem Commit: Dataset ist garantiert persistiert, keine Waisen-Gefahr. - // Fire-and-forget, damit ein Log-Fehler den bereits erfolgreichen Upload nicht kippt. - void ActivityLogger.log({ - type: 'dataset.uploaded', - description: `New publication uploaded: ${mainTitle ?? 'Untitled'}`, - userId: user.id, - subjectType: 'Dataset', - subjectId: dataset.id, - }).catch((err) => logger.error({ err }, 'failed to record dataset.uploaded activity')); } catch (error) { // Clean up temporary files if validation or later steps fail uploadedTmpFiles.forEach((tmpPath) => { @@ -575,15 +564,6 @@ export default class DatasetController { await this.savePersons(dataset, request.input('contributors', []), 'contributor', trx); //save main and additional titles - // const titles = request.input('titles', []); - // for (const titleData of titles) { - // const title = new Title(); - // title.value = titleData.value; - // title.language = titleData.language; - // title.type = titleData.type; - // await dataset.useTransaction(trx).related('titles').save(title); - // } - let mainTitle: string | null = null; const titles = request.input('titles', []); for (const titleData of titles) { const title = new Title(); @@ -591,11 +571,6 @@ export default class DatasetController { title.language = titleData.language; title.type = titleData.type; await dataset.useTransaction(trx).related('titles').save(title); - - if (titleData.type === 'Main') { - // <-- an eure Typ-Konvention anpassen - mainTitle = titleData.value; - } } // save descriptions @@ -689,8 +664,6 @@ export default class DatasetController { await dataset.useTransaction(trx).related('files').save(newFile); await newFile.createHashValues(trx); } - - return { dataset, mainTitle }; // <-- statt void } private generateRandomString(length: number): string { @@ -1122,7 +1095,7 @@ export default class DatasetController { } }); const error = new errors.E_VALIDATION_ERROR({ - files: `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`, + 'files': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`, }); request.multipart.abort(error); } diff --git a/app/controllers/activities_controller.ts b/app/controllers/activities_controller.ts deleted file mode 100644 index 6526108..0000000 --- a/app/controllers/activities_controller.ts +++ /dev/null @@ -1,43 +0,0 @@ -// app/controllers/activities_controller.ts -import type { HttpContext } from '@adonisjs/core/http'; -import Activity from '#models/activity'; - -export default class ActivitiesController { - async index({ response }: HttpContext) { - // const activities = await Activity.query() - // .preload('user', (q) => q.select('id', 'login')) - // .orderBy('created_at', 'desc') - // .limit(10); - - // return response.json( - // activities.map((a) => ({ - // id: a.id, - // type: a.type, - // description: a.description, - // user: a.user?.login ?? null, - // created_at: a.createdAt.toISO(), // relativeTime() expects ISO - // })), - // ); - try { - const activities = await Activity.query() - .preload('user', (q) => q.select('id', 'login')) - .orderBy('created_at', 'desc') - .limit(10); - - return response.json( - activities.map((a) => ({ - id: a.id, - type: a.type, - description: a.description, - user: a.user?.login ?? null, - created_at: a.createdAt.toISO(), // relativeTime() expects ISO - })), - ); - } catch (error) { - console.error('Error fetching activities:', error); - return response.status(500).json({ error: 'Internal Server Error' }); - } - } - - -} diff --git a/app/models/activity.ts b/app/models/activity.ts deleted file mode 100644 index 9d3403a..0000000 --- a/app/models/activity.ts +++ /dev/null @@ -1,57 +0,0 @@ -// app/models/activity.ts -import { DateTime } from 'luxon'; -import { belongsTo, column } from '@adonisjs/lucid/orm'; -import BaseModel from './base_model.js'; -import type { BelongsTo } from '@adonisjs/lucid/types/relations'; -import User from '#models/user'; -import { SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm'; - -export default class Activity extends BaseModel { - public static namingStrategy = new SnakeCaseNamingStrategy(); - public static primaryKey = 'id'; - public static table = 'activities'; - - @column({ isPrimary: true }) - declare id: number; - - @column() - declare type: string; - - @column() - declare userId: number | null; - - @column() - declare subjectType: string | null; - - @column() - declare subjectId: number | null; - - @column() - declare description: string; - - // Manual JSON (de)serialization keeps this working on SQLite/MySQL. - // On Postgres json/jsonb the driver already parses — drop the `consume` - // JSON.parse there to avoid double-handling. - // @column({ - // prepare: (value: Record | null) => (value ? JSON.stringify(value) : null), - // consume: (value: string | null) => (value ? JSON.parse(value) : null), - // }) - // declare properties: Record | null; - - @column() -declare properties: Record | null; - - @column.dateTime({ autoCreate: true }) - declare createdAt: DateTime; - - @column.dateTime({ autoCreate: true, autoUpdate: true }) - declare updatedAt: DateTime; - - // @belongsTo(() => User) - // declare user: BelongsTo; - - @belongsTo(() => User, { - foreignKey: 'userId', - }) - declare user: BelongsTo; -} diff --git a/app/models/dataset.ts b/app/models/dataset.ts index 1bf772b..1233a28 100644 --- a/app/models/dataset.ts +++ b/app/models/dataset.ts @@ -5,10 +5,7 @@ import { belongsTo, hasMany, computed, - hasOne, - afterCreate, - beforeUpdate, - afterUpdate, + hasOne } from '@adonisjs/lucid/orm'; import { DateTime } from 'luxon'; import dayjs from 'dayjs'; @@ -26,11 +23,10 @@ import DatasetIdentifier from './dataset_identifier.js'; import Project from './project.js'; import DocumentXmlCache from './DocumentXmlCache.js'; import DatasetExtension from '#models/traits/dataset_extension'; -import type { ManyToMany } from '@adonisjs/lucid/types/relations'; -import type { BelongsTo } from '@adonisjs/lucid/types/relations'; -import type { HasMany } from '@adonisjs/lucid/types/relations'; -import type { HasOne } from '@adonisjs/lucid/types/relations'; -import ActivityLogger from '#services/activity_logger'; +import type { ManyToMany } from "@adonisjs/lucid/types/relations"; +import type { BelongsTo } from "@adonisjs/lucid/types/relations"; +import type { HasMany } from "@adonisjs/lucid/types/relations"; +import type { HasOne } from "@adonisjs/lucid/types/relations"; export default class Dataset extends DatasetExtension { public static namingStrategy = new SnakeCaseNamingStrategy(); @@ -50,7 +46,7 @@ export default class Dataset extends DatasetExtension { @column({ columnName: 'creating_corporation' }) public creating_corporation: string; - @column.dateTime({ + @column.dateTime({ columnName: 'embargo_date', serialize: (value: Date | null) => { return value ? dayjs(value).format('YYYY-MM-DD') : value; @@ -64,7 +60,7 @@ export default class Dataset extends DatasetExtension { @column({}) public language: string; - @column({ columnName: 'publish_id' }) + @column({columnName: 'publish_id'}) public publish_id: number | null = null; @column({}) @@ -270,12 +266,10 @@ export default class Dataset extends DatasetExtension { return model || null; } - static async getMax(column: string) { - let dataset = await this.query() - .max(column + ' as max_publish_id') - .firstOrFail(); + static async getMax (column: string) { + let dataset = await this.query().max(column + ' as max_publish_id').firstOrFail(); return dataset.$extras.max_publish_id; - } + } @computed({ serializeAs: 'remaining_time', @@ -290,34 +284,4 @@ export default class Dataset extends DatasetExtension { return 0; } } - - // @afterCreate() - // static async logUploaded(dataset: Dataset) { - // await dataset.preload('titles'); - - // await ActivityLogger.log({ - // type: 'dataset.uploaded', - // description: `New publication uploaded: ${dataset.mainTitle ?? 'Untitled'}`, - // subjectType: 'Dataset', - // subjectId: dataset.id, - // }); - // } - - @beforeUpdate() - static capturePublish(dataset: Dataset) { - // $dirty is populated here, before persistence - (dataset as any).$becamePublished = dataset.$dirty.status !== undefined && dataset.status === 'published'; - } - - @afterUpdate() - static async logPublished(dataset: Dataset) { - if ((dataset as any).$becamePublished) { - await ActivityLogger.log({ - type: 'dataset.published', - description: `Publication published: ${dataset.mainTitle}`, - subjectType: 'Dataset', - subjectId: dataset.id, - }); - } - } } diff --git a/app/models/user.ts b/app/models/user.ts index 038b569..7be02b9 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -14,7 +14,6 @@ import type { ManyToMany } from '@adonisjs/lucid/types/relations'; import type { HasMany } from '@adonisjs/lucid/types/relations'; import { compose } from '@adonisjs/core/helpers'; import BackupCode from './backup_code.js'; -import Activity from './activity.js'; const AuthFinder = withAuthFinder(() => hash.use('laravel'), { uids: ['email'], @@ -112,11 +111,6 @@ export default class User extends compose(BaseModel, AuthFinder) { }) public backupcodes: HasMany; - @hasMany(() => Activity, { - foreignKey: 'user_id', - }) - public activities: HasMany; - @computed({ serializeAs: 'is_admin', }) diff --git a/app/services/activity_logger.ts b/app/services/activity_logger.ts deleted file mode 100644 index c81b4e5..0000000 --- a/app/services/activity_logger.ts +++ /dev/null @@ -1,27 +0,0 @@ -// app/services/activity_logger.ts -import Activity from '#models/activity' - -interface LogOptions { - type: string - description: string - userId?: number | null - subjectType?: string | null - subjectId?: number | string | null - properties?: Record -} - -export default class ActivityLogger { - static async log(options: LogOptions): Promise { - await Activity.create({ - type: options.type, - description: options.description, - userId: options.userId ?? null, - subjectType: options.subjectType ?? null, - subjectId: options.subjectId != null ? Number(options.subjectId) : null, - properties: options.properties ?? null, - }) - - // Invalidate the cache if you add one (see Redis section). - // await redis.del('activities:recent') - } -} \ No newline at end of file diff --git a/commands/fix_version_related_ids.ts b/commands/fix_version_related_ids.ts deleted file mode 100644 index 40afc13..0000000 --- a/commands/fix_version_related_ids.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* -|-------------------------------------------------------------------------- -| node ace make:command fix-version-related-ids -| DONE: create commands/fix_version_related_ids.ts -|-------------------------------------------------------------------------- -| Repairs the `related_document_id` foreign key on version references -| (IsNewVersionOf / IsPreviousVersionOf, both directions). -| -| The DOI stored in `value` is the reliable link; `related_document_id` -| is frequently NULL or self-referential. This command resolves the target -| dataset via its DOI and sets `related_document_id` accordingly, correcting -| both NULL and wrong-but-non-null values. -| -| Examples: -| node ace fix:version-related-ids // dry run, all datasets -| node ace fix:version-related-ids --verbose // dry run with per-row detail -| node ace fix:version-related-ids --fix // apply changes -| node ace fix:version-related-ids --fix -p 226 // apply, only refs owned by publish_id 226 -*/ -import { BaseCommand, flags } from '@adonisjs/core/ace'; -import type { CommandOptions } from '@adonisjs/core/types/ace'; -import Dataset from '#models/dataset'; -import DatasetReference from '#models/dataset_reference'; - -export default class FixVersionRelatedIds extends BaseCommand { - static commandName = 'fix:version-related-ids'; - static description = - 'Backfill/repair related_document_id on IsNewVersionOf / IsPreviousVersionOf references by resolving the target dataset via its DOI'; - - public static needsApplication = true; - - @flags.boolean({ alias: 'f', description: 'Apply changes. Without this flag the command runs as a dry run.' }) - public fix: boolean = false; - - @flags.boolean({ alias: 'v', description: 'Verbose output (per-reference detail)' }) - public verbose: boolean = false; - - @flags.number({ alias: 'p', description: 'Only process references owned by this publish_id' }) - public publish_id?: number; - - public static options: CommandOptions = { - startApp: true, - staysAlive: false, - }; - - // Only the version relations, both directions. - private readonly VERSION_RELATIONS = ['IsNewVersionOf', 'IsPreviousVersionOf']; - - async run() { - this.logger.info(`🔍 Scanning ${this.VERSION_RELATIONS.join(' / ')} references...`); - this.logger.info(this.fix ? '✏️ Mode: APPLY (changes will be written)' : '👀 Mode: DRY RUN (no changes written)'); - if (typeof this.publish_id === 'number') { - this.logger.info(`🎯 Filtering by owning publish_id: ${this.publish_id}`); - } - - try { - const query = DatasetReference.query() - .whereIn('relation', this.VERSION_RELATIONS) - .whereIn('type', ['DOI', 'URL']) - .where((q) => { - q.where('value', 'like', '%doi.org/10.24341/tethys.%').orWhere('value', 'like', '%tethys.at/dataset/%'); - }); - - // Restrict to references owned by a specific dataset (by publish_id), if requested. - if (typeof this.publish_id === 'number') { - query.whereHas('dataset', (d) => d.where('publish_id', this.publish_id as number)); - } - - const refs = await query.exec(); - this.logger.info(`🔗 Found ${refs.length} version reference(s) to inspect`); - - let alreadyCorrect = 0; - let filledFromNull = 0; - let correctedWrong = 0; - let unresolved = 0; - - for (const ref of refs) { - const target = await this.resolveTarget(ref); - - if (!target) { - unresolved++; - if (this.verbose) { - this.logger.warning(`⚠️ Reference ${ref.id}: could not resolve target (value: ${ref.value})`); - } - continue; - } - - // Never let a reference point at its own owning document. - if (target.id === ref.document_id) { - unresolved++; - if (this.verbose) { - this.logger.warning( - `⚠️ Reference ${ref.id}: target resolves to its own document (${ref.document_id}); skipping self-link`, - ); - } - continue; - } - - if (ref.related_document_id === target.id) { - alreadyCorrect++; - continue; - } - - const previous = ref.related_document_id; - const wasNull = previous === null || previous === undefined; - - if (this.fix) { - ref.related_document_id = target.id; - await ref.save(); - } - - if (wasNull) { - filledFromNull++; - } else { - correctedWrong++; - } - - if (this.verbose) { - const action = this.fix ? 'Updated' : '📝 Would update'; - this.logger.info( - `${action} reference ${ref.id} (doc ${ref.document_id}, ${ref.relation}): ` + - `related_document_id ${previous ?? 'NULL'} → ${target.id} (publish_id ${target.publish_id})`, - ); - } - } - - this.logger.info('────────────────────────────────────────'); - this.logger.info(`✔️ Already correct: ${alreadyCorrect}`); - this.logger.info(`➕ Filled from NULL: ${filledFromNull}`); - this.logger.info(`🔧 Corrected wrong value: ${correctedWrong}`); - this.logger.info(`⚠️ Unresolved/skipped: ${unresolved}`); - this.logger.info('────────────────────────────────────────'); - - const changes = filledFromNull + correctedWrong; - if (!this.fix && changes > 0) { - this.logger.info(`💡 Dry run only. Re-run with --fix to write ${changes} change(s).`); - } else if (this.fix) { - this.logger.success(`Done. ${changes} reference(s) updated.`); - } else { - this.logger.success('Nothing to change — all version references already linked correctly.'); - } - } catch (error) { - this.logger.error('Error fixing version related_document_id values:', error); - process.exit(1); - } - } - - /** - * Resolve the dataset a version reference points to. - * Prefers the DOI in `value` (reliable); falls back to a tethys publish_id URL. - */ - private async resolveTarget(ref: DatasetReference): Promise { - const doi = this.normalizeDoi(ref.value); - if (doi) { - const byDoi = await Dataset.query() - .whereHas('identifier', (q) => q.where('value', doi)) - .first(); - if (byDoi) return byDoi; - } - - const publishId = this.extractPublishId(ref.value); - if (publishId) { - const byPublishId = await Dataset.query().where('publish_id', publishId).first(); - if (byPublishId) return byPublishId; - } - - return null; - } - - /** - * Strip the resolver prefix so a reference value like - * "https://doi.org/10.24341/tethys.108.2" matches the identifier - * table value "10.24341/tethys.108.2". Returns null if it isn't a DOI. - */ - private normalizeDoi(value: string | null): string | null { - if (!value) return null; - const cleaned = value - .trim() - .replace(/^https?:\/\/(dx\.)?doi\.org\//i, '') - .replace(/^doi:/i, ''); - return /^10\.\d{4,}\//.test(cleaned) ? cleaned : null; - } - - private extractPublishId(value: string | null): number | null { - if (!value) return null; - const urlMatch = value.match(/tethys\.at\/dataset\/(\d+)/); - return urlMatch ? parseInt(urlMatch[1], 10) : null; - } -} \ No newline at end of file diff --git a/config/inertia.ts b/config/inertia.ts index 1bbab4b..7a13efc 100644 --- a/config/inertia.ts +++ b/config/inertia.ts @@ -1,7 +1,6 @@ import { defineConfig } from '@adonisjs/inertia'; import type { HttpContext } from '@adonisjs/core/http'; -import type { InferSharedProps } from '@adonisjs/inertia/types'; -import env from '#start/env'; +import type { InferSharedProps } from '@adonisjs/inertia/types' const inertiaConfig = defineConfig({ /** @@ -22,8 +21,6 @@ const inertiaConfig = defineConfig({ return ctx.session?.flashMessages.get('user_id'); }, - opensearch_host: env.get('OPENSEARCH_HOST'), - flash: (ctx) => { return { message: ctx.session?.flashMessages.get('message'), diff --git a/database/migrations/1782294801884_create_activities_table.ts b/database/migrations/1782294801884_create_activities_table.ts deleted file mode 100644 index 7b497be..0000000 --- a/database/migrations/1782294801884_create_activities_table.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BaseSchema } from '@adonisjs/lucid/schema'; - -export default class extends BaseSchema { - protected tableName = 'activities'; - - async up() { - this.schema.createTable(this.tableName, (table) => { - table.increments('id'); - table.string('type').notNullable().index(); // 'dataset.uploaded', 'auth.login' - table.integer('user_id').unsigned().nullable().references('id').inTable('accounts').onDelete('SET NULL'); - table.string('subject_type').nullable(); // manual morph: model name - table.bigInteger('subject_id').unsigned().nullable(); - table.string('description').notNullable(); - table.json('properties').nullable(); - table.timestamp('created_at'); - table.timestamp('updated_at'); - table.index(['subject_type', 'subject_id']) - table.index('created_at') - }); - } - - async down() { - this.schema.dropTable(this.tableName); - } -} diff --git a/resources/js/Components/Map/SearchMap.vue b/resources/js/Components/Map/SearchMap.vue index efe8b02..25ce75d 100644 --- a/resources/js/Components/Map/SearchMap.vue +++ b/resources/js/Components/Map/SearchMap.vue @@ -1,5 +1,5 @@ @@ -440,4 +210,4 @@ const relativeTime = (iso: string): string => { animation: none; } } - + \ No newline at end of file diff --git a/resources/js/Stores/main.ts b/resources/js/Stores/main.ts index aa1e84c..3a31057 100644 --- a/resources/js/Stores/main.ts +++ b/resources/js/Stores/main.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import axios from 'axios'; -import { Activity, Dataset } from '@/Dataset'; +import { Dataset } from '@/Dataset'; import menu from '@/menu'; // import type Person from '#models/person'; @@ -133,8 +133,6 @@ export const MainService = defineStore('main', { used: 0, codes: [], - activities: [] as Array, // <-- neu - graphData: {}, }), actions: { @@ -205,17 +203,6 @@ export const MainService = defineStore('main', { }); }, - // async fetchApi(resource: string) { - // try { - // const { data } = await axios.get(`/api/${resource}`); - // // @ts-ignore – dynamischer Key - // this[resource] = data; - // } catch (error) { - // console.error(`Failed to fetch ${resource}`, error); - // } - // }, - - setState(state: any) { this.totpState = state; }, diff --git a/start/env.ts b/start/env.ts index 767c6ec..371f1cf 100644 --- a/start/env.ts +++ b/start/env.ts @@ -35,7 +35,6 @@ export default await Env.create(new URL('../', import.meta.url), { HASH_DRIVER: Env.schema.enum(['scrypt', 'argon', 'bcrypt', 'laravel', undefined] as const), OAI_LIST_SIZE: Env.schema.number(), - OPENSEARCH_HOST: Env.schema.string(), /* |---------------------------------------------------------- diff --git a/start/routes/api.ts b/start/routes/api.ts index 5586b16..ca3b079 100644 --- a/start/routes/api.ts +++ b/start/routes/api.ts @@ -8,15 +8,12 @@ import AvatarController from '#controllers/Http/Api/AvatarController'; import UserController from '#controllers/Http/Api/UserController'; import CollectionsController from '#controllers/Http/Api/collections_controller'; import { middleware } from '../kernel.js'; -import ActivitiesController from '#app/controllers/activities_controller'; // Clean DOI URL routes (no /api prefix) // API routes with /api prefix router .group(() => { - router.get('activities', [ActivitiesController, 'index']).as('activities.index'); - router.get('clients', [UserController, 'getSubmitters']).as('client.index').use(middleware.auth()); router.get('authors', [AuthorsController, 'index']).as('author.index').use(middleware.auth()); router.get('datasets', [DatasetController, 'index']).as('dataset.index');