Compare commits

..

40 Commits

Author SHA1 Message Date
f67b736a88 feat: Enhance dataset management and improve frontend components
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m0s
- Added preloads 'allowed_extensions_mimetypes' and 'dependent_array_min_length' in adonisrc.ts
- Updated @symfony/webpack-encore from ^4.6.1 to ^5.0.1
- AdminuserController: Implemented pagination for 10 records in index method
- Enabled reviewers to reject datasets to editors with email notifications (DatasetController.ts)
- Submitter DatasetController: Files now loaded in ascending order (sort_order) in edit mode
- file.ts: Removed serialization of fileData due to browser issues
- Modified FileUpload.vue to mark already uploaded files as deleted
- Improved keyword search in SearchCategoryAutocomplete.vue
- Started development on Category.vue for submitters to categorize DDC
- Added new route /dataset/categorize in routes.ts
- Introduced 2 new rules in start/rules: allowed_extensions_mimetypes.ts and dependent_array_min_length.ts
- Performed npm updates
2024-11-29 15:46:26 +01:00
49bd96ee77 feat: enhance user management, mimetype creation, and validation
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m8s
- **AdminuserController.ts**: enable editing `first_name` and `last_name` for user creation and updates
- **MimetypeController.ts**: add creation support for mimetypes with selectable extensions
- **Models**: add `Mimetype` model (mime_type.ts); add `SnakeCaseNamingStrategy` for User model
- **Validators**:
  - **updateDatasetValidator**: increase title length to 255 and description length to 2500
  - **User Validators**: refine `createUserValidator` and `updateUserValidator` to include `first_name` and `last_name`
- **vanilla_error_reporter**: improve error reporting for wildcard fields
- **SKOS Query**: refine keyword request in `SearchCategoryAutocomplete.vue`
- **UI Enhancements**:
  - improve icon design in wizard (Wizard.vue)
  - add components for mimetype creation (Create.vue and button in Index.vue)
- **Routes**: update `routes.ts` to include new AdonisJS routes
2024-10-31 11:02:36 +01:00
2235f3905a - improved vies and controllers for rejecting datasets with email for reviewer and editor role
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m8s
- falsh also error via config/inertia.ts
- npm updates
2024-09-26 13:51:35 +02:00
b06ccae603 - added @adonisjs/mail
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m2s
- mail_settings_controller for setting smtp settings
- added view ror rjecting dataset for editor
- added new model AppConfig for stroing appwide config values
- better validate_chesum.ts command with process chunking
- added vue3 apps 'BasicSettings' like email, profile settings
- started with 2 multilingual capabilities
- npm updates
2024-09-16 17:59:46 +02:00
010bead723 - add password strength meter for creating or editing user passwords
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m0s
- add public opensearch api host
2024-08-07 14:22:36 +02:00
f4854d70b9 - npm updates
Some checks failed
CI Pipeline / japa-tests (push) Failing after 54s
- added migration files for creating db table for 'backupcodes'
2024-07-29 10:43:36 +02:00
49ea0fc967 - small adaption for OpenAire inside OaiController.ts
Some checks failed
CI Pipeline / japa-tests (push) Failing after 59s
- validate abstratct for max 2500 characters
- small changes inside page for releasing a dataset
- npm updates
- adapted validating messages
2024-07-26 14:51:57 +02:00
005df2e454 - added backup codes for 2 factor authentication
Some checks failed
CI Pipeline / japa-tests (push) Failing after 58s
- npm updates
- coverage validation: elevation ust be positive, depth must be negative
- vinejs-provider.js: get enabled extensions from database, not via validOptions.extnames
- vue components for backup codes: e.g.: PersonalSettings.vue
- validate spaital coverage in leaflet map: draw.component.vue, map.component.vue
- add backup code authentication into Login.vue
- preset to use no preferred reviewer: Release.vue
- 2 new vinejs validation rules: file_scan.ts and file-length.ts
2024-07-08 13:52:20 +02:00
ac473b1e72 - added LicenseController.ts and MimetypeController for enabling mime_types and licences
Some checks failed
CI Pipeline / japa-tests (push) Failing after 58s
- add new authors and contributors only by unique email addresses
- allow multiple file upload
- added validation rule for validating length of uploaded files
- modified Dockerfile for starting "bin/server.js" instead of *server.js"
- npm updates
2024-06-14 12:38:04 +02:00
770e791613 - typsafe vanilla_error_reporter.ts
Some checks failed
CI Pipeline / japa-tests (push) Failing after 59s
- updated Dockerfole for using node 20
- remove validator_old.ts
- npm updates
2024-05-21 14:41:10 +02:00
ec17d79cf2 - replaced validation library @adonisjs/validator with @vinejs/vine (performance)
Some checks failed
CI Pipeline / japa-tests (push) Failing after 56s
- npm updates
2024-05-16 13:47:06 +02:00
08c2edca3b - npm updates
Some checks failed
CI Pipeline / japa-tests (push) Failing after 54s
- renamed 'models' and 'validators' folders
- removed unneccessary files in contracts folder
2024-04-30 11:50:50 +02:00
a29865b781 - renamings to the new naming convetion for adonisjs version 6
Some checks failed
CI Pipeline / japa-tests (push) Failing after 58s
- npm updates
2024-04-29 11:25:50 +02:00
bee76f8d5b - npm added @japa/api-client, @japa/assert, @types/supertest
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m13s
- webpack added opions['__VUE_PROD_HYDRATION_MISMATCH_DETAILS__'] = false;
- bodyparser config replaced whitelistedMethods with allowedMethods
- extended stardust_provider
- adapted tests for adonisjs v6
2024-04-25 15:17:22 +02:00
296c8fd46e - added own provider for drive methods
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m13s
- renamed middleware Role and Can to role_middleware and can_middleware
- added some typing for inertia vue3 components
- npm updates
2024-04-23 19:36:45 +02:00
cb51a4136f - update to AdonisJS 6
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m15s
2024-03-14 20:25:27 +01:00
f828ca4491 - added 2fa authentication during login. see resources/js/Pages/Auth/login.vue
All checks were successful
CI Pipeline / japa-tests (push) Successful in 1m2s
- added validate() method inside app/Srvices/TwoFactorProvider.ts
- added twoFactorChallenge() method inside app/Controllers/Http/Auth/AuthController.ts for logging in via 2fa-code
2024-02-16 15:32:47 +01:00
b2dce0259a - npm updates
All checks were successful
CI Pipeline / japa-tests (push) Successful in 1m29s
- Admins can no longer delete any users or roles
- Additionally, the name of a role in edit mode is now read-only
- extra new SetupConfirmation.vue compoenent for verifying qrcode 2FA
- adapted ci.yaml
2024-02-14 13:47:10 +01:00
4efa53673f - npm updates
All checks were successful
CI Pipeline / japa-tests (push) Successful in 55s
- removed all controller methods from 'app/Controlles/Http/Admin/UsersControllers.ts'
- merged all authentication methods inside 'app/Controllers/Http/Auth/UserController.ts'
2024-02-06 16:39:33 +01:00
68928b5e07 - HomeController.ts: addes api method for showing number of publications per month for given year
All checks were successful
CI Pipeline / japa-tests (push) Successful in 50s
- adapted command ValidateChecksum.ts: on published files are checked. better information logging
- better LineChart.vue component: showing real statistics
- start/routes/apu.ts: added Route.get('/statistic/:year', 'HomeController.findPublicationsPerMonth');
2024-02-02 14:00:54 +01:00
8cef7390d7 - removed extra test datacite accounts from .env
All checks were successful
CI Pipeline / japa-tests (push) Successful in 57s
- updated DoiClient.ts
- removed test error via /welcome page
- npm updates
2024-01-31 13:19:46 +01:00
c9ba7d6adc - added doi registration
Some checks failed
CI Pipeline / japa-tests (push) Failing after 57s
- npm updates for webpack-encore and postcss-loader
- DatasetExtension.ts: use relation contributors for PersonContributor
- added DoiClient.ts and DoiClientContract.ts
- rozes.ts: addes routes for creating and storing doi identifier
- addes xslt doi_datacite.xslt needed for registering DOI identifier
2024-01-26 09:39:03 +01:00
ebc62d9117 - added api UserController.ts for 2FA
Some checks failed
CI Pipeline / japa-tests (push) Failing after 56s
- added PersonalTotpSettings.vue vor enablin/disabling 2FA
- changed User.ts: added attributes: state, twoFactorSecret and twoFactorRecoveryCodes
- added resources/js/utils/toast.ts for notifications
- modified start/routes/api.ts
- npm updates
2024-01-19 15:33:46 +01:00
18635f77b3 - npm updates
Some checks failed
CI Pipeline / japa-tests (push) Failing after 51s
- added views and controller coder for reviewer role
- added program logic for publishing a dataset by editor
- added reviewer menu
- adapted routes.ts for additional routes
2024-01-04 16:40:05 +01:00
c70fa4a0d8 - aded npm packages @types/qrcode, qrcode and node-f2a
Some checks failed
CI Pipeline / japa-tests (push) Failing after 53s
- corrected UsersController.ts and RoleController.ts with correct routes for settings
- added migration script and ui and Controller for 2 Factor Authentication
- npm updates
2023-12-29 15:54:49 +01:00
87e9314b00 - added NcModal.vue, NcActions.vue, NcButton.vue, FirstrunWizard.vue, Card.vue, Page0.vue, Page1.vue, Page2.vue, Page3.vue and some icons
Some checks failed
CI Pipeline / japa-tests (push) Failing after 51s
- added lime color inside tailwind.config.js
- added some utilities scripts needed for components
- npm updates
- changed postcss.config.js for nesting css styles
- added about function to NavBar.vue
2023-12-21 09:30:21 +01:00
cefd9081ae - add AvatarController.ts
Some checks failed
CI Pipeline / japa-tests (push) Failing after 52s
- adapted menu.ts, NavBar.vue, NavBarItem.vue for highlighting active nav item
- NavBarItemLabel.vue for app menu highlighting
- adapted routes.ts
- adapted app.edge for new favicon
- adapted LayoutAuthenticated.vue (:showAsideMenu="false") for showing AsideMenu optional
- new material icons: BriefcaseCheck.vue, SwapHorizontal.vue, AccountGroup.vue, Lock.vue
- started with FirstRunWizard
2023-12-15 17:17:33 +01:00
ae0c471e93 - now authenticated user can change password with check of old password and password confirmination
Some checks failed
CI Pipeline / japa-tests (push) Failing after 52s
- cchanged route app.dashboard to apps.dashboard
- add editor and reviewer relation to Dataset.ts
- added personal menu in asideMenu
- added Approve.vue for editor
- show warning in Index.vue  (editor), if no dataset is loaded
- user Receive.vue without inertia helper form
- npm updates
- added routes in routes.ts
2023-12-12 15:22:25 +01:00
0d51002903 - default routing to "/app/dashboard"
Some checks failed
CI Pipeline / japa-tests (push) Failing after 53s
- default route after login "/app/dashboard" in AuthController.ts
- npm updates
- corrected route in menu.ts
- better styling for listing datasets for editor and submitter in Index.vue
- personal setting to route "/settings/user"
2023-12-01 10:44:19 +01:00
6fef581dd0 - small adaptions for AsideMenuItem.vue, AsideMenuLayer.vue
All checks were successful
CI Pipeline / japa-tests (push) Successful in 50s
- new routes editor.dataset.list and editor.dataset.update
- fir functionalities for editor role, suche as listing and receiving released datasets
- npm updates
2023-11-30 13:40:32 +01:00
c1e056b9fc Uploaded organigram to support reply to reviewer 2 in R05
All checks were successful
CI Pipeline / japa-tests (push) Successful in 52s
2023-11-30 11:43:25 +00:00
bf9d25ae3e - advanced AsideMenuList.vue, AsideMenuItem.vue
All checks were successful
CI Pipeline / japa-tests (push) Successful in 53s
- npm updates
- load menu in AsideMenu.vue via main.ts store for saving the satus of menu items
- extended jappa tests: test also permission on dataset controller code
2023-11-29 16:52:41 +01:00
b6fdfbff41 - addes @adonisjs/redis fo saving session into redis with redis.ts contract and config
Some checks failed
CI Pipeline / japa-tests (push) Failing after 52s
- npm updated
- added createHashValues and dlete inside File.ts
- added dataset_count property inside Subject.ts
- corrected rotes.ts with correct permissions
2023-11-27 17:17:22 +01:00
d8bdce1369 - added npm package dotenv-webpack for using env variables on clientside
All checks were successful
CI Pipeline / japa-tests (push) Successful in 53s
- added API File Controller for downloading files e.g. /api/download/1022
- also create has codes by submitting new dataset
- added edit dataset functionalities for role submitter
- added the following route for role submitter: /dataset/:id/update', 'DatasetController.update'
- created extra UpdateDatasetValidator.ts for validating updated dataset
- npm updates
2023-11-22 17:06:55 +01:00
a7142f694f - prettier formatting
All checks were successful
CI Pipeline / japa-tests (push) Successful in 51s
- npm updates
- new SearchMap.vue component
2023-10-31 15:38:43 +01:00
7bc9f90cca - implemented spatial filtering
All checks were successful
CI Pipeline / japa-tests (push) Successful in 51s
- Component 'draw.component.vue' has been extended with the 'preserve' property to control whether the drawn rectangle disappears again
- npm updates
2023-10-23 15:27:39 +02:00
2360a81d1e - added route for showing map with all bounding boxes
All checks were successful
CI Pipeline / japa-tests (push) Successful in 50s
- npm updates
- new Map.vue
2023-10-20 15:26:25 +02:00
cf859ba402 - remove VOLUME assignments from DOXKERFILE
All checks were successful
CI Pipeline / japa-tests (push) Successful in 54s
- add package @opensearch-project/opensearch for manipulating opensearch index
- index tethys datasets via new command  IndexDatasets, callable node ace index:datasets or node ace index:datasets -p 193
- add mapping file for opensearch index in public/records.json
- added solr.xslt for transforming Datset model to json for opensearch adding in opensearch
- added route /editor/ dataset/:id/update (beginning of editor/DatasetController.ts
- npm updates
2023-10-17 15:45:41 +02:00
7915f66dd6 - added earliestPublicationDate for App/Models/Dataset.ts
All checks were successful
CI Pipeline / japa-tests (push) Successful in 49s
- new classes TokenWorkerService.ts, TokenWorker.ts and ResumptionToken.ts for using REDIS with paging OAI results
- deletd public/asstes2/langCodeMap.xml: integrated it directly in datasetxml2oai-pmh.xslt
- added redis npm package
- added TokenWorkerProvider.ts for using singleton of TokenWorkerService inside OaiController.ts
- added config/oai.ts for oai related configs from .env-file
- adapted XmlModel.ts for grting domDocument from database
2023-10-03 21:11:02 +02:00
2a7480d2ed - added new class CollectionRole.ts
All checks were successful
CI Pipeline / japa-tests (push) Successful in 50s
- added relation 'collectionRole' to Collection.ts class
- added 'ListSets' and  =GetRecord request for OaiController.ts
- npm updates
- added utility-functions.ts
2023-09-28 22:43:46 +02:00
404 changed files with 48877 additions and 25355 deletions

View File

@ -1,72 +0,0 @@
{
"typescript": true,
"commands": [
"./commands",
"@adonisjs/core/build/commands/index.js",
"@adonisjs/repl/build/commands",
"@eidellev/inertia-adonisjs/build/commands",
"@adonisjs/lucid/build/commands"
],
"exceptionHandlerNamespace": "App/Exceptions/Handler",
"aliases": {
"App": "app",
"Config": "config",
"Database": "database",
"Contracts": "contracts"
},
"preloads": [
"./start/routes",
"./start/kernel",
{
"file": "./start/inertia",
"environment": [
"web"
]
},
{
"file": "./start/validator",
"environment": [
"web"
]
}
],
"providers": [
"./providers/AppProvider",
"@adonisjs/core",
"@adonisjs/session",
"@adonisjs/view",
"@adonisjs/shield",
"@eidellev/inertia-adonisjs",
"@adonisjs/lucid",
"@adonisjs/auth",
"@eidellev/adonis-stardust",
"./providers/QueryBuilderProvider"
],
"metaFiles": [
{
"pattern": "public/**",
"reloadServer": false
},
{
"pattern": "resources/views/**/*.edge",
"reloadServer": false
}
],
"aceProviders": [
"@adonisjs/repl"
],
"tests": {
"suites": [
{
"name": "functional",
"files": [
"tests/functional/**/*.spec(.ts|.js)"
],
"timeout": 60000
}
]
},
"testProviders": [
"@japa/preset-adonis/TestsProvider"
]
}

View File

@ -2,7 +2,7 @@ root = true
[*] [*]
indent_style = space indent_style = space
indent_size = 2 indent_size = 4
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true

View File

@ -11,3 +11,10 @@ PG_PORT=5432
PG_USER=lucid PG_USER=lucid
PG_PASSWORD= PG_PASSWORD=
PG_DB_NAME=lucid PG_DB_NAME=lucid
REDIS_CONNECTION=local
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
SMTP_HOST=
SMTP_PORT=
RESEND_API_KEY=

View File

@ -1,24 +1,14 @@
{ {
"extends": [ "extends": ["plugin:adonis/typescriptApp", "prettier"],
"plugin:adonis/typescriptApp", "plugins": ["prettier"],
"prettier"
],
"plugins": [
"prettier"
],
"rules": { "rules": {
"prettier/prettier": [ "prettier/prettier": ["error", { "singleQuote": true }],
"error",
{ "singleQuote": true }
],
"@typescript-eslint/indent": ["error", 4, { "ignoredNodes": ["PropertyDefinition", "TSUnionType"] }], "@typescript-eslint/indent": ["error", 4, { "ignoredNodes": ["PropertyDefinition", "TSUnionType"] }],
"@typescript-eslint/naming-convention": [ "@typescript-eslint/naming-convention": [
"warn", "warn",
{ {
"selector": "interface", "selector": "interface",
"format": [ "format": ["PascalCase"],
"PascalCase"
],
"custom": { "custom": {
"regex": "^I[A-Z]", "regex": "^I[A-Z]",
"match": false "match": false

View File

@ -12,12 +12,12 @@ jobs:
# run build on latest ubuntu # run build on latest ubuntu
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:16-bullseye container: node:18-bullseye
services: services:
mydb: mydb:
image: postgres:latest image: postgres:latest
container_name: mydb # container_name: mydb
env: env:
POSTGRES_USER: alice POSTGRES_USER: alice
POSTGRES_PASSWORD: iEx4Vj7zBb6 POSTGRES_PASSWORD: iEx4Vj7zBb6
@ -28,7 +28,6 @@ jobs:
# Set health checks to wait until postgres has started # Set health checks to wait until postgres has started
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps: steps:
# this will check out the current branch (https://github.com/actions/checkout#Push-a-commit-using-the-built-in-token) # this will check out the current branch (https://github.com/actions/checkout#Push-a-commit-using-the-built-in-token)
- name: 1 Check out repository code - name: 1 Check out repository code
@ -66,7 +65,7 @@ jobs:
&& echo "HASH_DRIVER=bcrypt" >> .env.test && echo "HASH_DRIVER=bcrypt" >> .env.test
&& echo "HOST=127.0.0.1" >> .env.test && echo "HOST=127.0.0.1" >> .env.test
&& echo "PORT=3333" >> .env.test && echo "PORT=3333" >> .env.test
&& echo "APP_NAME=AdonisJs" >> .env.test && echo "APP_NAME=TethysCloud" >> .env.test
&& echo "APP_URL=http://${HOST}:${PORT}" >> .env.test && echo "APP_URL=http://${HOST}:${PORT}" >> .env.test
&& echo "CACHE_VIEWS=false" >> .env.test && echo "CACHE_VIEWS=false" >> .env.test
&& echo "APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej" >> .env.test && echo "APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej" >> .env.test

View File

@ -1,7 +1,7 @@
################## First Stage - Creating base ######################### ################## First Stage - Creating base #########################
# Created a variable to hold our node base image # Created a variable to hold our node base image
ARG NODE_IMAGE=node:18-bookworm-slim ARG NODE_IMAGE=node:20-bookworm-slim
FROM $NODE_IMAGE AS base FROM $NODE_IMAGE AS base
# Install dumb-init and ClamAV, and perform ClamAV database update # Install dumb-init and ClamAV, and perform ClamAV database update
@ -44,7 +44,7 @@ USER node
# initial update of av databases # initial update of av databases
RUN freshclam RUN freshclam
VOLUME /var/lib/clamav # VOLUME /var/lib/clamav
COPY --chown=node:clamav docker-entrypoint.sh /home/node/app/docker-entrypoint.sh COPY --chown=node:clamav docker-entrypoint.sh /home/node/app/docker-entrypoint.sh
RUN chmod +x /home/node/app/docker-entrypoint.sh RUN chmod +x /home/node/app/docker-entrypoint.sh
ENV TZ="Europe/Vienna" ENV TZ="Europe/Vienna"
@ -66,8 +66,11 @@ COPY --chown=node:node . .
################## Third Stage - Building Stage ##################### ################## Third Stage - Building Stage #####################
# In this stage, we will start building dependencies # In this stage, we will start building dependencies
FROM dependencies AS build FROM dependencies AS build
ENV NODE_ENV=production
# We run "node ace build" to build the app (dist folder) for production # We run "node ace build" to build the app (dist folder) for production
RUN node ace build --production RUN node ace build --ignore-ts-errors
# RUN node ace build --production
# RUN node ace build --ignore-ts-errors
################## Final Stage - Production ######################### ################## Final Stage - Production #########################
@ -85,7 +88,7 @@ RUN npm ci --omit=dev
# Copy files to the working directory from the build folder the user # Copy files to the working directory from the build folder the user
COPY --chown=node:node --from=build /home/node/app/build . COPY --chown=node:node --from=build /home/node/app/build .
# Expose port # Expose port
EXPOSE $PORT EXPOSE 3333
ENTRYPOINT ["/home/node/app/docker-entrypoint.sh"] ENTRYPOINT ["/home/node/app/docker-entrypoint.sh"]
# Run the command to start the server using "dumb-init" # Run the command to start the server using "dumb-init"
CMD [ "dumb-init", "node", "server.js" ] CMD [ "dumb-init", "node", "bin/server.js" ]

16
ace
View File

@ -1,16 +0,0 @@
/*
|--------------------------------------------------------------------------
| Ace Commands
|--------------------------------------------------------------------------
|
| This file is the entry point for running ace commands.
|
*/
require('reflect-metadata')
require('source-map-support').install({ handleUncaughtExceptions: false })
const { Ignitor } = require('@adonisjs/core/build/standalone')
new Ignitor(__dirname)
.ace()
.handle(process.argv.slice(2))

View File

@ -1,609 +0,0 @@
{
"commands": {
"validate:checksum": {
"settings": {
"loadApp": true,
"stayAlive": false
},
"commandPath": "./commands/ValidateChecksum",
"commandName": "validate:checksum",
"description": "",
"args": [],
"aliases": [],
"flags": []
},
"dump:rcfile": {
"settings": {},
"commandPath": "@adonisjs/core/build/commands/DumpRc",
"commandName": "dump:rcfile",
"description": "Dump contents of .adonisrc.json file along with defaults",
"args": [],
"aliases": [],
"flags": []
},
"list:routes": {
"settings": {
"loadApp": true,
"stayAlive": true
},
"commandPath": "@adonisjs/core/build/commands/ListRoutes/index",
"commandName": "list:routes",
"description": "List application routes",
"args": [],
"aliases": [],
"flags": [
{
"name": "verbose",
"propertyName": "verbose",
"type": "boolean",
"description": "Display more information"
},
{
"name": "reverse",
"propertyName": "reverse",
"type": "boolean",
"alias": "r",
"description": "Reverse routes display"
},
{
"name": "methods",
"propertyName": "methodsFilter",
"type": "array",
"alias": "m",
"description": "Filter routes by method"
},
{
"name": "patterns",
"propertyName": "patternsFilter",
"type": "array",
"alias": "p",
"description": "Filter routes by the route pattern"
},
{
"name": "names",
"propertyName": "namesFilter",
"type": "array",
"alias": "n",
"description": "Filter routes by route name"
},
{
"name": "json",
"propertyName": "json",
"type": "boolean",
"description": "Output as JSON"
},
{
"name": "table",
"propertyName": "table",
"type": "boolean",
"description": "Output as Table"
},
{
"name": "max-width",
"propertyName": "maxWidth",
"type": "number",
"description": "Specify maximum rendering width. Ignored for JSON Output"
}
]
},
"generate:key": {
"settings": {},
"commandPath": "@adonisjs/core/build/commands/GenerateKey",
"commandName": "generate:key",
"description": "Generate a new APP_KEY secret",
"args": [],
"aliases": [],
"flags": []
},
"repl": {
"settings": {
"loadApp": true,
"environment": "repl",
"stayAlive": true
},
"commandPath": "@adonisjs/repl/build/commands/AdonisRepl",
"commandName": "repl",
"description": "Start a new REPL session",
"args": [],
"aliases": [],
"flags": []
},
"ssr:build": {
"settings": {
"stayAlive": true
},
"commandPath": "@eidellev/inertia-adonisjs/build/commands/Build",
"commandName": "ssr:build",
"description": "Build and watch files for changes",
"args": [],
"aliases": [],
"flags": []
},
"ssr:watch": {
"settings": {
"stayAlive": true
},
"commandPath": "@eidellev/inertia-adonisjs/build/commands/Watch",
"commandName": "ssr:watch",
"description": "Build and watch files for changes",
"args": [],
"aliases": [],
"flags": []
},
"db:seed": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/DbSeed",
"commandName": "db:seed",
"description": "Execute database seeders",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection for the seeders",
"alias": "c"
},
{
"name": "interactive",
"propertyName": "interactive",
"type": "boolean",
"description": "Run seeders in interactive mode",
"alias": "i"
},
{
"name": "files",
"propertyName": "files",
"type": "array",
"description": "Define a custom set of seeders files names to run",
"alias": "f"
},
{
"name": "compact-output",
"propertyName": "compactOutput",
"type": "boolean",
"description": "A compact single-line output"
}
]
},
"db:wipe": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/DbWipe",
"commandName": "db:wipe",
"description": "Drop all tables, views and types in database",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
},
{
"name": "drop-views",
"propertyName": "dropViews",
"type": "boolean",
"description": "Drop all views"
},
{
"name": "drop-types",
"propertyName": "dropTypes",
"type": "boolean",
"description": "Drop all custom types (Postgres only)"
},
{
"name": "force",
"propertyName": "force",
"type": "boolean",
"description": "Explicitly force command to run in production"
}
]
},
"db:truncate": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/DbTruncate",
"commandName": "db:truncate",
"description": "Truncate all tables in database",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
},
{
"name": "force",
"propertyName": "force",
"type": "boolean",
"description": "Explicitly force command to run in production"
}
]
},
"make:model": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/MakeModel",
"commandName": "make:model",
"description": "Make a new Lucid model",
"args": [
{
"type": "string",
"propertyName": "name",
"name": "name",
"required": true,
"description": "Name of the model class"
}
],
"aliases": [],
"flags": [
{
"name": "migration",
"propertyName": "migration",
"type": "boolean",
"alias": "m",
"description": "Generate the migration for the model"
},
{
"name": "controller",
"propertyName": "controller",
"type": "boolean",
"alias": "c",
"description": "Generate the controller for the model"
},
{
"name": "factory",
"propertyName": "factory",
"type": "boolean",
"alias": "f",
"description": "Generate a factory for the model"
}
]
},
"make:migration": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/MakeMigration",
"commandName": "make:migration",
"description": "Make a new migration file",
"args": [
{
"type": "string",
"propertyName": "name",
"name": "name",
"required": true,
"description": "Name of the migration file"
}
],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "The connection flag is used to lookup the directory for the migration file"
},
{
"name": "folder",
"propertyName": "folder",
"type": "string",
"description": "Pre-select a migration directory"
},
{
"name": "create",
"propertyName": "create",
"type": "string",
"description": "Define the table name for creating a new table"
},
{
"name": "table",
"propertyName": "table",
"type": "string",
"description": "Define the table name for altering an existing table"
}
]
},
"make:seeder": {
"settings": {},
"commandPath": "@adonisjs/lucid/build/commands/MakeSeeder",
"commandName": "make:seeder",
"description": "Make a new Seeder file",
"args": [
{
"type": "string",
"propertyName": "name",
"name": "name",
"required": true,
"description": "Name of the seeder class"
}
],
"aliases": [],
"flags": []
},
"make:factory": {
"settings": {},
"commandPath": "@adonisjs/lucid/build/commands/MakeFactory",
"commandName": "make:factory",
"description": "Make a new factory",
"args": [
{
"type": "string",
"propertyName": "model",
"name": "model",
"required": true,
"description": "The name of the model"
}
],
"aliases": [],
"flags": [
{
"name": "model-path",
"propertyName": "modelPath",
"type": "string",
"description": "The path to the model"
},
{
"name": "exact",
"propertyName": "exact",
"type": "boolean",
"description": "Create the factory with the exact name as provided",
"alias": "e"
}
]
},
"migration:run": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/Migration/Run",
"commandName": "migration:run",
"description": "Migrate database by running pending migrations",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
},
{
"name": "force",
"propertyName": "force",
"type": "boolean",
"description": "Explicitly force to run migrations in production"
},
{
"name": "dry-run",
"propertyName": "dryRun",
"type": "boolean",
"description": "Do not run actual queries. Instead view the SQL output"
},
{
"name": "compact-output",
"propertyName": "compactOutput",
"type": "boolean",
"description": "A compact single-line output"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
},
"migration:rollback": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/Migration/Rollback",
"commandName": "migration:rollback",
"description": "Rollback migrations to a specific batch number",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
},
{
"name": "force",
"propertyName": "force",
"type": "boolean",
"description": "Explictly force to run migrations in production"
},
{
"name": "dry-run",
"propertyName": "dryRun",
"type": "boolean",
"description": "Do not run actual queries. Instead view the SQL output"
},
{
"name": "batch",
"propertyName": "batch",
"type": "number",
"description": "Define custom batch number for rollback. Use 0 to rollback to initial state"
},
{
"name": "compact-output",
"propertyName": "compactOutput",
"type": "boolean",
"description": "A compact single-line output"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
},
"migration:status": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/Migration/Status",
"commandName": "migration:status",
"description": "View migrations status",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
}
]
},
"migration:reset": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/Migration/Reset",
"commandName": "migration:reset",
"description": "Rollback all migrations",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
},
{
"name": "force",
"propertyName": "force",
"type": "boolean",
"description": "Explicitly force command to run in production"
},
{
"name": "dry-run",
"propertyName": "dryRun",
"type": "boolean",
"description": "Do not run actual queries. Instead view the SQL output"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
},
"migration:refresh": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/Migration/Refresh",
"commandName": "migration:refresh",
"description": "Rollback and migrate database",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
},
{
"name": "force",
"propertyName": "force",
"type": "boolean",
"description": "Explicitly force command to run in production"
},
{
"name": "dry-run",
"propertyName": "dryRun",
"type": "boolean",
"description": "Do not run actual queries. Instead view the SQL output"
},
{
"name": "seed",
"propertyName": "seed",
"type": "boolean",
"description": "Run seeders"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
},
"migration:fresh": {
"settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/Migration/Fresh",
"commandName": "migration:fresh",
"description": "Drop all tables and re-migrate the database",
"args": [],
"aliases": [],
"flags": [
{
"name": "connection",
"propertyName": "connection",
"type": "string",
"description": "Define a custom database connection",
"alias": "c"
},
{
"name": "force",
"propertyName": "force",
"type": "boolean",
"description": "Explicitly force command to run in production"
},
{
"name": "seed",
"propertyName": "seed",
"type": "boolean",
"description": "Run seeders"
},
{
"name": "drop-views",
"propertyName": "dropViews",
"type": "boolean",
"description": "Drop all views"
},
{
"name": "drop-types",
"propertyName": "dropTypes",
"type": "boolean",
"description": "Drop all custom types (Postgres only)"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
}
},
"aliases": {}
}

24
ace.js Normal file
View File

@ -0,0 +1,24 @@
/*
/*
|--------------------------------------------------------------------------
| JavaScript entrypoint for running ace commands
|--------------------------------------------------------------------------
|
| Since, we cannot run TypeScript source code using "node" binary, we need
| a JavaScript entrypoint to run ace commands.
|
| This file registers the "ts-node/esm" hook with the Node.js module system
| and then imports the "bin/console.ts" file.
|
*/
/**
* Register hook to process TypeScript files using ts-node
*/
import { register } from 'node:module'
register('ts-node/esm', import.meta.url)
/**
* Import ace console entrypoint
*/
await import('./bin/console.js')

115
adonisrc.ts Normal file
View File

@ -0,0 +1,115 @@
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
/*
|--------------------------------------------------------------------------
| Commands
|--------------------------------------------------------------------------
|
| List of ace commands to register from packages. The application commands
| will be scanned automatically from the "./commands" directory.
*/
commands: [
() => import('@adonisjs/core/commands'),
() => import('@adonisjs/lucid/commands'),
() => import('@adonisjs/mail/commands')
],
/*
|--------------------------------------------------------------------------
| Preloads
|--------------------------------------------------------------------------
|
| List of modules to import before starting the application.
|
*/
preloads: [
() => import('./start/routes.js'),
() => import('./start/kernel.js'),
() => import('#start/validator'),
() => import('#start/rules/unique'),
() => import('#start/rules/translated_language'),
() => import('#start/rules/unique_person'),
() => import('#start/rules/file_length'),
() => import('#start/rules/file_scan'),
() => import('#start/rules/allowed_extensions_mimetypes'),
() => import('#start/rules/dependent_array_min_length')
],
/*
|--------------------------------------------------------------------------
| Service providers
|--------------------------------------------------------------------------
|
| List of service providers to import and register when booting the
| application
|
*/
providers: [
// () => import('./providers/AppProvider.js'),
() => import('@adonisjs/core/providers/app_provider'),
() => import('@adonisjs/core/providers/hash_provider'),
{
file: () => import('@adonisjs/core/providers/repl_provider'),
environment: ['repl', 'test'],
},
() => import('@adonisjs/session/session_provider'),
() => import('@adonisjs/core/providers/edge_provider'),
() => import('@adonisjs/shield/shield_provider'),
// () => import('@eidellev/inertia-adonisjs'),
// () => import('@adonisjs/inertia/inertia_provider'),
() => import('#providers/app_provider'),
() => import('#providers/inertia_provider'),
() => import('@adonisjs/lucid/database_provider'),
() => import('@adonisjs/auth/auth_provider'),
// () => import('@eidellev/adonis-stardust'),
() => import('@adonisjs/redis/redis_provider'),
() => import('@adonisjs/encore/encore_provider'),
() => import('@adonisjs/static/static_provider'),
() => import('#providers/stardust_provider'),
() => import('#providers/query_builder_provider'),
() => import('#providers/token_worker_provider'),
// () => import('#providers/validator_provider'),
() => import('#providers/drive/provider/drive_provider'),
// () => import('@adonisjs/core/providers/vinejs_provider'),
() => import('#providers/vinejs_provider'),
() => import('@adonisjs/mail/mail_provider')
// () => import('#providers/mail_provider'),
],
metaFiles: [
{
pattern: 'public/**',
reloadServer: false,
},
{
pattern: 'resources/views/**/*.edge',
reloadServer: false,
},
],
/*
|--------------------------------------------------------------------------
| Tests
|--------------------------------------------------------------------------
|
| List of test suites to organize tests by their type. Feel free to remove
| and add additional suites.
|
*/
tests: {
suites: [
{
files: ['tests/unit/**/*.spec(.ts|.js)'],
name: 'unit',
timeout: 2000,
},
{
files: ['tests/functional/**/*.spec(.ts|.js)'],
name: 'functional',
timeout: 30000,
},
],
forceExit: false,
},
})

View File

@ -1,14 +1,14 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
import User from 'App/Models/User'; import User from '#models/user';
import Role from 'App/Models/Role'; import Role from '#models/role';
import type { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm'; import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
import CreateUserValidator from 'App/Validators/CreateUserValidator'; import { createUserValidator, updateUserValidator } from '#validators/user';
import UpdateUserValidator from 'App/Validators/UpdateUserValidator'; // import { schema, rules } from '@ioc:Adonis/Core/Validator';
import { RenderResponse } from '@ioc:EidelLev/Inertia'; // import Hash from '@ioc:Adonis/Core/Hash';
// import { schema, rules } from '@ioc:Adonis/Core/Validator'; // import { schema, rules } from '@ioc:Adonis/Core/Validator';
export default class UsersController { export default class AdminuserController {
public async index({ auth, request, inertia }: HttpContextContract) { public async index({ auth, request, inertia }: HttpContext) {
const page = request.input('page', 1); const page = request.input('page', 1);
// const limit = 10 // const limit = 10
@ -45,7 +45,7 @@ export default class UsersController {
// .filter(qs) // .filter(qs)
// .preload('focusInterests') // .preload('focusInterests')
// .preload('role') // .preload('role')
.paginate(page, 5); .paginate(page, 10);
// var test = request.all(); // var test = request.all();
@ -61,7 +61,7 @@ export default class UsersController {
}); });
} }
public async create({ inertia }: HttpContextContract) { public async create({ inertia }: HttpContext) {
// let rolesPluck = {}; // let rolesPluck = {};
// (await Role.query().select('id', 'name')).forEach((user) => { // (await Role.query().select('id', 'name')).forEach((user) => {
// rolesPluck[user.id] = user.name; // rolesPluck[user.id] = user.name;
@ -73,18 +73,19 @@ export default class UsersController {
}); });
} }
public async store({ request, response, session }: HttpContextContract) { public async store({ request, response, session }: HttpContext) {
// node ace make:validator CreateUser // node ace make:validator CreateUser
try { try {
// Step 2 - Validate request body against the schema // Step 2 - Validate request body against the schema
await request.validate(CreateUserValidator); // await request.validate(CreateUserValidator);
await request.validateUsing(createUserValidator);
// console.log({ payload }); // console.log({ payload });
} catch (error) { } catch (error) {
// Step 3 - Handle errors // Step 3 - Handle errors
// return response.badRequest(error.messages); // return response.badRequest(error.messages);
throw error; throw error;
} }
const input = request.only(['login', 'email', 'password']); const input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
const user = await User.create(input); const user = await User.create(input);
if (request.input('roles')) { if (request.input('roles')) {
const roles: Array<number> = request.input('roles'); const roles: Array<number> = request.input('roles');
@ -92,10 +93,10 @@ export default class UsersController {
} }
session.flash('message', 'User has been created successfully'); session.flash('message', 'User has been created successfully');
return response.redirect().toRoute('user.index'); return response.redirect().toRoute('settings.user.index');
} }
public async show({ request, inertia }: HttpContextContract) { public async show({ request, inertia }: HttpContext) {
const id = request.param('id'); const id = request.param('id');
const user = await User.query().where('id', id).firstOrFail(); const user = await User.query().where('id', id).firstOrFail();
@ -110,7 +111,7 @@ export default class UsersController {
}); });
} }
public async edit({ request, inertia }: HttpContextContract) { public async edit({ request, inertia }: HttpContext) {
const id = request.param('id'); const id = request.param('id');
const user = await User.query().where('id', id).firstOrFail(); const user = await User.query().where('id', id).firstOrFail();
@ -125,20 +126,24 @@ export default class UsersController {
}); });
} }
public async update({ request, response, session }: HttpContextContract) { public async update({ request, response, session }: HttpContext) {
// node ace make:validator UpdateUser // node ace make:validator UpdateUser
const id = request.param('id'); const id = request.param('id');
const user = await User.query().where('id', id).firstOrFail(); const user = await User.query().where('id', id).firstOrFail();
// validate update form // validate update form
await request.validate(UpdateUserValidator); await request.validateUsing(updateUserValidator, {
meta: {
objId: user.id,
},
});
// password is optional // password is optional
let input; let input;
if (request.input('password')) { if (request.input('password')) {
input = request.only(['login', 'email', 'password']); input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
} else { } else {
input = request.only(['login', 'email']); input = request.only(['login', 'email', 'first_name', 'last_name']);
} }
await user.merge(input).save(); await user.merge(input).save();
// await user.save(); // await user.save();
@ -149,61 +154,20 @@ export default class UsersController {
} }
session.flash('message', 'User has been updated successfully'); session.flash('message', 'User has been updated successfully');
return response.redirect().toRoute('user.index'); return response.redirect().toRoute('settings.user.index');
} }
public async destroy({ request, response, session }: HttpContextContract) { public async destroy({ request, response, session }: HttpContext) {
const id = request.param('id'); const id = request.param('id');
const user = await User.findOrFail(id); const user = await User.findOrFail(id);
await user.delete(); await user.delete();
session.flash('message', `User ${user.login} has been deleted.`); session.flash('message', `User ${user.login} has been deleted.`);
return response.redirect().toRoute('user.index'); return response.redirect().toRoute('settings.user.index');
} }
/** // private async syncRoles(objId: number, roleIds: Array<number>) {
* Show the user a form to change their personal information & password. // const user = await User.findOrFail(objId)
*
* @return \Inertia\Response
*/
public accountInfo({ inertia, auth }: HttpContextContract): RenderResponse {
const user = auth.user;
// const id = request.param('id');
// const user = await User.query().where('id', id).firstOrFail();
return inertia.render('Admin/User/AccountInfo', {
user: user,
});
}
/**
* Save the modified personal information for a user.
*
* @param HttpContextContract ctx
* @return : RedirectContract
*/
public async accountInfoStore({ request, response, auth, session }: HttpContextContract) {
// validate update form
await request.validate(UpdateUserValidator);
const payload = request.only(['login', 'email']);
auth.user?.merge(payload);
const user = await auth.user?.save();
// $user = \Auth::user()->update($request->except(['_token']));
let message;
if (user) {
message = 'Account updated successfully.';
} else {
message = 'Error while saving. Please try again.';
}
session.flash(message);
return response.redirect().toRoute('admin.account.info');
//->with('message', __($message));
}
// private async syncRoles(userId: number, roleIds: Array<number>) {
// const user = await User.findOrFail(userId)
// // const roles: Role[] = await Role.query().whereIn('id', roleIds); // // const roles: Role[] = await Role.query().whereIn('id', roleIds);
// // await user.roles().sync(roles.rows.map(role => role.id)) // // await user.roles().sync(roles.rows.map(role => role.id))

View File

@ -1,17 +1,17 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
export default class HomeController { export default class HomeController {
public async index({}: HttpContextContract) {} public async index({}: HttpContext) {}
public async create({}: HttpContextContract) {} public async create({}: HttpContext) {}
public async store({}: HttpContextContract) {} public async store({}: HttpContext) {}
public async show({}: HttpContextContract) {} public async show({}: HttpContext) {}
public async edit({}: HttpContextContract) {} public async edit({}: HttpContext) {}
public async update({}: HttpContextContract) {} public async update({}: HttpContext) {}
public async destroy({}: HttpContextContract) {} public async destroy({}: HttpContext) {}
} }

View File

@ -0,0 +1,51 @@
import type { HttpContext } from '@adonisjs/core/http';
import License from '#models/license';
export default class LicenseController {
public async index({ auth, inertia }: HttpContext) {
const direction = 'asc'; // or 'desc'
const licenses = await License.query().orderBy('sort_order', direction).exec();
return inertia.render('Admin/License/Index', {
licenses: licenses,
can: {
edit: await auth.user?.can(['settings']),
},
});
}
public async down({ request, response }: HttpContext) {
const id = request.param('id');
const license = await License.findOrFail(id);
license.active = false;
await license.save();
// session.flash({ message: 'person has been deactivated!' });
return response.flash('License has been deactivated!', 'message').toRoute('settings.license.index')
}
public async up({ request, response }: HttpContext) {
const id = request.param('id');
const license = await License.findOrFail(id);
license.active = true;
await license.save();
// session.flash({ message: 'person has been activated!' });
return response.flash('License has been activated!', 'message').toRoute('settings.license.index');
}
// public async edit({ request, inertia }: HttpContext) {
// const id = request.param('id');
// const license = await License.query().where('id', id).firstOrFail();
// // const permissions = await Permission.query().pluck('name', 'id');
// // // const userHasRoles = user.roles;
// // const rolerHasPermissions = await role.related('permissions').query().orderBy('name').pluck('id');
// return inertia.render('Admin/License/Edit', {
// // permissions: permissions,
// license: license,
// // roleHasPermissions: Object.keys(rolerHasPermissions).map((key) => rolerHasPermissions[key]), //convert object to array with role ids
// });
// }
}

View File

@ -0,0 +1,141 @@
import type { HttpContext } from '@adonisjs/core/http';
import MimeType from '#models/mime_type';
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
export default class MimetypeController {
public async index({ auth, inertia }: HttpContext) {
const direction = 'asc'; // or 'desc'
const mimetypes = await MimeType.query().orderBy('name', direction).exec();
return inertia.render('Admin/Mimetype/Index', {
mimetypes: mimetypes,
can: {
create: await auth.user?.can(['settings']),
edit: await auth.user?.can(['settings']),
},
});
}
public async create({ inertia }: HttpContext) {
// const permissions = await Permission.query().select('id', 'name').pluck('name', 'id');
return inertia.render('Admin/Mimetype/Create', {});
}
public async store({ request, response, session }: HttpContext) {
const newDatasetSchema = vine.object({
name: vine.string().trim().isUnique({ table: 'mime_types', column: 'name' }),
file_extension: vine.array(vine.string()).minLength(1), // define at least one extension for the new mimetype
enabled: vine.boolean(),
});
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
try {
// 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);
await request.validateUsing(validator);
} catch (error) {
// Step 3 - Handle errors
// return response.badRequest(error.messages);
throw error;
}
const input = request.only(['name', 'enabled', 'file_extension']);
// Concatenate the file_extensions array into a string with '|' as the separator
if (Array.isArray(input.file_extension)) {
input.file_extension = input.file_extension.join('|');
}
await MimeType.create(input);
// if (request.input('roles')) {
// const roles: Array<number> = request.input('roles');
// await user.related('roles').attach(roles);
// }
session.flash('message', 'MimeType has been created successfully');
return response.redirect().toRoute('settings.mimetype.index');
}
public messages = {
'minLength': '{{ field }} must be at least {{ min }} characters long',
'maxLength': '{{ field }} must be less then {{ max }} characters long',
'isUnique': '{{ field }} must be unique, and this value is already taken',
'required': '{{ field }} is required',
'file_extension.minLength': 'at least {{ min }} mimetypes must be defined',
'file_extension.*.string': 'Each file extension must be a valid string', // Adjusted to match the type
};
public async edit({ request, inertia }: HttpContext) {
const id = request.param('id');
const mimetype = await MimeType.query().where('id', id).firstOrFail();
// const permissions = await Permission.query().pluck('name', 'id');
// // const userHasRoles = user.roles;
// const rolerHasPermissions = await role.related('permissions').query().orderBy('name').pluck('id');
return inertia.render('Admin/Mimetype/Edit', {
mimetype: mimetype,
});
}
// public async update({ request, response, session }: HttpContext) {
// // node ace make:validator UpdateUser
// const id = request.param('id');
// const role = await Role.query().where('id', id).firstOrFail();
// // validate update form
// // await request.validate(UpdateRoleValidator);
// await request.validateUsing(updateRoleValidator, {
// meta: {
// roleId: role.id,
// },
// });
// // password is optional
// const input = request.only(['name', 'description']);
// await role.merge(input).save();
// // await user.save();
// if (request.input('permissions')) {
// const permissions: Array<number> = request.input('permissions');
// await role.related('permissions').sync(permissions);
// }
// session.flash('message', 'Role has been updated successfully');
// return response.redirect().toRoute('settings.role.index');
// }
public async down({ request, response }: HttpContext) {
const id = request.param('id');
const mimetype = await MimeType.findOrFail(id);
mimetype.enabled = false;
await mimetype.save();
// session.flash({ message: 'person has been deactivated!' });
return response.flash('mimetype has been deactivated!', 'message').toRoute('settings.mimetype.index');
}
public async up({ request, response }: HttpContext) {
const id = request.param('id');
const mimetype = await MimeType.findOrFail(id);
mimetype.enabled = true;
await mimetype.save();
// session.flash({ message: 'person has been activated!' });
return response.flash('mimetype has been activated!', 'message').toRoute('settings.mimetype.index');
}
// public async edit({ request, inertia }: HttpContext) {
// const id = request.param('id');
// const license = await License.query().where('id', id).firstOrFail();
// // const permissions = await Permission.query().pluck('name', 'id');
// // // const userHasRoles = user.roles;
// // const rolerHasPermissions = await role.related('permissions').query().orderBy('name').pluck('id');
// return inertia.render('Admin/License/Edit', {
// // permissions: permissions,
// license: license,
// // roleHasPermissions: Object.keys(rolerHasPermissions).map((key) => rolerHasPermissions[key]), //convert object to array with role ids
// });
// }
}

View File

@ -1,14 +1,13 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
import Role from 'App/Models/Role'; import Role from '#models/role';
import Permission from 'App/Models/Permission'; import Permission from '#models/permission';
import type { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm'; import { createRoleValidator, updateRoleValidator } from '#validators/role';
import CreateRoleValidator from 'App/Validators/CreateRoleValidator'; import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
import UpdateRoleValidator from 'App/Validators/UpdateRoleValidator';
import { RenderResponse } from '@ioc:EidelLev/Inertia';
// import { schema, rules } from '@ioc:Adonis/Core/Validator'; // import { schema, rules } from '@ioc:Adonis/Core/Validator';
export default class RoleController { export default class RoleController {
public async index({ auth, request, inertia }: HttpContextContract) { public async index({ auth, request, inertia }: HttpContext) {
let roles: ModelQueryBuilderContract<typeof Role, Role> = Role.query(); let roles: ModelQueryBuilderContract<typeof Role, Role> = Role.query();
if (request.input('search')) { if (request.input('search')) {
@ -48,18 +47,19 @@ export default class RoleController {
}); });
} }
public async create({ inertia }: HttpContextContract) { public async create({ inertia }: HttpContext) {
const permissions = await Permission.query().select('id', 'name').pluck('name', 'id'); const permissions = await Permission.query().select('id', 'name').pluck('name', 'id');
return inertia.render('Admin/Role/Create', { return inertia.render('Admin/Role/Create', {
permissions: permissions, permissions: permissions,
}); });
} }
public async store({ request, response, session }: HttpContextContract) { public async store({ request, response, session }: HttpContext) {
// node ace make:validator CreateUser // node ace make:validator CreateUser
try { try {
// Step 2 - Validate request body against the schema // Step 2 - Validate request body against the schema
await request.validate(CreateRoleValidator); // await request.validate(CreateRoleValidator);
await request.validateUsing(createRoleValidator);
// await request.validate({ schema: roleSchema }); // await request.validate({ schema: roleSchema });
// console.log({ payload }); // console.log({ payload });
} catch (error) { } catch (error) {
@ -76,10 +76,10 @@ export default class RoleController {
} }
session.flash('message', `Role ${role.name} has been created successfully`); session.flash('message', `Role ${role.name} has been created successfully`);
return response.redirect().toRoute('role.index'); return response.redirect().toRoute('settings.role.index');
} }
public async show({ request, inertia }: HttpContextContract): RenderResponse { public async show({ request, inertia }: HttpContext) {
const id = request.param('id'); const id = request.param('id');
const role = await Role.query().where('id', id).firstOrFail(); const role = await Role.query().where('id', id).firstOrFail();
@ -94,7 +94,7 @@ export default class RoleController {
}); });
} }
public async edit({ request, inertia }: HttpContextContract) { public async edit({ request, inertia }: HttpContext) {
const id = request.param('id'); const id = request.param('id');
const role = await Role.query().where('id', id).firstOrFail(); const role = await Role.query().where('id', id).firstOrFail();
@ -109,13 +109,18 @@ export default class RoleController {
}); });
} }
public async update({ request, response, session }: HttpContextContract) { public async update({ request, response, session }: HttpContext) {
// node ace make:validator UpdateUser // node ace make:validator UpdateUser
const id = request.param('id'); const id = request.param('id');
const role = await Role.query().where('id', id).firstOrFail(); const role = await Role.query().where('id', id).firstOrFail();
// validate update form // validate update form
await request.validate(UpdateRoleValidator); // await request.validate(UpdateRoleValidator);
await request.validateUsing(updateRoleValidator, {
meta: {
roleId: role.id,
},
});
// password is optional // password is optional
@ -129,15 +134,15 @@ export default class RoleController {
} }
session.flash('message', 'Role has been updated successfully'); session.flash('message', 'Role has been updated successfully');
return response.redirect().toRoute('role.index'); return response.redirect().toRoute('settings.role.index');
} }
public async destroy({ request, response, session }: HttpContextContract) { public async destroy({ request, response, session }: HttpContext) {
const id = request.param('id'); const id = request.param('id');
const role = await Role.findOrFail(id); const role = await Role.findOrFail(id);
await role.delete(); await role.delete();
session.flash('message', `Role ${role.name} has been deleted.`); session.flash('message', `Role ${role.name} has been deleted.`);
return response.redirect().toRoute('role.index'); return response.redirect().toRoute('settings.role.index');
} }
} }

View File

@ -0,0 +1,103 @@
import type { HttpContext } from '@adonisjs/core/http';
import vine from '@vinejs/vine';
import AppConfig from '#models/appconfig';
import mail from '@adonisjs/mail/services/main';
// import config from '@adonisjs/core/services/config';
// import { configProvider } from '@adonisjs/core';
// import app from '@adonisjs/core/services/app';
export default class MailSettingsController {
/**
* Save the email server settings
*/
public async setMailSettings({ request, response }: HttpContext) {
const settingsSchema = vine.compile(
vine.object({
mail_domain: vine.string(),
mail_from_address: vine.string(),
mail_smtp_mode: vine.string(),
mail_smtpsecure: vine.string().optional(),
mail_smtphost: vine.string(),
mail_smtpport: vine.string(),
mail_smtpauth: vine.boolean(),
// mail_sendmailmode: vine.string().optional(),
}),
);
const validatedData = await request.validateUsing(settingsSchema);
const configData: any = { ...validatedData };
if (!validatedData.mail_smtpauth) {
configData.mail_smtpname = null;
configData.mail_smtppassword = null;
}
// Prepare the settings to be saved
const settingsToSave = [
{ appid: 'settings', configkey: 'default', configvalue: validatedData.mail_smtp_mode, type: 1, lazy: 0 },
{ appid: 'settings', configkey: 'host', configvalue: validatedData.mail_smtphost, type: 1, lazy: 0 },
{ appid: 'settings', configkey: 'port', configvalue: validatedData.mail_smtpport, type: 1, lazy: 0 },
{
appid: 'settings',
configkey: 'from.address',
configvalue: `${validatedData.mail_from_address}@${validatedData.mail_domain}`,
type: 1,
lazy: 0,
},
];
// if (validatedData.mail_smtpauth) {
// settingsToSave.push(
// { appid: 'settings', configkey: 'smtp_user', configvalue: validatedData.mail_smtpname, type: 1, lazy: 0 },
// { appid: 'settings', configkey: 'smtp_password', configvalue: validatedData.mail_smtppassword, type: 1, lazy: 0 },
// );
// } else {
// settingsToSave.push(
// { appid: 'settings', configkey: 'smtp_user', configvalue: null, type: 1, lazy: 0 },
// { appid: 'settings', configkey: 'smtp_password', configvalue: null, type: 1, lazy: 0 },
// );
// }
// Save or update the settings in the database
for (const setting of settingsToSave) {
await AppConfig.updateOrCreate(
{ appid: setting.appid, configkey: setting.configkey },
{ configvalue: setting.configvalue, type: setting.type, lazy: setting.lazy },
);
}
return response.json({ success: true, message: 'Mail settings updated successfully' });
}
/**
* Send a test email to ensure settings work
*/
public async sendTestMail({ response, auth }: HttpContext) {
const user = auth.user!;
const userEmail = user.email;
// let mailManager = await app.container.make('mail.manager');
// let iwas = mailManager.use();
// let test = mail.config.mailers.smtp();
if (!userEmail) {
return response.badRequest({ message: 'User email is not set. Please update your profile.' });
}
try {
await mail.send((message) => {
message
// .from(Config.get('mail.from.address'))
.from('tethys@geosphere.at')
.to(userEmail)
.subject('Test Email')
.html('<p>If you received this email, the email configuration seems to be correct.</p>');
});
return response.json({ success: true, message: 'Test email sent successfully' });
// return response.flash('Test email sent successfully!', 'message').redirect().back();
} catch (error) {
return response.internalServerError({ message: `Error sending test email: ${error.message}` });
}
}
}

View File

@ -1,10 +1,10 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
import Person from 'App/Models/Person'; import Person from '#models/person';
// import Dataset from 'App/Models/Dataset'; // import Dataset from 'App/Models/Dataset';
// node ace make:controller Author // node ace make:controller Author
export default class AuthorsController { export default class AuthorsController {
public async index({}: HttpContextContract) { public async index({}: HttpContext) {
// select * from gba.persons // select * from gba.persons
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id" // where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id")); // where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
@ -19,7 +19,7 @@ export default class AuthorsController {
return authors; return authors;
} }
public async persons({ request }: HttpContextContract) { public async persons({ request }: HttpContext) {
const authors = Person.query().where('status', true); const authors = Person.query().where('status', true);
if (request.input('filter')) { if (request.input('filter')) {

View File

@ -0,0 +1,65 @@
import type { HttpContext } from '@adonisjs/core/http';
import { StatusCodes } from 'http-status-codes';
// import * as fs from 'fs';
// import * as path from 'path';
const prefixes = ['von', 'van'];
// node ace make:controller Author
export default class AvatarController {
public async generateAvatar({ request, response }: HttpContext) {
try {
const { name, background, textColor, size } = request.only(['name', 'background', 'textColor', 'size']);
// Generate initials
// const initials = name
// .split(' ')
// .map((part) => part.charAt(0).toUpperCase())
// .join('');
const initials = this.getInitials(name);
// Define SVG content with dynamic values for initials, background color, text color, and size
const svgContent = `
<svg width="${size || 50}" height="${size || 50}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#${background || '7F9CF5'}"/>
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${
(size / 100) * 40 || 25
}" fill="#${textColor || 'ffffff'}">${initials}</text>
</svg>
`;
// Set response headers for SVG content
response.header('Content-type', 'image/svg+xml');
response.header('Cache-Control', 'no-cache');
response.header('Pragma', 'no-cache');
response.header('Expires', '0');
return response.send(svgContent);
} catch (error) {
return response.status(StatusCodes.OK).json({ error: error.message });
}
}
private getInitials(name: string) {
const parts = name.split(' ');
let initials = '';
if (parts.length >= 2) {
const firstName = parts[0];
const lastName = parts[parts.length - 1];
const firstInitial = firstName.charAt(0).toUpperCase();
const lastInitial = lastName.charAt(0).toUpperCase();
if (prefixes.includes(lastName.toLowerCase()) && lastName === lastName.toUpperCase()) {
initials = firstInitial + lastName.charAt(1).toUpperCase();
} else {
initials = firstInitial + lastInitial;
}
} else if (parts.length === 1) {
initials = parts[0].substring(0, 2).toUpperCase();
}
return initials;
}
}

View File

@ -1,11 +1,11 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
// import Person from 'App/Models/Person'; // import Person from 'App/Models/Person';
import Dataset from 'App/Models/Dataset'; import Dataset from '#models/dataset';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
// node ace make:controller Author // node ace make:controller Author
export default class DatasetController { export default class DatasetController {
public async index({}: HttpContextContract) { public async index({}: HttpContext) {
// select * from gba.persons // select * from gba.persons
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id" // where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id")); // where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
@ -14,7 +14,7 @@ export default class DatasetController {
return datasets; return datasets;
} }
public async findAll({ response }: HttpContextContract) { public async findAll({ response }: HttpContext) {
try { try {
const datasets = await Dataset.query() const datasets = await Dataset.query()
.where('server_state', 'published') .where('server_state', 'published')
@ -29,7 +29,7 @@ export default class DatasetController {
} }
} }
public async findOne({ params }: HttpContextContract) { public async findOne({ params }: HttpContext) {
const datasets = await Dataset.query() const datasets = await Dataset.query()
.where('publish_id', params.publish_id) .where('publish_id', params.publish_id)
.preload('titles') .preload('titles')

View File

@ -0,0 +1,54 @@
import type { HttpContext } from '@adonisjs/core/http';
import File from '#models/file';
import { StatusCodes } from 'http-status-codes';
import * as fs from 'fs';
import * as path from 'path';
// node ace make:controller Author
export default class FileController {
// @Get("download/:id")
public async findOne({ response, params }: HttpContext) {
const id = params.id;
const file = await File.findOrFail(id);
// const file = await File.findOne({
// where: { id: id },
// });
if (file) {
const filePath = '/storage/app/public/' + file.pathName;
const ext = path.extname(filePath);
const fileName = file.label + ext;
try {
fs.accessSync(filePath, fs.constants.R_OK); //| fs.constants.W_OK);
// console.log("can read/write:", path);
response
.header('Cache-Control', 'no-cache private')
.header('Content-Description', 'File Transfer')
.header('Content-Type', file.mimeType)
.header('Content-Disposition', 'inline; filename=' + fileName)
.header('Content-Transfer-Encoding', 'binary')
.header('Access-Control-Allow-Origin', '*')
.header('Access-Control-Allow-Methods', 'GET,POST');
response.status(StatusCodes.OK).download(filePath);
} catch (err) {
// console.log("no access:", path);
response.status(StatusCodes.NOT_FOUND).send({
message: `File with id ${id} doesn't exist on file server`,
});
}
// res.status(StatusCodes.OK).sendFile(filePath, (err) => {
// // res.setHeader("Content-Type", "application/json");
// // res.removeHeader("Content-Disposition");
// res.status(StatusCodes.NOT_FOUND).send({
// message: `File with id ${id} doesn't exist on file server`,
// });
// });
} else {
response.status(StatusCodes.NOT_FOUND).send({
message: `Cannot find File with id=${id}.`,
});
}
}
}

View File

@ -1,9 +1,9 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
import Database from '@ioc:Adonis/Lucid/Database'; import db from '@adonisjs/lucid/services/db';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
export default class HomeController { export default class HomeController {
public async findDocumentsPerYear({ response, params }: HttpContextContract) { public async findDocumentsPerYear({ response, params }: HttpContext) {
const year = params.year; const year = params.year;
const from = parseInt(year); const from = parseInt(year);
const serverState = 'published'; const serverState = 'published';
@ -17,8 +17,8 @@ export default class HomeController {
// .preload('authors') // .preload('authors')
// .orderBy('server_date_published'); // .orderBy('server_date_published');
const datasets = await Database.from('documents as doc') const datasets = await db.from('documents as doc')
.select(['publish_id', 'server_date_published', Database.raw(`date_part('year', server_date_published) as pub_year`)]) .select(['publish_id', 'server_date_published', db.raw(`date_part('year', server_date_published) as pub_year`)])
.where('server_state', serverState) .where('server_state', serverState)
.innerJoin('link_documents_persons as ba', 'doc.id', 'ba.document_id') .innerJoin('link_documents_persons as ba', 'doc.id', 'ba.document_id')
.andWhereRaw(`date_part('year', server_date_published) = ?`, [from]) .andWhereRaw(`date_part('year', server_date_published) = ?`, [from])
@ -32,17 +32,17 @@ export default class HomeController {
} }
} }
public async findYears({ response }: HttpContextContract) { public async findYears({ response }: HttpContext) {
const serverState = 'published'; const serverState = 'published';
// Use raw SQL queries to select all cars which belongs to the user // Use raw SQL queries to select all cars which belongs to the user
try { try {
const datasets = await Database.rawQuery( const datasets = await db.rawQuery(
'SELECT distinct EXTRACT(YEAR FROM server_date_published) as published_date FROM gba.documents WHERE server_state = ?', 'SELECT distinct EXTRACT(YEAR FROM server_date_published) as published_date FROM gba.documents WHERE server_state = ?',
[serverState], [serverState],
); );
// Pluck the ids of the cars // Pluck the ids of the cars
const years = datasets.rows.map((dataset) => dataset.published_date); const years = datasets.rows.map((dataset: any) => dataset.published_date);
// check if the cars is returned // check if the cars is returned
// if (years.length > 0) { // if (years.length > 0) {
return response.status(StatusCodes.OK).json(years); return response.status(StatusCodes.OK).json(years);
@ -53,4 +53,91 @@ export default class HomeController {
}); });
} }
} }
public async findPublicationsPerMonth({ response }: HttpContext) {
const serverState = 'published';
// const year = params.year;
// const from = parseInt(year);
try {
// const datasets = await Database.from('documents as doc')
// .select([Database.raw(`date_part('month', server_date_published) as pub_month`), Database.raw('COUNT(*) as count')])
// .where('server_state', serverState)
// .innerJoin('link_documents_persons as ba', 'doc.id', 'ba.document_id')
// .andWhereRaw(`date_part('year', server_date_published) = ?`, [from])
// .groupBy('pub_month');
// // .orderBy('server_date_published');
const years = [2021, 2022, 2023]; // Add the second year
const result = await db.from('documents as doc')
.select([
db.raw(`date_part('year', server_date_published) as pub_year`),
db.raw(`date_part('month', server_date_published) as pub_month`),
db.raw('COUNT(*) as count'),
])
.where('server_state', serverState)
// .innerJoin('link_documents_persons as ba', 'doc.id', 'ba.document_id')
// .whereIn('pub_year', years) // Filter by both years
.whereRaw(`date_part('year', server_date_published) IN (${years.join(',')})`) // Filter by both years
.groupBy('pub_year', 'pub_month')
.orderBy('pub_year', 'asc')
.orderBy('pub_month', 'asc');
const labels = Array.from({ length: 12 }, (_, i) => i + 1); // Assuming 12 months
const inputDatasets: Map<string, ChartDataset> = result.reduce((acc, item) => {
const { pub_year, pub_month, count } = item;
if (!acc[pub_year]) {
acc[pub_year] = {
data: Array.from({ length: 12 }).fill(0),
label: pub_year.toString(),
borderColor: this.getRandomHexColor, // pub_year === 2022 ? '#3e95cd' : '#8e5ea2',
fill: false,
};
}
acc[pub_year].data[pub_month - 1] = parseInt(count);
return acc ;
}, {});
const outputDatasets = Object.entries(inputDatasets).map(([year, data]) => ({
data: data.data,
label: year,
borderColor: data.borderColor,
fill: data.fill
}));
const data = {
labels: labels,
datasets: outputDatasets,
};
return response.json(data);
} catch (error) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: error.message || 'Some error occurred while retrieving datasets.',
});
}
}
private getRandomHexColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
}
interface ChartDataset {
data: Array<number>;
label: string;
borderColor: string;
fill: boolean;
} }

View File

@ -0,0 +1,114 @@
import type { HttpContext } from '@adonisjs/core/http';
import User from '#models/user';
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
import { StatusCodes } from 'http-status-codes';
import { InvalidArgumentException } from 'node-exceptions';
import { TotpState } from '#contracts/enums';
import BackupCodeStorage, { SecureRandom } from '#services/backup_code_storage';
import BackupCode from '#models/backup_code';
// Here we are generating secret and recovery codes for the user thats enabling 2FA and storing them to our database.
export default class UserController {
public async enable({ auth, response, request }: HttpContext) {
const user = (await User.find(auth.user?.id)) as User;
// await user.load('totp_secret');
// if (!user.totp_secret) {
// let totpSecret = new TotpSecret();
// user.related('totp_secret').save(totpSecret);
// await user.load('totp_secret');
// }
if (!user) {
throw new Error('user not available');
}
const state: number = request.input('state');
try {
switch (state) {
case TotpState.STATE_DISABLED:
// user.twoFactorSecret = null;
// user.twoFactorRecoveryCodes = null;
await BackupCode.deleteCodes(user);
user.twoFactorSecret = '';
// user.twoFactorRecoveryCodes = [''];
await user.save();
user.state = TotpState.STATE_DISABLED;
await user.save();
let storage = new BackupCodeStorage(new SecureRandom());
let backupState = await storage.getBackupCodesState(user);
return response.status(StatusCodes.OK).json({
state: TotpState.STATE_DISABLED,
backupState: backupState,
});
case TotpState.STATE_CREATED:
user.twoFactorSecret = TwoFactorAuthProvider.generateSecret(user);
user.state = TotpState.STATE_CREATED;
await user.save();
let qrcode = await TwoFactorAuthProvider.generateQrCode(user);
// throw new InvalidArgumentException('code is missing');
return response.status(StatusCodes.OK).json({
state: user.state,
secret: user.twoFactorSecret,
url: qrcode.url,
svg: qrcode.svg,
});
case TotpState.STATE_ENABLED:
let code: string = request.input('code');
if (!code) {
throw new InvalidArgumentException('code is missing');
}
const success = await TwoFactorAuthProvider.enable(user, code);
return response.status(StatusCodes.OK).json({
state: success ? TotpState.STATE_ENABLED : TotpState.STATE_CREATED,
});
default:
throw new InvalidArgumentException('Invalid TOTP state');
}
} catch (error) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: 'Invalid TOTP state',
});
}
}
// public async fetchRecoveryCodes({ auth, view }) {
// const user = auth?.user;
// return view.render('pages/settings', {
// twoFactorEnabled: user.isTwoFactorEnabled,
// recoveryCodes: user.twoFactorRecoveryCodes,
// });
// }
/**
* @NoAdminRequired
* @PasswordConfirmationRequired
*
* @return JSONResponse
*/
public async createCodes({ auth, response }: HttpContext) {
// $user = $this->userSession->getUser();
const user = (await User.find(auth.user?.id)) as User;
// let codes = TwoFactorAuthProvider.generateRecoveryCodes();
let storage = new BackupCodeStorage(new SecureRandom());
// $codes = $this->storage->createCodes($user);
const codes = await storage.createCodes(user);
let backupState = await storage.getBackupCodesState(user);
// return new JSONResponse([
// 'codes' => $codes,
// 'state' => $this->storage->getBackupCodesState($user),
// ]);
return response.status(StatusCodes.OK).json({
codes: codes,
// state: success ? TotpState.STATE_ENABLED : TotpState.STATE_CREATED,
backupState: backupState, //storage.getBackupCodesState(user),
});
}
}

View File

@ -1,25 +1,55 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
// import User from 'App/Models/User'; import User from '#models/user';
import BackupCode from '#models/backup_code';
// import Hash from '@ioc:Adonis/Core/Hash'; // import Hash from '@ioc:Adonis/Core/Hash';
// import InvalidCredentialException from 'App/Exceptions/InvalidCredentialException'; // import InvalidCredentialException from 'App/Exceptions/InvalidCredentialException';
import AuthValidator from 'App/Validators/AuthValidator'; import { authValidator } from '#validators/auth';
import hash from '@adonisjs/core/services/hash';
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
// import { Authenticator } from '@adonisjs/auth';
// import { LoginState } from 'Contracts/enums';
// import { StatusCodes } from 'http-status-codes';
// interface MyHttpsContext extends HttpContext {
// auth: Authenticator<User>
// }
export default class AuthController { export default class AuthController {
// login function // login function{ request, auth, response }:HttpContext
public async login({ request, response, auth, session }: HttpContextContract) { public async login({ request, response, auth, session }: HttpContext) {
// console.log({ // console.log({
// registerBody: request.body(), // registerBody: request.body(),
// }); // });
await request.validate(AuthValidator); // await request.validate(AuthValidator);
await request.validateUsing(authValidator);
const plainPassword = await request.input('password'); // const plainPassword = await request.input('password');
const email = await request.input('email'); // const email = await request.input('email');
// grab uid and password values off request body // grab uid and password values off request body
// const { email, password } = request.only(['email', 'password']) const { email, password } = request.only(['email', 'password']);
try { try {
// attempt to login // // attempt to verify credential and login user
await auth.use('web').attempt(email, plainPassword); // 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) {
// 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);
} catch (error) { } catch (error) {
// if login fails, return vague form message and redirect back // if login fails, return vague form message and redirect back
session.flash('message', 'Your username, email, or password is incorrect'); session.flash('message', 'Your username, email, or password is incorrect');
@ -27,14 +57,68 @@ export default class AuthController {
} }
// otherwise, redirect todashboard // otherwise, redirect todashboard
response.redirect('/dashboard'); 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 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);
response.redirect('/apps/dashboard');
} else {
session.flash('message', 'Your two-factor code is incorrect');
return response.redirect().back();
}
} 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 = null;
for (const backupCode of codes) {
const isVerified = await hash.verify(backupCode.code, backup_code);
if (isVerified) {
backupCodeToDelete = backupCode;
break;
}
}
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();
}
}
} }
// logout function // logout function
public async logout({ auth, response }: HttpContextContract) { public async logout({ auth, response }: HttpContext) {
// await auth.logout(); // await auth.logout();
await auth.use('web').logout(); await auth.use('web').logout();
response.redirect('/app/login'); return response.redirect('/app/login');
// return response.status(200); // return response.status(200);
} }
} }

View File

@ -0,0 +1,138 @@
import type { HttpContext } from '@adonisjs/core/http';
import User from '#models/user';
// import { RenderResponse } from '@ioc:EidelLev/Inertia';
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
import hash from '@adonisjs/core/services/hash';
// import { schema, rules } from '@adonisjs/validator';
import vine from '@vinejs/vine';
import BackupCodeStorage, { SecureRandom } from '#services/backup_code_storage';
// Here we are generating secret and recovery codes for the user thats enabling 2FA and storing them to our database.
export default class UserController {
/**
* Show the user a form to change their personal information & password.
*
* @return \Inertia\Response
*/
public async accountInfo({ inertia, auth }: HttpContext) {
// const user = auth.user;
const user = (await User.find(auth.user?.id)) as User;
// const id = request.param('id');
// const user = await User.query().where('id', id).firstOrFail();
let storage = new BackupCodeStorage(new SecureRandom());
// const codes= user.isTwoFactorEnabled? (await user.getBackupCodes()).map((role) => role.code) : [];
let backupState = await storage.getBackupCodesState(user);
return inertia.render('Auth/AccountInfo', {
user: user,
twoFactorEnabled: user.isTwoFactorEnabled,
// code: await TwoFactorAuthProvider.generateQrCode(user),
backupState: backupState,
});
}
public async accountInfoStore({ auth, request, response, session }: HttpContext) {
// const passwordSchema = schema.create({
// old_password: schema.string({ trim: true }, [rules.required()]),
// new_password: schema.string({ trim: true }, [rules.minLength(8), rules.maxLength(255), rules.confirmed('confirm_password')]),
// confirm_password: schema.string({ trim: true }, [rules.required()]),
// });
const passwordSchema = vine.object({
// first step
old_password: vine
.string()
.trim()
.regex(/^[a-zA-Z0-9]+$/),
new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255),
});
try {
// await request.validate({ schema: passwordSchema });
const validator = vine.compile(passwordSchema);
await request.validateUsing(validator);
} catch (error) {
// return response.badRequest(error.messages);
throw error;
}
try {
const user = await auth.user as User;
const { old_password, new_password } = request.only(['old_password', 'new_password']);
// if (!(old_password && new_password && confirm_password)) {
// return response.status(400).send({ warning: 'Old password and new password are required.' });
// }
// Verify if the provided old password matches the user's current password
const isSame = await hash.verify(user.password, old_password);
if (!isSame) {
return response.flash('warning', 'Old password is incorrect.').redirect().back();
}
// Hash the new password before updating the user's password
user.password = new_password;
await user.save();
// return response.status(200).send({ message: 'Password updated successfully.' });
session.flash({ message: 'Password updated successfully.' });
return response.redirect().toRoute('settings.user');
} catch (error) {
// return response.status(500).send({ message: 'Internal server error.' });
return response.flash('warning', `Invalid server state. Internal server error.`).redirect().back();
}
}
public async enableTwoFactorAuthentication({ auth, response, session }: HttpContext): Promise<void> {
// const user: User | undefined = auth?.user;
const user = (await User.find(auth.user?.id)) as User;
user.twoFactorSecret = TwoFactorAuthProvider.generateSecret(user);
user.twoFactorRecoveryCodes = await TwoFactorAuthProvider.generateRecoveryCodes();
await user.save();
session.flash('message', 'Two factor authentication enabled.');
return response.redirect().back();
// return inertia.render('Auth/AccountInfo', {
// // status: {
// // type: 'success',
// // message: 'Two factor authentication enabled.',
// // },
// user: user,
// twoFactorEnabled: user.isTwoFactorEnabled,
// code: await TwoFactorAuthProvider.generateQrCode(user),
// recoveryCodes: user.twoFactorRecoveryCodes,
// });
}
public async disableTwoFactorAuthentication({ auth, response, session }: HttpContext): Promise<void> {
const user: User | undefined = auth.user;
if (user) {
user.twoFactorSecret = null;
user.twoFactorRecoveryCodes = null;
await user.save();
session.flash('message', 'Two-factor authentication disabled.');
} else {
session.flash('error', 'User not found.');
}
return response.redirect().back();
// return inertia.render('Auth/AccountInfo', {
// // status: {
// // type: 'success',
// // message: 'Two factor authentication disabled.',
// // },
// user: user,
// twoFactorEnabled: user.isTwoFactorEnabled,
// });
}
// public async fetchRecoveryCodes({ auth, view }) {
// const user = auth?.user;
// return view.render('pages/settings', {
// twoFactorEnabled: user.isTwoFactorEnabled,
// recoveryCodes: user.twoFactorRecoveryCodes,
// });
// }
}

View File

@ -0,0 +1,682 @@
import type { HttpContext } from '@adonisjs/core/http';
import { Client } from '@opensearch-project/opensearch';
import User from '#models/user';
import Dataset from '#models/dataset';
import DatasetIdentifier from '#models/dataset_identifier';
import XmlModel from '#app/Library/XmlModel';
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
import { create } from 'xmlbuilder2';
import { readFileSync } from 'fs';
import SaxonJS from 'saxon-js';
import { DateTime } from 'luxon';
import Index from '#app/Library/Utils/Index';
import { getDomain } from '#app/utils/utility-functions';
import { DoiClient } from '#app/Library/Doi/DoiClient';
import DoiClientException from '#app/exceptions/DoiClientException';
import logger from '@adonisjs/core/services/logger';
import { HttpException } from 'node-exceptions';
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
import mail from '@adonisjs/mail/services/main';
// import { resolveMx } from 'dns/promises';
// import * as net from 'net';
import { validate } from 'deep-email-validator';
// Create a new instance of the client
const client = new Client({ node: 'http://localhost:9200' }); // replace with your OpenSearch endpoint
export default class DatasetsController {
private proc;
public messages = {
// 'required': '{{ field }} is required',
// 'licenses.minLength': 'at least {{ options.minLength }} permission must be defined',
'reviewer_id.required': 'reviewer_id must be defined',
'publisher_name.required': 'publisher name must be defined',
};
constructor() {
this.proc = readFileSync('public/assets2/solr.sef.json');
// Load the XSLT file
// this.proc = readFileSync('public/assets2/datasetxml2oai.sef.json');
}
// public async index({}: HttpContextContract) {}
public async index({ auth, request, inertia }: HttpContext) {
const user = (await User.find(auth.user?.id)) as User;
const page = request.input('page', 1);
let datasets: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query();
// if (request.input('search')) {
// // users = users.whereRaw('name like %?%', [request.input('search')])
// const searchTerm = request.input('search');
// datasets.where('name', 'ilike', `%${searchTerm}%`);
// }
if (request.input('sort')) {
type SortOrder = 'asc' | 'desc' | undefined;
let attribute = request.input('sort');
let sortOrder: SortOrder = 'asc';
if (attribute.substr(0, 1) === '-') {
sortOrder = 'desc';
// attribute = substr(attribute, 1);
attribute = attribute.substr(1);
}
datasets.orderBy(attribute, sortOrder);
} else {
// users.orderBy('created_at', 'desc');
datasets.orderBy('id', 'asc');
}
// const users = await User.query().orderBy('login').paginate(page, limit);
const myDatasets = await datasets
.where('server_state', 'released')
.orWhere((dQuery) => {
dQuery
.whereIn('server_state', ['editor_accepted', 'rejected_reviewer', 'reviewed', 'published'])
.where('editor_id', user.id)
.doesntHave('identifier', 'and');
})
// .preload('identifier')
.preload('titles')
.preload('user', (query) => query.select('id', 'login'))
.preload('editor', (query) => query.select('id', 'login'))
.paginate(page, 10);
return inertia.render('Editor/Dataset/Index', {
datasets: myDatasets.serialize(),
filters: request.all(),
can: {
receive: await auth.user?.can(['dataset-receive']),
approve: await auth.user?.can(['dataset-approve']),
reject: await auth.user?.can(['dataset-editor-reject']),
edit: await auth.user?.can(['dataset-editor-update']),
delete: await auth.user?.can(['dataset-editor-delete']),
publish: await auth.user?.can(['dataset-publish']),
},
});
}
public async receive({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
.preload('titles')
.preload('descriptions')
.preload('user', (builder) => {
builder.select('id', 'login');
})
.firstOrFail();
const validStates = ['released'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be received. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.back();
}
return inertia.render('Editor/Dataset/Receive', {
dataset,
});
}
public async receiveUpdate({ auth, request, response }: HttpContext) {
const id = request.param('id');
// const { id } = params;
const dataset = await Dataset.findOrFail(id);
const validStates = ['released'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be received by editor. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('editor.dataset.list');
}
dataset.server_state = 'editor_accepted';
const user = (await User.find(auth.user?.id)) as User;
// dataset.editor().associate(user).save();
try {
await dataset.related('editor').associate(user); // speichert schon ab
// await dataset.save();
return response.toRoute('editor.dataset.list').flash(`You have accepted dataset ${dataset.id}!`, 'message');
} catch (error) {
// Handle any errors
console.error(error);
return response.status(500).json({ error: 'An error occurred while accepting the data.' });
}
}
public async approve({ request, inertia, response }: HttpContext) {
const id = request.param('id');
// $dataset = Dataset::with('user:id,login')->findOrFail($id);
const dataset = await Dataset.findOrFail(id);
const validStates = ['editor_accepted', 'rejected_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be approved. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.back();
}
const reviewers = await User.query()
.whereHas('roles', (builder) => {
builder.where('name', 'reviewer');
})
.pluck('login', 'id');
return inertia.render('Editor/Dataset/Approve', {
dataset,
reviewers,
});
}
public async approveUpdate({ request, response }: HttpContext) {
const approveDatasetSchema = vine.object({
reviewer_id: vine.number(),
});
try {
// await request.validate({ schema: approveDatasetSchema, messages: this.messages });
const validator = vine.compile(approveDatasetSchema);
await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) });
} catch (error) {
// return response.badRequest(error.messages);
throw error;
}
const id = request.param('id');
const dataset = await Dataset.findOrFail(id);
const validStates = ['editor_accepted', 'rejected_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be approved. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.back();
}
dataset.server_state = 'approved';
if (dataset.reject_reviewer_note != null) {
dataset.reject_reviewer_note = null;
}
//save main and additional titles
const reviewer_id = request.input('reviewer_id', null);
dataset.reviewer_id = reviewer_id;
if (await dataset.save()) {
return response.toRoute('editor.dataset.list').flash('message', 'You have approved one dataset!');
}
}
public async reject({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
// .preload('titles')
// .preload('descriptions')
.preload('user', (builder) => {
builder.select('id', 'login', 'email');
})
.firstOrFail();
const validStates = ['editor_accepted', 'rejected_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('editor.dataset.list');
}
return inertia.render('Editor/Dataset/Reject', {
dataset,
});
}
// private async checkEmailDomain(email: string): Promise<boolean> {
// const domain = email.split('@')[1];
// try {
// // Step 1: Check MX records for the domain
// const mxRecords = await resolveMx(domain);
// if (mxRecords.length === 0) {
// return false; // No MX records, can't send email
// }
// // Sort MX records by priority
// mxRecords.sort((a, b) => a.priority - b.priority);
// // Step 2: Attempt SMTP connection to the first available mail server
// const smtpServer = mxRecords[0].exchange;
// return await this.checkMailboxExists(smtpServer, email);
// } catch (error) {
// console.error('Error during MX lookup or SMTP validation:', error);
// return false;
// }
// }
//// Helper function to check if the mailbox exists using SMTP
// private async checkMailboxExists(smtpServer: string, email: string): Promise<boolean> {
// return new Promise((resolve, reject) => {
// const socket = net.createConnection(25, smtpServer);
// socket.on('connect', () => {
// socket.write(`HELO ${smtpServer}\r\n`);
// socket.write(`MAIL FROM: <test@example.com>\r\n`);
// socket.write(`RCPT TO: <${email}>\r\n`);
// });
// socket.on('data', (data) => {
// const response = data.toString();
// if (response.includes('250')) {
// // 250 is an SMTP success code
// socket.end();
// resolve(true); // Email exists
// } else if (response.includes('550')) {
// // 550 means the mailbox doesn't exist
// socket.end();
// resolve(false); // Email doesn't exist
// }
// });
// socket.on('error', (error) => {
// console.error('SMTP connection error:', error);
// socket.end();
// resolve(false);
// });
// socket.on('end', () => {
// // SMTP connection closed
// });
// socket.setTimeout(5000, () => {
// // Timeout after 5 seconds
// socket.end();
// resolve(false); // Assume email doesn't exist if no response
// });
// });
// }
public async rejectUpdate({ request, response, auth }: HttpContext) {
const authUser = auth.user!;
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
.preload('user', (builder) => {
builder.select('id', 'login', 'email');
})
.firstOrFail();
const newSchema = vine.object({
server_state: vine.string().trim(),
reject_editor_note: vine.string().trim().minLength(10).maxLength(500),
send_mail: vine.boolean().optional(),
});
try {
// await request.validate({ schema: newSchema });
const validator = vine.compile(newSchema);
await request.validateUsing(validator);
} catch (error) {
// return response.badRequest(error.messages);
throw error;
}
const validStates = ['editor_accepted', 'rejected_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
return response
.flash(
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
'warning'
)
.redirect()
.toRoute('editor.dataset.list');
}
dataset.server_state = 'rejected_editor';
const rejectEditorNote = request.input('reject_editor_note', '');
dataset.reject_editor_note = rejectEditorNote;
// add logic for sending reject message
const sendMail = request.input('send_email', false);
// const validRecipientEmail = await this.checkEmailDomain('arno.kaimbacher@outlook.at');
const validationResult = await validate({
email: dataset.user.email,
validateSMTP: false,
});
const validRecipientEmail: boolean = validationResult.valid;
let emailStatusMessage = '';
if (sendMail == true) {
if (dataset.user.email && validRecipientEmail) {
try {
await mail.send((message) => {
message.to(dataset.user.email).subject('Dataset Rejection Notification').html(`
<p>Dear ${dataset.user.login},</p>
<p>Your dataset with ID ${dataset.id} has been rejected.</p>
<p>Reason for rejection: ${rejectEditorNote}</p>
<p>Best regards,<br>Your Tethys editor: ${authUser.login}</p>
`);
});
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.user.email}.`;
} catch (error) {
logger.error(error);
return response.flash('Dataset has not been rejected due to an email error: ' + error.message, 'error').toRoute('editor.dataset.list');
}
} else {
emailStatusMessage = ` However, the email could not be sent because the submitter's email address (${dataset.user.email}) is not valid.`;
}
}
await dataset.save();
return response
.flash(
`You have successfully rejected dataset ${dataset.id} submitted by ${dataset.user.login}.${emailStatusMessage}`,
'message',
)
.toRoute('editor.dataset.list');
}
public async publish({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
.preload('titles')
.preload('authors')
// .preload('persons', (builder) => {
// builder.wherePivot('role', 'author')
// })
.firstOrFail();
const validStates = ['reviewed'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be published. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.back();
}
return inertia.render('Editor/Dataset/Publish', {
dataset,
});
}
public async publishUpdate({ request, response }: HttpContext) {
const publishDatasetSchema = vine.object({
publisher_name: vine.string().trim(),
});
try {
// await request.validate({ schema: publishDatasetSchema, messages: this.messages });
const validator = vine.compile(publishDatasetSchema);
await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) });
} catch (error) {
throw error;
}
const id = request.param('id');
const dataset = await Dataset.findOrFail(id);
// let test = await Dataset.getMax('publish_id');
// const maxPublishId = await Database.from('documents').max('publish_id as max_publish_id').first();
// const max = maxPublishId.max_publish_id;
const max = await Dataset.getMax('publish_id');
let publish_id = 0;
if (max != null) {
publish_id = max + 1;
} else {
publish_id = publish_id + 1;
}
dataset.publish_id = publish_id;
dataset.server_state = 'published';
dataset.server_date_published = DateTime.now();
const publisherName = request.input('publisher_name', 'Tethys');
dataset.publisher_name = publisherName;
if (await dataset.save()) {
const index_name = 'tethys-records';
await Index.indexDocument(dataset, index_name);
return response.toRoute('editor.dataset.list').flash('message', 'You have successfully published the dataset!');
}
}
public async doiCreate({ request, inertia }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
.preload('titles')
.preload('descriptions')
// .preload('identifier')
.preload('authors')
.firstOrFail();
return inertia.render('Editor/Dataset/Doi', {
dataset,
});
}
public async doiStore({ request, response }: HttpContext) {
const dataId = request.param('publish_id');
const dataset = await Dataset.query()
// .preload('xmlCache')
.where('publish_id', dataId)
.firstOrFail();
const xmlMeta = (await Index.getDoiRegisterString(dataset)) as string;
let prefix = '';
let base_domain = '';
// const datacite_environment = process.env.DATACITE_ENVIRONMENT || 'debug';
prefix = process.env.DATACITE_PREFIX || '';
base_domain = process.env.BASE_DOMAIN || '';
// register DOI:
const doiValue = prefix + '/tethys.' + dataset.publish_id; //'10.21388/tethys.213'
const landingPageUrl = 'https://doi.' + getDomain(base_domain) + '/' + prefix + '/tethys.' + dataset.publish_id; //https://doi.dev.tethys.at/10.21388/tethys.213
const doiClient = new DoiClient();
const dataciteResponse = await doiClient.registerDoi(doiValue, xmlMeta, landingPageUrl);
if (dataciteResponse?.status === 201) {
// if response OK 201; save the Identifier value into db
const doiIdentifier = new DatasetIdentifier();
doiIdentifier.value = doiValue;
doiIdentifier.dataset_id = dataset.id;
doiIdentifier.type = 'doi';
doiIdentifier.status = 'findable';
// save modified date of datset for re-caching model in db an update the search index
dataset.server_date_modified = DateTime.now();
// save updated dataset to db an index to OpenSearch
try {
await dataset.related('identifier').save(doiIdentifier);
const index_name = 'tethys-records';
await Index.indexDocument(dataset, index_name);
} catch (error) {
logger.error(`${__filename}: Indexing document ${dataset.id} failed: ${error.message}`);
// Log the error or handle it as needed
throw new HttpException(error.message);
}
return response.toRoute('editor.dataset.list').flash('message', 'You have successfully created a DOI for the dataset!');
} else {
const message = `Unexpected DataCite MDS response code ${dataciteResponse?.status}`;
// Log the error or handle it as needed
throw new DoiClientException(dataciteResponse?.status, message);
}
// return response.toRoute('editor.dataset.list').flash('message', xmlMeta);
}
public async show({}: HttpContext) {}
public async edit({}: HttpContext) {}
// public async update({}: HttpContextContract) {}
public async update({ response }: HttpContext) {
const id = 273; //request.param('id');
const dataset = await Dataset.query().preload('xmlCache').where('id', id).firstOrFail();
// add xml elements
let xml = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');
const datasetNode = xml.root().ele('Dataset');
await this.createXmlRecord(dataset, datasetNode);
// const domNode = await this.getDatasetXmlDomNode(dataset);
// const xmlString = xml.end({ prettyPrint: true });
// const data = request.only(['field1', 'field2']); // get it from xslt
// Create an index with non-default settings.
var index_name = 'tethys-features';
const xmlString = xml.end({ prettyPrint: false });
let doc = '';
try {
const result = await SaxonJS.transform({
// stylesheetFileName: `${config.TMP_BASE_DIR}/data-quality/rules/iati.sef.json`,
stylesheetText: this.proc,
destination: 'serialized',
// sourceFileName: sourceFile,
sourceText: xmlString,
// stylesheetParams: xsltParameter,
// logLevel: 10,
});
doc = result.principalResult;
} catch (error) {
return response.status(500).json({
message: 'An error occurred while creating the user',
error: error.message,
});
}
// var settings = {
// settings: {
// index: {
// number_of_shards: 4,
// number_of_replicas: 3,
// },
// },
// };
// var test = await client.indices.create({
// index: index_name,
// body: settings,
// });
// var document = {
// title: 'Sample Document',
// authors: [
// {
// first_name: 'John',
// last_name: 'Doe',
// },
// {
// first_name: 'Jane',
// last_name: 'Smith',
// },
// ],
// year: '2018',
// genre: 'Crime fiction',
// };
// http://localhost:9200/datastets/_doc/1
// var id = '1';
try {
// console.log(doc);
let document = JSON.parse(`${doc}`);
// https://opensearch.org/docs/2.1/opensearch/supported-field-types/geo-shape/
// Define the new document
// const document = {
// title: 'Your Document Name',
// id: dataset.publish_id,
// doctype: 'GIS',
// // "location" : {
// // "type" : "point",
// // "coordinates" : [74.00, 40.71]
// // },
// geo_location: {
// type: 'linestring',
// coordinates: [
// [-77.03653, 38.897676],
// [-77.009051, 38.889939],
// ],
// },
// // geo_location: 'BBOX (71.0589, 74.0060, 42.3601, 40.7128)'
// // geo_location: {
// // type: 'envelope',
// // coordinates: [
// // [13.0, 53.0],
// // [14.0, 52.0],
// // ], // Define your BBOX coordinates
// // },
// };
// Update the document
var test = await client.index({
id: dataset.publish_id?.toString(),
index: index_name,
body: document,
refresh: true,
});
// Return the result
return response.json(test.body);
} catch (error) {
// Handle any errors
console.error(error);
return response.status(500).json({ error: 'An error occurred while updating the data.' });
}
}
public async destroy({}: HttpContext) {}
private async createXmlRecord(dataset: Dataset, datasetNode: XMLBuilder) {
const domNode = await this.getDatasetXmlDomNode(dataset);
if (domNode) {
datasetNode.import(domNode);
}
}
private async getDatasetXmlDomNode(dataset: Dataset) {
const xmlModel = new XmlModel(dataset);
// xmlModel.setModel(dataset);
xmlModel.excludeEmptyFields();
xmlModel.caching = true;
// const cache = dataset.xmlCache ? dataset.xmlCache : null;
// dataset.load('xmlCache');
if (dataset.xmlCache) {
xmlModel.xmlCache = dataset.xmlCache;
}
// return cache.getDomDocument();
const domDocument: XMLBuilder | null = await xmlModel.getDomDocument();
return domDocument;
}
}

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<resource xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://datacite.org/schema/kernel-4"
xsi:schemaLocation="http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4.3/metadata.xsd">
<identifier identifierType="DOI">10.21388/tethys.213</identifier>
<creators>
<creator>
<creatorName nameType="Personal">Moser, Michael</creatorName>
<givenName>Michael</givenName>
<familyName>Moser</familyName>
<affiliation>GBA</affiliation>
</creator>
</creators>
<titles>
<title xml:lang="en">rewerewr</title>
</titles>
<publisher>Tethys RDR</publisher>
<publicationYear>2024</publicationYear>
<subjects>
<subject xml:lang="de">Aletshausen-Langenneufnach Störung</subject>
<subject xml:lang="de">Wolfersberg-Moosach Störung</subject>
<subject xml:lang="en">wefwef</subject>
</subjects>
<language>en</language>
<contributors>
<contributor contributorType="RegistrationAuthority">
<contributorName>Jürgen Reitner</contributorName>
</contributor>
</contributors>
<dates>
<date dateType="Created">2023-11-30</date>
</dates>
<version>1</version>
<resourceType resourceTypeGeneral="Dataset">Dataset</resourceType>
<alternateIdentifiers>
<alternateIdentifier alternateIdentifierType="url">https://www.tethys.at/dataset/213</alternateIdentifier>
</alternateIdentifiers>
<rightsList>
<rights xml:lang="" rightsURI="https://creativecommons.org/licenses/by/4.0/deed.en"
schemeURI="https://spdx.org/licenses/" rightsIdentifierScheme="SPDX"
rightsIdentifier="CC-BY-4.0">Creative Commons Attribution 4.0 International (CC BY 4.0)</rights>
<rights rightsURI="info:eu-repo/semantics/openAccess">Open Access</rights>
</rightsList>
<sizes>
<size>1 datasets</size>
</sizes>
<formats>
<format>image/png</format>
</formats>
<descriptions>
<description xml:lang="en" descriptionType="Abstract">rewrewr</description>
</descriptions>
<geoLocations>
<geoLocation>
<geoLocationBox>
<westBoundLongitude>11.71142578125</westBoundLongitude>
<eastBoundLongitude>14.414062500000002</eastBoundLongitude>
<southBoundLatitude>46.58906908309185</southBoundLatitude>
<northBoundLatitude>47.45780853075031</northBoundLatitude>
</geoLocationBox>
</geoLocation>
</geoLocations>
</resource>

View File

@ -1,16 +1,31 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
import { RequestContract } from '@ioc:Adonis/Core/Request'; // import { RequestContract } from '@ioc:Adonis/Core/Request';
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; import { Request } from '@adonisjs/core/http';
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
import { create } from 'xmlbuilder2'; import { create } from 'xmlbuilder2';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc.js';
import timezone from 'dayjs/plugin/timezone'; import timezone from 'dayjs/plugin/timezone.js';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { transform } from 'saxon-js'; import SaxonJS from 'saxon-js';
// import { Xslt, xmlParse } from 'xslt-processor' // import { Xslt, xmlParse } from 'xslt-processor'
import { OaiErrorCodes, OaiModelError } from 'App/Exceptions/OaiErrorCodes'; import { OaiErrorCodes, OaiModelError } from '#app/exceptions/OaiErrorCodes';
import { OaiModelException } from 'App/Exceptions/OaiModelException'; import { OaiModelException, BadOaiModelException } from '#app/exceptions/OaiModelException';
import Dataset from '#models/dataset';
import Collection from '#models/collection';
import { getDomain, preg_match } from '#app/utils/utility-functions';
import XmlModel from '#app/Library/XmlModel';
import logger from '@adonisjs/core/services/logger';
import ResumptionToken from '#app/Library/Oai/ResumptionToken';
// import Config from '@ioc:Adonis/Core/Config';
import config from '@adonisjs/core/services/config'
// import { inject } from '@adonisjs/fold';
import { inject } from '@adonisjs/core'
// import { TokenWorkerContract } from "MyApp/Models/TokenWorker";
import TokenWorkerContract from '#library/Oai/TokenWorkerContract';
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
interface XslTParameter { interface XslTParameter {
[key: string]: any; [key: string]: any;
@ -20,10 +35,20 @@ interface Dictionary {
[index: string]: string; [index: string]: string;
} }
interface ListParameter {
cursor: number;
totalIds: number;
start: number;
reldocIds: (number | null)[];
metadataPrefix: string;
}
@inject()
export default class OaiController { export default class OaiController {
// private deliveringDocumentStates = ["published", "deleted"]; private deliveringDocumentStates = ['published', 'deleted'];
// private sampleRegEx = /^[A-Za-zäüÄÜß0-9\-_.!~]+$/; private sampleRegEx = /^[A-Za-zäüÄÜß0-9\-_.!~]+$/;
private xsltParameter: XslTParameter; private xsltParameter: XslTParameter;
/** /**
* Holds xml representation of document information to be processed. * Holds xml representation of document information to be processed.
* *
@ -32,23 +57,20 @@ export default class OaiController {
private xml: XMLBuilder; private xml: XMLBuilder;
private proc; private proc;
constructor() {
constructor(public tokenWorker: TokenWorkerContract) {
// Load the XSLT file // Load the XSLT file
this.proc = readFileSync('public/assets2/datasetxml2oai.sef.json'); this.proc = readFileSync('public/assets2/datasetxml2oai.sef.json');
// tests
// const xslPath = 'assets/datasetxml2oai-pmh.xslt'; // Replace with the actual path to your XSLT file
// this.proc = readFileSync(xslPath, 'utf-8');
// this.configuration = new Configuration();
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
} }
public async index({ response, request }: HttpContextContract): Promise<void> { public async index({ response, request }: HttpContext): Promise<void> {
this.xml = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>'); this.xml = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');
// this.proc = new XSLTProcessor(); // this.proc = new XSLTProcessor();
// const stylesheet = readFileSync(__dirname + "/datasetxml2oai.sef.json"); // const stylesheet = readFileSync(__dirname + "/datasetxml2oai.sef.json");
const xsltParameter = (this.xsltParameter = {}); const xsltParameter: XslTParameter = (this.xsltParameter = {});
let oaiRequest: Dictionary = {}; let oaiRequest: Dictionary = {};
if (request.method() === 'POST') { if (request.method() === 'POST') {
@ -59,9 +81,16 @@ export default class OaiController {
xsltParameter['oai_error_code'] = 'unknown'; xsltParameter['oai_error_code'] = 'unknown';
xsltParameter['oai_error_message'] = 'Only POST and GET methods are allowed for OAI-PMH.'; xsltParameter['oai_error_message'] = 'Only POST and GET methods are allowed for OAI-PMH.';
} }
let earliestDateFromDb;
// const oaiRequest: OaiParameter = request.body; // const oaiRequest: OaiParameter = request.body;
try { try {
this.handleRequest(oaiRequest, request); const firstPublishedDataset: Dataset | null = await Dataset.earliestPublicationDate();
firstPublishedDataset != null &&
(earliestDateFromDb = firstPublishedDataset.server_date_published.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"));
this.xsltParameter['earliestDatestamp'] = earliestDateFromDb;
// start the request
await this.handleRequest(oaiRequest, request);
} catch (error) { } catch (error) {
if (error instanceof OaiModelException) { if (error instanceof OaiModelException) {
const code = error.oaiCode; const code = error.oaiCode;
@ -80,9 +109,9 @@ export default class OaiController {
const xmlString = this.xml.end({ prettyPrint: true }); const xmlString = this.xml.end({ prettyPrint: true });
let xmlOutput; let xmlOutput; // = xmlString;
try { try {
const result = await transform({ const result = await SaxonJS.transform({
// stylesheetFileName: `${config.TMP_BASE_DIR}/data-quality/rules/iati.sef.json`, // stylesheetFileName: `${config.TMP_BASE_DIR}/data-quality/rules/iati.sef.json`,
stylesheetText: this.proc, stylesheetText: this.proc,
destination: 'serialized', destination: 'serialized',
@ -106,7 +135,7 @@ export default class OaiController {
response.status(StatusCodes.OK).send(xmlOutput); response.status(StatusCodes.OK).send(xmlOutput);
} }
protected handleRequest(oaiRequest: Dictionary, request: RequestContract) { protected async handleRequest(oaiRequest: Dictionary, request: Request) {
// Setup stylesheet // Setup stylesheet
// $this->loadStyleSheet('datasetxml2oai-pmh.xslt'); // $this->loadStyleSheet('datasetxml2oai-pmh.xslt');
@ -116,7 +145,7 @@ export default class OaiController {
this.xsltParameter['unixTimestamp'] = now.unix(); this.xsltParameter['unixTimestamp'] = now.unix();
// set OAI base url // set OAI base url
const baseDomain = process.env.BASE_DOMAIN || 'localhost'; const baseDomain = process.env.OAI_BASE_DOMAIN || 'localhost';
this.xsltParameter['baseURL'] = baseDomain + '/oai'; this.xsltParameter['baseURL'] = baseDomain + '/oai';
this.xsltParameter['repURL'] = request.protocol() + '://' + request.hostname(); this.xsltParameter['repURL'] = request.protocol() + '://' + request.hostname();
this.xsltParameter['downloadLink'] = request.protocol() + '://' + request.hostname() + '/file/download/'; this.xsltParameter['downloadLink'] = request.protocol() + '://' + request.hostname() + '/file/download/';
@ -130,17 +159,15 @@ export default class OaiController {
this.handleIdentify(); this.handleIdentify();
} else if (verb === 'ListMetadataFormats') { } else if (verb === 'ListMetadataFormats') {
this.handleListMetadataFormats(); this.handleListMetadataFormats();
} } else if (verb == 'GetRecord') {
// else if (verb == "GetRecord") { await this.handleGetRecord(oaiRequest);
// await this.handleGetRecord(oaiRequest); } else if (verb == 'ListRecords') {
// } else if (verb == "ListRecords") { await this.handleListRecords(oaiRequest);
// await this.handleListRecords(oaiRequest); } else if (verb == 'ListIdentifiers') {
// } else if (verb == "ListIdentifiers") { await this.handleListIdentifiers(oaiRequest);
// await this.handleListIdentifiers(oaiRequest); } else if (verb == 'ListSets') {
// } else if (verb == "ListSets") { await this.handleListSets();
// await this.handleListSets(); } else {
// }
else {
this.handleIllegalVerb(); this.handleIllegalVerb();
} }
} else { } else {
@ -182,6 +209,434 @@ export default class OaiController {
this.xml.root().ele('Datasets'); this.xml.root().ele('Datasets');
} }
protected async handleListSets() {
const repIdentifier = 'tethys.at';
this.xsltParameter['repIdentifier'] = repIdentifier;
const datasetElement = this.xml.root().ele('Datasets');
const sets: { [key: string]: string } = {
'open_access': 'Set for open access licenses',
'openaire_data': "OpenAIRE",
'doc-type:ResearchData': 'Set for document type ResearchData',
...(await this.getSetsForDatasetTypes()),
...(await this.getSetsForCollections()),
// ... await this.getSetsForProjects(),
} as Dictionary;
for (const [key, value] of Object.entries(sets)) {
const setElement = datasetElement.ele('Rdr_Sets');
setElement.att('Type', key);
setElement.att('TypeName', value);
}
}
protected async handleGetRecord(oaiRequest: Dictionary) {
const repIdentifier = 'tethys.at';
this.xsltParameter['repIdentifier'] = repIdentifier;
const dataId = this.validateAndGetIdentifier(oaiRequest);
const dataset = await Dataset.query()
.where('publish_id', dataId)
.preload('xmlCache')
.preload('collections', (builder) => {
builder.preload('collectionRole');
})
.first();
if (!dataset || !dataset.publish_id) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'The value of the identifier argument is unknown or illegal in this repository.',
OaiErrorCodes.IDDOESNOTEXIST,
);
}
const metadataPrefix = this.validateAndGetMetadataPrefix(oaiRequest);
this.xsltParameter['oai_metadataPrefix'] = metadataPrefix;
// do not deliver datasets which are restricted by document state defined in deliveringStates
this.validateDatasetState(dataset);
// add xml elements
const datasetNode = this.xml.root().ele('Datasets');
await this.createXmlRecord(dataset, datasetNode);
}
protected async handleListIdentifiers(oaiRequest: Dictionary) {
!this.tokenWorker.isConnected && (await this.tokenWorker.connect());
const maxIdentifier: number = config.get('oai.max.listidentifiers', 100);
await this.handleLists(oaiRequest, maxIdentifier);
}
protected async handleListRecords(oaiRequest: Dictionary) {
!this.tokenWorker.isConnected && (await this.tokenWorker.connect());
const maxRecords: number = config.get('oai.max.listrecords', 100);
await this.handleLists(oaiRequest, maxRecords);
}
private async handleLists(oaiRequest: Dictionary, maxRecords: number) {
maxRecords = maxRecords || 100;
const repIdentifier = 'tethys.at';
this.xsltParameter['repIdentifier'] = repIdentifier;
const datasetNode = this.xml.root().ele('Datasets');
// list initialisation
const numWrapper: ListParameter = {
cursor: 0,
totalIds: 0,
start: maxRecords + 1,
reldocIds: [],
metadataPrefix: '',
};
// resumptionToken is defined
if ('resumptionToken' in oaiRequest) {
await this.handleResumptionToken(oaiRequest, maxRecords, numWrapper);
} else {
// no resumptionToken is given
await this.handleNoResumptionToken(oaiRequest, numWrapper);
}
// handling of document ids
const restIds = numWrapper.reldocIds as number[];
const workIds = restIds.splice(0, maxRecords) as number[]; // array_splice(restIds, 0, maxRecords);
// no records returned
if (workIds.length == 0) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'The combination of the given values results in an empty list.',
OaiErrorCodes.NORECORDSMATCH,
);
}
const datasets: Dataset[] = await Dataset.query()
.whereIn('publish_id', workIds)
.preload('xmlCache')
.preload('collections', (builder) => {
builder.preload('collectionRole');
})
.orderBy('publish_id');
for (const dataset of datasets) {
await this.createXmlRecord(dataset, datasetNode);
}
// store the further Ids in a resumption-file
const countRestIds = restIds.length; //84
if (countRestIds > 0) {
const token = new ResumptionToken();
token.startPosition = numWrapper.start; //101
token.totalIds = numWrapper.totalIds; //184
token.documentIds = restIds; //101 -184
token.metadataPrefix = numWrapper.metadataPrefix;
// $tokenWorker->storeResumptionToken($token);
const res: string = await this.tokenWorker.set(token);
// set parameters for the resumptionToken-node
// const res = token.ResumptionId;
this.setParamResumption(res, numWrapper.cursor, numWrapper.totalIds);
}
}
private async handleResumptionToken(oaiRequest: Dictionary, maxRecords: number, numWrapper: ListParameter) {
const resParam = oaiRequest['resumptionToken']; //e.g. "158886496600000"
const token = await this.tokenWorker.get(resParam);
if (!token) {
throw new OaiModelException(StatusCodes.INTERNAL_SERVER_ERROR, 'cache is outdated.', OaiErrorCodes.BADRESUMPTIONTOKEN);
}
numWrapper.cursor = token.startPosition - 1; //startet dann bei Index 10
numWrapper.start = token.startPosition + maxRecords;
numWrapper.totalIds = token.totalIds;
numWrapper.reldocIds = token.documentIds;
numWrapper.metadataPrefix = token.metadataPrefix;
this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
}
private async handleNoResumptionToken(oaiRequest: Dictionary, numWrapper: ListParameter) {
// no resumptionToken is given
if ('metadataPrefix' in oaiRequest) {
numWrapper.metadataPrefix = oaiRequest['metadataPrefix'];
} else {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'The prefix of the metadata argument is unknown.',
OaiErrorCodes.BADARGUMENT,
);
}
this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
let finder: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query();
// add server state restrictions
finder.whereIn('server_state', this.deliveringDocumentStates);
if ('set' in oaiRequest) {
const set = oaiRequest['set'] as string;
const setArray = set.split(':');
if (setArray[0] == 'data-type') {
if (setArray.length == 2 && setArray[1]) {
finder.where('type', setArray[1]);
}
} else if (setArray[0] == 'open_access') {
const openAccessLicences = ['CC-BY-4.0', 'CC-BY-SA-4.0'];
finder.andWhereHas('licenses', (query) => {
query.whereIn('name', openAccessLicences);
});
} else if (setArray[0] == 'ddc') {
if (setArray.length == 2 && setArray[1] != '') {
finder.andWhereHas('collections', (query) => {
query.where('number', setArray[1]);
});
}
}
}
// const timeZone = "Europe/Vienna"; // Canonical time zone name
// &from=2020-09-03&until2020-09-03
// &from=2020-09-11&until=2021-05-11
if ('from' in oaiRequest && 'until' in oaiRequest) {
const from = oaiRequest['from'] as string;
let fromDate = dayjs(from); //.tz(timeZone);
const until = oaiRequest['until'] as string;
let untilDate = dayjs(until); //.tz(timeZone);
if (!fromDate.isValid() || !untilDate.isValid()) {
throw new OaiModelException(StatusCodes.INTERNAL_SERVER_ERROR, 'Date Parameter is not valid.', OaiErrorCodes.BADARGUMENT);
}
fromDate = dayjs.tz(from, 'Europe/Vienna');
untilDate = dayjs.tz(until, 'Europe/Vienna');
if (from.length != until.length) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'The request has different granularities for the from and until parameters.',
OaiErrorCodes.BADARGUMENT,
);
}
fromDate.hour() == 0 && (fromDate = fromDate.startOf('day'));
untilDate.hour() == 0 && (untilDate = untilDate.endOf('day'));
finder.whereBetween('server_date_published', [fromDate.format('YYYY-MM-DD HH:mm:ss'), untilDate.format('YYYY-MM-DD HH:mm:ss')]);
} else if ('from' in oaiRequest && !('until' in oaiRequest)) {
const from = oaiRequest['from'] as string;
let fromDate = dayjs(from);
if (!fromDate.isValid()) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'From date parameter is not valid.',
OaiErrorCodes.BADARGUMENT,
);
}
fromDate = dayjs.tz(from, 'Europe/Vienna');
fromDate.hour() == 0 && (fromDate = fromDate.startOf('day'));
const now = dayjs();
if (fromDate.isAfter(now)) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'Given from date is greater than now. The given values results in an empty list.',
OaiErrorCodes.NORECORDSMATCH,
);
} else {
finder.andWhere('server_date_published', '>=', fromDate.format('YYYY-MM-DD HH:mm:ss'));
}
} else if (!('from' in oaiRequest) && 'until' in oaiRequest) {
const until = oaiRequest['until'] as string;
let untilDate = dayjs(until);
if (!untilDate.isValid()) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'Until date parameter is not valid.',
OaiErrorCodes.BADARGUMENT,
);
}
untilDate = dayjs.tz(until, 'Europe/Vienna');
untilDate.hour() == 0 && (untilDate = untilDate.endOf('day'));
const firstPublishedDataset: Dataset = (await Dataset.earliestPublicationDate()) as Dataset;
const earliestPublicationDate = dayjs(firstPublishedDataset.server_date_published.toISO()); //format("YYYY-MM-DDThh:mm:ss[Z]"));
if (earliestPublicationDate.isAfter(untilDate)) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
`earliestDatestamp is greater than given until date.
The given values results in an empty list.`,
OaiErrorCodes.NORECORDSMATCH,
);
} else {
finder.andWhere('server_date_published', '<=', untilDate.format('YYYY-MM-DD HH:mm:ss'));
}
}
let reldocIdsDocs = await finder.select('publish_id').orderBy('publish_id');
numWrapper.reldocIds = reldocIdsDocs.map((dat) => dat.publish_id);
numWrapper.totalIds = numWrapper.reldocIds.length; //212
}
private setParamResumption(res: string, cursor: number, totalIds: number) {
const tomorrow = dayjs().add(1, 'day').format('YYYY-MM-DDThh:mm:ss[Z]');
this.xsltParameter['dateDelete'] = tomorrow;
this.xsltParameter['res'] = res;
this.xsltParameter['cursor'] = cursor;
this.xsltParameter['totalIds'] = totalIds;
}
private validateAndGetIdentifier(oaiRequest: Dictionary): number {
// Identifier references metadata Urn, not plain Id!
// Currently implemented as 'oai:foo.bar.de:{docId}' or 'urn:nbn...-123'
if (!('identifier' in oaiRequest)) {
throw new BadOaiModelException('The prefix of the identifier argument is unknown.');
}
const dataId = Number(this.getDocumentIdByIdentifier(oaiRequest.identifier));
if (isNaN(dataId)) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'The value of the identifier argument is illegal in this repository.',
OaiErrorCodes.BADARGUMENT,
);
}
return dataId;
}
private validateAndGetMetadataPrefix(oaiRequest: Dictionary): string {
let metadataPrefix = '';
if ('metadataPrefix' in oaiRequest) {
metadataPrefix = oaiRequest['metadataPrefix'];
} else {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'The prefix of the metadata argument is unknown.',
OaiErrorCodes.BADARGUMENT,
);
}
return metadataPrefix;
}
private validateDatasetState(dataset: Dataset): void {
if (dataset.server_state == null || !this.deliveringDocumentStates.includes(dataset.server_state)) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'Document is not available for OAI export!',
OaiErrorCodes.NORECORDSMATCH,
);
}
}
private async createXmlRecord(dataset: Dataset, datasetNode: XMLBuilder) {
const domNode = await this.getDatasetXmlDomNode(dataset);
if (domNode) {
// add frontdoor url and data-type
dataset.publish_id && this.addLandingPageAttribute(domNode, dataset.publish_id.toString());
this.addSpecInformation(domNode, 'data-type:' + dataset.type);
if (dataset.collections) {
for (const coll of dataset.collections) {
const collRole = coll.collectionRole;
this.addSpecInformation(domNode, collRole.oai_name + ':' + coll.number);
}
}
datasetNode.import(domNode);
}
}
private async getDatasetXmlDomNode(dataset: Dataset) {
const xmlModel = new XmlModel(dataset);
// xmlModel.setModel(dataset);
xmlModel.excludeEmptyFields();
xmlModel.caching = true;
// const cache = dataset.xmlCache ? dataset.xmlCache : null;
// dataset.load('xmlCache');
if (dataset.xmlCache) {
xmlModel.xmlCache = dataset.xmlCache;
}
// return cache.getDomDocument();
const domDocument: XMLBuilder | null = await xmlModel.getDomDocument();
return domDocument;
}
private addSpecInformation(domNode: XMLBuilder, information: string) {
domNode.ele('SetSpec').att('Value', information);
}
private addLandingPageAttribute(domNode: XMLBuilder, dataid: string) {
const baseDomain = process.env.OAI_BASE_DOMAIN || 'localhost';
const url = 'https://' + getDomain(baseDomain) + '/dataset/' + dataid;
// add attribute du dataset xml element
domNode.att('landingpage', url);
}
private getDocumentIdByIdentifier(oaiIdentifier: string): string {
const identifierParts: string[] = oaiIdentifier.split(':'); // explode(":", $oaiIdentifier);
const dataId: string = identifierParts[2];
// switch (identifierParts[0]) {
// case 'oai':
// if (isset($identifierParts[2])) {
// $dataId = $identifierParts[2];
// }
// break;
// default:
// throw new OaiModelException(
// 'The prefix of the identifier argument is unknown.',
// OaiModelError::BADARGUMENT
// );
// break;
// }
// if (empty($dataId) or !preg_match('/^\d+$/', $dataId)) {
// throw new OaiModelException(
// 'The value of the identifier argument is unknown or illegal in this repository.',
// OaiModelError::IDDOESNOTEXIST
// );
return dataId;
}
private async getSetsForCollections(): Promise<Dictionary> {
const sets: { [key: string]: string } = {} as Dictionary;
const collections = await Collection.query()
.select('name', 'number', 'role_id')
.whereHas('collectionRole', (query) => {
query.where('visible_oai', true);
})
.preload('collectionRole');
collections.forEach((collection) => {
// if collection has a collection role (classification like ddc):
if (collection.number) {
// collection.load('collectionRole');
const setSpec = collection.collectionRole?.oai_name + ':' + collection.number;
sets[setSpec] = `Set ${collection.number} '${collection.name}'`;
}
});
return sets;
}
private async getSetsForDatasetTypes(): Promise<Dictionary> {
const sets: { [key: string]: string } = {} as Dictionary;
const datasets: Array<Dataset> = await Dataset.query().select('type').where('server_state', 'published');
datasets.forEach((dataset) => {
if (dataset.type && false == preg_match(this.sampleRegEx, dataset.type)) {
const msg = `Invalid SetSpec (data-type='${dataset.type}').
Allowed characters are [${this.sampleRegEx}].`;
// Log::error("OAI-PMH: $msg");
logger.error(`OAI-PMH: ${msg}`);
return;
}
const setSpec = 'data-type:' + dataset.type;
sets[setSpec] = `Set for document type '${dataset.type}'`;
});
return sets;
}
private handleIllegalVerb() { private handleIllegalVerb() {
this.xsltParameter['oai_error_code'] = 'badVerb'; this.xsltParameter['oai_error_code'] = 'badVerb';
this.xsltParameter['oai_error_message'] = 'The verb provided in the request is illegal.'; this.xsltParameter['oai_error_message'] = 'The verb provided in the request is illegal.';

View File

@ -0,0 +1,310 @@
import type { HttpContext } from '@adonisjs/core/http';
import User from '#models/user';
import Dataset from '#models/dataset';
import Field from '#app/Library/Field';
import BaseModel from '#models/base_model';
import { DateTime } from 'luxon';
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
import vine from '@vinejs/vine';
import mail from '@adonisjs/mail/services/main';
import logger from '@adonisjs/core/services/logger';
import { validate } from 'deep-email-validator';
interface Dictionary {
[index: string]: string;
}
export default class DatasetsController {
public async index({ auth, request, inertia }: HttpContext) {
const user = (await User.find(auth.user?.id)) as User;
const page = request.input('page', 1);
let datasets: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query();
// if (request.input('search')) {
// // users = users.whereRaw('name like %?%', [request.input('search')])
// const searchTerm = request.input('search');
// datasets.where('name', 'ilike', `%${searchTerm}%`);
// }
if (request.input('sort')) {
type SortOrder = 'asc' | 'desc' | undefined;
let attribute = request.input('sort');
let sortOrder: SortOrder = 'asc';
if (attribute.substr(0, 1) === '-') {
sortOrder = 'desc';
// attribute = substr(attribute, 1);
attribute = attribute.substr(1);
}
datasets.orderBy(attribute, sortOrder);
} else {
// users.orderBy('created_at', 'desc');
datasets.orderBy('id', 'asc');
}
// const users = await User.query().orderBy('login').paginate(page, limit);
const myDatasets = await datasets
.where('server_state', 'approved')
.where('reviewer_id', user.id)
.preload('titles')
.preload('user', (query) => query.select('id', 'login'))
.preload('editor', (query) => query.select('id', 'login'))
.paginate(page, 10);
return inertia.render('Reviewer/Dataset/Index', {
datasets: myDatasets.serialize(),
filters: request.all(),
can: {
review: await auth.user?.can(['dataset-review']),
reject: await auth.user?.can(['dataset-review-reject']),
},
});
}
public async review({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
// .preload('titles')
// .preload('descriptions')
.preload('user', (builder) => {
builder.select('id', 'login');
})
.firstOrFail();
const validStates = ['approved'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be reviewed. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('reviewer.dataset.list');
}
const fieldnames: Array<string> = await dataset.describe();
const fields: Dictionary = {};
for (const fieldName of fieldnames) {
const field: Field = dataset.getField(fieldName) as Field;
const modelClass = field.getValueModelClass();
let fieldValues = field.getValue();
let value = '';
if (fieldValues === null || fieldValues == undefined) {
continue;
}
if (modelClass === null) {
if (typeof fieldValues === 'number') {
// If the field values are a number, use them as is
value = fieldValues.toString();
} else {
// If the field values are not a number, use the replace() function to remove non-printable characters
value = fieldValues.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '\xEF\xBF\xBD ');
}
} else {
if (!Array.isArray(fieldValues)) {
fieldValues = [fieldValues];
}
for (const fieldValue of fieldValues) {
if (fieldValue === null) {
continue;
}
if (modelClass.prototype instanceof BaseModel) {
// this.mapModelAttributes(fieldValue, childNode);
value = '<ul>';
Object.keys(fieldValue).forEach((prop) => {
let modelValue = fieldValue[prop];
// console.log(`${prop}: ${value}`);
if (modelValue != null) {
if (modelValue instanceof DateTime) {
modelValue = modelValue.toFormat('yyyy-MM-dd HH:mm:ss').trim();
} else {
modelValue = modelValue.toString().trim();
}
// Replace invalid XML-1.0-Characters by UTF-8 replacement character.
modelValue = modelValue.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '\xEF\xBF\xBD ');
value = value + '<li>' + prop + ' : ' + modelValue + '</li>';
}
});
value = value + '</ul>';
} else if (modelClass instanceof DateTime) {
// console.log('Value is a luxon date');
// this.mapDateAttributes(fieldValue, childNode);
value = value + ' Year ' + modelClass.year.toString();
value = value + ' Month ' + modelClass.month.toString();
value = value + ' Day ' + modelClass.day.toString();
value = value + ' Hour ' + modelClass.hour.toString();
value = value + ' Minute ' + modelClass.minute.toString();
value = value + ' Second ' + modelClass.second.toString();
value = value + ' UnixTimestamp ' + modelClass.toUnixInteger().toString();
let zoneName = modelClass.zoneName ? modelClass.zoneName : '';
value = value + ' Timezone ' + zoneName;
}
}
}
if (value != '') {
fields[fieldName] = value;
}
}
return inertia.render('Reviewer/Dataset/Review', {
dataset,
fields: fields,
});
}
public async reviewUpdate({ request, response }: HttpContext) {
const id = request.param('id');
// const { id } = params;
const dataset = await Dataset.findOrFail(id);
const validStates = ['approved'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be reviewed. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('reviewer.dataset.list');
}
dataset.server_state = 'reviewed';
try {
// await dataset.related('editor').associate(user); // speichert schon ab
await dataset.save();
return response.toRoute('reviewer.dataset.list').flash('message', `You have successfully reviewed dataset ${dataset.id}!`);
} catch (error) {
// Handle any errors
console.error(error);
return response.status(500).json({ error: 'An error occurred while reviewing the data.' });
}
}
public async reject({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
// .preload('titles')
// .preload('descriptions')
.preload('editor', (builder) => {
builder.select('id', 'login', 'email');
})
.firstOrFail();
const validStates = ['approved'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('reviewer.dataset.list');
}
return inertia.render('Reviewer/Dataset/Reject', {
dataset,
});
}
public async rejectUpdate({ request, response, auth }: HttpContext) {
const authUser = auth.user!;
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
.preload('editor', (builder) => {
builder.select('id', 'login', 'email');
})
.firstOrFail();
// const newSchema = schema.create({
// server_state: schema.string({ trim: true }),
// reject_reviewer_note: schema.string({ trim: true }, [rules.minLength(10), rules.maxLength(500)]),
// });
const newSchema = vine.object({
server_state: vine.string().trim(),
reject_reviewer_note: vine.string().trim().minLength(10).maxLength(500),
send_mail: vine.boolean().optional(),
});
try {
// await request.validate({ schema: newSchema });
const validator = vine.compile(newSchema);
await request.validateUsing(validator);
} catch (error) {
// return response.badRequest(error.messages);
throw error;
}
const validStates = ['approved'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
return response
.flash(
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
'warning',
)
.redirect()
.toRoute('reviewer.dataset.list');
}
// dataset.server_state = 'reviewed';
dataset.server_state = 'rejected_reviewer';
const rejectReviewerNote = request.input('reject_reviewer_note', '');
dataset.reject_reviewer_note = rejectReviewerNote;
// add logic for sending reject message
const sendMail = request.input('send_email', false);
// const validRecipientEmail = await this.checkEmailDomain('arno.kaimbacher@outlook.at');
const validationResult = await validate({
email: dataset.editor.email,
validateSMTP: false,
});
const validRecipientEmail: boolean = validationResult.valid;
let emailStatusMessage = '';
if (sendMail == true) {
if (dataset.editor.email && validRecipientEmail) {
try {
await mail.send((message) => {
message.to(dataset.editor.email).subject('Dataset Rejection Notification').html(`
<p>Dear editor ${dataset.editor.login},</p>
<p>Your approved dataset with ID ${dataset.id} has been rejected.</p>
<p>Reason for rejection: ${rejectReviewerNote}</p>
<p>Best regards,<br>Your Tethys reviewer: ${authUser.login}</p>
`);
});
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.editor.email}.`;
} catch (error) {
logger.error(error);
return response
.flash('Dataset has not been rejected due to an email error: ' + error.message, 'error')
.toRoute('reviewer.dataset.list');
}
} else {
emailStatusMessage = ` However, the email could not be sent because the editor's email address (${dataset.editor.email}) is not valid.`;
}
}
await dataset.save();
return response
.toRoute('reviewer.dataset.list')
.flash(`You have rejected dataset ${dataset.id}! to editor ${dataset.editor.login}`, 'message');
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
// import User from 'App/Models/User'; // import User from 'App/Models/User';
// import Dataset from 'App/Models/Dataset'; // import Dataset from 'App/Models/Dataset';
// import License from 'App/Models/License'; // import License from 'App/Models/License';
@ -10,12 +10,11 @@ import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
// import Collection from 'App/Models/Collection'; // import Collection from 'App/Models/Collection';
// import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator'; // import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
// import dayjs from 'dayjs'; // import dayjs from 'dayjs';
import Person from 'App/Models/Person'; import Person from '#models/person';
import { ModelQueryBuilderContract } from "@adonisjs/lucid/types/model";
import type { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm';
export default class PersonController { export default class PersonController {
public async index({ auth, request, inertia }: HttpContextContract) { public async index({ auth, request, inertia }: HttpContext) {
// const user = (await User.find(auth.user?.id)) as User; // const user = (await User.find(auth.user?.id)) as User;
const page = request.input('page', 1); const page = request.input('page', 1);
let persons: ModelQueryBuilderContract<typeof Person, Person> = Person.query(); let persons: ModelQueryBuilderContract<typeof Person, Person> = Person.query();

View File

@ -1,61 +0,0 @@
/*
|--------------------------------------------------------------------------
| Http Exception Handler
|--------------------------------------------------------------------------
|
| AdonisJs will forward all exceptions occurred during an HTTP request to
| the following class. You can learn more about exception handling by
| reading docs.
|
| The exception handler extends a base `HttpExceptionHandler` which is not
| mandatory, however it can do lot of heavy lifting to handle the errors
| properly.
|
*/
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import Logger from '@ioc:Adonis/Core/Logger';
import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler';
export default class ExceptionHandler extends HttpExceptionHandler {
protected statusPages = {
'401,403': 'errors/unauthorized',
'404': 'errors/not-found',
'500..599': 'errors/server-error',
};
constructor() {
super(Logger);
}
public async handle(error: any, ctx: HttpContextContract) {
const { response, request, inertia } = ctx;
/**
* Handle failed authentication attempt
*/
// if (['E_INVALID_AUTH_PASSWORD', 'E_INVALID_AUTH_UID'].includes(error.code)) {
// session.flash('errors', { login: error.message });
// return response.redirect('/login');
// }
// if ([401].includes(error.status)) {
// session.flash('errors', { login: error.message });
// return response.redirect('/dashboard');
// }
// https://github.com/inertiajs/inertia-laravel/issues/56
if (request.header('X-Inertia') && [500, 503, 404, 403, 401].includes(response.getStatus())) {
return inertia.render('Error', {
status: response.getStatus(),
message: error.message,
});
// ->toResponse($request)
// ->setStatusCode($response->status());
}
/**
* Forward rest of the exceptions to the parent class
*/
return super.handle(error, ctx);
}
}

View File

@ -1,34 +0,0 @@
import { Response } from '@adonisjs/http-server/build/src/Response';
import { ServerResponse, IncomingMessage } from 'http';
import { RouterContract } from '@ioc:Adonis/Core/Route';
import { EncryptionContract } from '@ioc:Adonis/Core/Encryption';
import { ResponseConfig, ResponseContract } from '@ioc:Adonis/Core/Response';
class FlashResponse extends Response implements ResponseContract {
protected static macros = {};
protected static getters = {};
constructor(
public request: IncomingMessage,
public response: ServerResponse,
flashEncryption: EncryptionContract,
flashConfig: ResponseConfig,
flashRouter: RouterContract,
) {
super(request, response, flashEncryption, flashConfig, flashRouter);
}
public nonce: string;
public flash(key: string, message: any): this {
// Store the flash message in the session
this.ctx?.session.flash(key, message);
return this;
}
public toRoute(route: string): this {
// Redirect to the specified route
super.redirect().toRoute(route);
return this;
}
}
export default FlashResponse;

View File

@ -0,0 +1,93 @@
// import { Client } from 'guzzle';
// import { Log } from '@adonisjs/core/build/standalone';
// import { DoiInterface } from './interfaces/DoiInterface';
import DoiClientContract from '#app/Library/Doi/DoiClientContract';
import DoiClientException from '#app/exceptions/DoiClientException';
import { StatusCodes } from 'http-status-codes';
import logger from '@adonisjs/core/services/logger';
import { AxiosResponse } from 'axios';
import axios from 'axios';
export class DoiClient implements DoiClientContract {
public username: string;
public password: string;
public serviceUrl: string;
constructor() {
// const datacite_environment = process.env.DATACITE_ENVIRONMENT || 'debug';
this.username = process.env.DATACITE_USERNAME || '';
this.password = process.env.DATACITE_PASSWORD || '';
this.serviceUrl = process.env.DATACITE_SERVICE_URL || '';
// this.prefix = process.env.DATACITE_PREFIX || '';
// this.base_domain = process.env.BASE_DOMAIN || '';
if (this.username === '' || this.password === '' || this.serviceUrl === '') {
const message = 'issing configuration settings to properly initialize DOI client';
logger.error(message);
throw new DoiClientException(StatusCodes.BAD_REQUEST, message);
}
}
/**
* Creates a DOI with the given identifier
*
* @param doiValue The desired DOI identifier e.g. '10.5072/tethys.999',
* @param xmlMeta
* @param landingPageUrl e.g. https://www.tethys.at/dataset/1
*
* @return Promise<AxiosResponse<any>> The http response in the form of a axios response
*/
public async registerDoi(doiValue: string, xmlMeta: string, landingPageUrl: string): Promise<AxiosResponse<any>> {
//step 1: register metadata via xml upload
// state draft
// let response;
// let url = `${this.serviceUrl}/metadata/${doiValue}`; //https://mds.test.datacite.org/metadata/10.21388/tethys.213
const auth = {
username: this.username,
password: this.password,
};
let headers = {
'Content-Type': 'application/xml;charset=UTF-8',
};
try {
const metadataResponse = await axios.default.put(`${this.serviceUrl}/metadata/${doiValue}`, xmlMeta, { auth, headers });
// Response Codes
// 201 Created: operation successful
// 401 Unauthorised: no login
// 403 Forbidden: login problem, quota exceeded
// 415 Wrong Content Type : Not including content type in the header.
// 422 Unprocessable Entity : invalid XML
// let test = metadataResponse.data; // 'OK (10.21388/TETHYS.213)'
if (metadataResponse.status !== 201) {
const message = `Unexpected DataCite MDS response code ${metadataResponse.status}`;
logger.error(message);
throw new DoiClientException(metadataResponse.status, message);
}
const doiResponse = await axios.default.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, {
auth,
headers,
});
// Response Codes
// 201 Created: operation successful
// 400 Bad Request: request body must be exactly two lines: DOI and URL; wrong domain, wrong prefix;
// 401 Unauthorised: no login
// 403 Forbidden: login problem, quota exceeded
// 412 Precondition failed: metadata must be uploaded first.
if (doiResponse.status !== 201) {
const message = `Unexpected DataCite MDS response code ${doiResponse.status}`;
logger.error(message);
throw new DoiClientException(doiResponse.status, message);
}
return doiResponse;
} catch (error) {
// const message = `request for registering DOI failed with ${error.message}`;
// Handle the error, log it, or rethrow as needed
logger.error(error.message);
throw new DoiClientException(error.response.status, error.response.data);
}
}
}

View File

@ -0,0 +1,13 @@
// import ResumptionToken from './ResumptionToken';
import { AxiosResponse } from 'axios';
export default interface DoiClientContract {
username: string;
password: string;
serviceUrl: string;
// prefix: string;
// base_domain: string;
registerDoi(doiValue: string, xmlMeta: string, landingPageUrl: string): Promise<AxiosResponse<any>>;
// get(key: string): Promise<ResumptionToken | null>;
// set(token: ResumptionToken): Promise<string>;
}

View File

@ -0,0 +1,51 @@
export default class ResumptionToken {
private _documentIds: number[] = [];
private _metadataPrefix = '';
private _resumptionId = '';
private _startPosition = 0;
private _totalIds = 0;
get key(): string {
return this.metadataPrefix + this.startPosition + this.totalIds;
}
get documentIds(): number[] {
return this._documentIds;
}
set documentIds(idsToStore: number | number[]) {
this._documentIds = Array.isArray(idsToStore) ? idsToStore : [idsToStore];
}
get metadataPrefix(): string {
return this._metadataPrefix;
}
set metadataPrefix(value: string) {
this._metadataPrefix = value;
}
get resumptionId(): string {
return this._resumptionId;
}
set resumptionId(resumptionId: string) {
this._resumptionId = resumptionId;
}
get startPosition(): number {
return this._startPosition;
}
set startPosition(startPosition: number) {
this._startPosition = startPosition;
}
get totalIds(): number {
return this._totalIds;
}
set totalIds(totalIds: number) {
this._totalIds = totalIds;
}
}

View File

@ -0,0 +1,11 @@
import ResumptionToken from './ResumptionToken.js';
export default abstract class TokenWorkerContract {
abstract ttl: number;
abstract isConnected: boolean;
abstract connect(): void;
abstract close(): void;
abstract get(key: string): Promise<ResumptionToken | null>;
abstract set(token: ResumptionToken): Promise<string>;
}

View File

@ -0,0 +1,95 @@
import ResumptionToken from './ResumptionToken.js';
import { createClient, RedisClientType } from 'redis';
import InternalServerErrorException from '#app/exceptions/InternalServerException';
import { sprintf } from 'sprintf-js';
import dayjs from 'dayjs';
import TokenWorkerContract from './TokenWorkerContract.js';
export default class TokenWorkerService implements TokenWorkerContract {
protected filePrefix = 'rs_';
protected fileExtension = 'txt';
private cache: RedisClientType;
public ttl: number;
private url: string;
private connected = false;
constructor(ttl: number) {
this.ttl = ttl; // time to live
this.url = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
}
public async connect() {
this.cache = createClient({ url: this.url });
this.cache.on('error', (err) => {
this.connected = false;
console.log('[Redis] Redis Client Error: ', err);
});
this.cache.on('connect', () => {
this.connected = true;
});
await this.cache.connect();
}
public get isConnected(): boolean {
return this.connected;
}
public async has(key: string): Promise<boolean> {
const result = await this.cache.get(key);
return result !== undefined && result !== null;
}
public async set(token: ResumptionToken): Promise<string> {
const uniqueName = await this.generateUniqueName();
const serialToken = JSON.stringify(token);
await this.cache.setEx(uniqueName, this.ttl, serialToken);
return uniqueName;
}
private async generateUniqueName(): Promise<string> {
let fc = 0;
const uniqueId = dayjs().unix().toString();
let uniqueName: string;
let cacheKeyExists: boolean;
do {
// format values
// %s - String
// %d - Signed decimal number (negative, zero or positive)
// [0-9] (Specifies the minimum width held of to the variable value)
uniqueName = sprintf('%s%05d', uniqueId, fc++);
cacheKeyExists = await this.has(uniqueName);
} while (cacheKeyExists);
return uniqueName;
}
public async get(key: string): Promise<ResumptionToken | null> {
if (!this.cache) {
throw new InternalServerErrorException('Dataset is not available for OAI export!');
}
const result = await this.cache.get(key);
return result ? this.parseToken(result) : null;
}
private parseToken(result: string): ResumptionToken {
const rToken: ResumptionToken = new ResumptionToken();
const parsed = JSON.parse(result);
Object.assign(rToken, parsed);
return rToken;
}
public del(key: string) {
this.cache.del(key);
}
public flush() {
this.cache.flushAll();
}
public async close() {
await this.cache.disconnect();
this.connected = false;
}
}

View File

@ -1,8 +1,8 @@
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
import { create } from 'xmlbuilder2'; import { create } from 'xmlbuilder2';
import Dataset from 'App/Models/Dataset'; import Dataset from '#models/dataset';
import Field from './Field'; import Field from './Field.js';
import BaseModel from 'App/Models/BaseModel'; import BaseModel from '#models/base_model';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
export default class Strategy { export default class Strategy {
@ -10,7 +10,7 @@ export default class Strategy {
private config; private config;
private xml: XMLBuilder; private xml: XMLBuilder;
constructor(config) { constructor(config: any) {
this.version = 1.0; this.version = 1.0;
this.config = config; this.config = config;
} }
@ -45,11 +45,11 @@ export default class Strategy {
for (const fieldname of fieldsDiff) { for (const fieldname of fieldsDiff) {
const field = model.getField(fieldname); const field = model.getField(fieldname);
this.mapField(field, modelNode); this.mapField(field as Field, modelNode);
} }
} }
private mapField(field, modelNode: XMLBuilder) { private mapField(field: Field, modelNode: XMLBuilder) {
const modelClass = field.getValueModelClass(); const modelClass = field.getValueModelClass();
let fieldValues = field.getValue(); let fieldValues = field.getValue();
@ -107,10 +107,10 @@ export default class Strategy {
childNode.att('Timezone', zoneName); childNode.att('Timezone', zoneName);
} }
private mapModelAttributes(myObject, childNode: XMLBuilder) { private mapModelAttributes(myObject: any, childNode: XMLBuilder) {
Object.keys(myObject).forEach((prop) => { Object.keys(myObject).forEach((prop) => {
let value = myObject[prop]; let value = myObject[prop];
console.log(`${prop}: ${value}`); // console.log(`${prop}: ${value}`);
if (value != null) { if (value != null) {
if (value instanceof DateTime) { if (value instanceof DateTime) {
value = value.toFormat('yyyy-MM-dd HH:mm:ss').trim(); value = value.toFormat('yyyy-MM-dd HH:mm:ss').trim();
@ -161,7 +161,7 @@ export default class Strategy {
return fieldValues?.toString().trim(); return fieldValues?.toString().trim();
} }
private createModelNode(model) { private createModelNode(model: Dataset) {
const className = 'Rdr_' + model.constructor.name.split('\\').pop(); //Rdr_Dataset const className = 'Rdr_' + model.constructor.name.split('\\').pop(); //Rdr_Dataset
// return dom.createElement(className); // return dom.createElement(className);
return this.xml.root().ele(className); return this.xml.root().ele(className);

197
app/Library/Utils/Index.ts Normal file
View File

@ -0,0 +1,197 @@
import Dataset from '#models/dataset';
import { Client } from '@opensearch-project/opensearch';
import { create } from 'xmlbuilder2';
import SaxonJS from 'saxon-js';
import XmlModel from '#app/Library/XmlModel';
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
import logger from '@adonisjs/core/services/logger';
import { readFileSync } from 'fs';
import { DateTime } from 'luxon';
// import Config from '@ioc:Adonis/Core/Config';
import { getDomain } from '#app/utils/utility-functions';
// const opensearchNode = process.env.OPENSEARCH_HOST || 'localhost';
// const client = new Client({ node: `http://${opensearchNode}` }); // replace with your OpenSearch endpoint
interface XslTParameter {
[key: string]: any;
}
export default {
// opensearchNode: process.env.OPENSEARCH_HOST || 'localhost',
client: new Client({ node: `http://${process.env.OPENSEARCH_HOST || 'localhost'}` }), // replace with your OpenSearch endpoint
async getDoiRegisterString(dataset: Dataset): Promise<string | undefined> {
try {
const proc = readFileSync('public/assets2/doi_datacite.sef.json');
const xsltParameter: XslTParameter = {};
let xml = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');
const datasetNode = xml.root().ele('Dataset');
await createXmlRecord(dataset, datasetNode);
const xmlString = xml.end({ prettyPrint: false });
// set timestamp
const date = DateTime.now();
const unixTimestamp = date.toUnixInteger();
xsltParameter['unixTimestamp'] = unixTimestamp;
// set prefix
let prefix = '';
// let base_domain = '';
// const datacite_environment = process.env.DATACITE_ENVIRONMENT || 'debug';
// if (datacite_environment === 'debug') {
// prefix = process.env.DATACITE_TEST_PREFIX || '';
// base_domain = process.env.TEST_BASE_DOMAIN || '';
// } else if (datacite_environment === 'production') {
// prefix = process.env.DATACITE_PREFIX || '';
// base_domain = process.env.BASE_DOMAIN || '';
// }
prefix = process.env.DATACITE_PREFIX || '';
xsltParameter['prefix'] = prefix;
const repIdentifier = 'tethys';
xsltParameter['repIdentifier'] = repIdentifier;
let xmlOutput; // = xmlString;
try {
const result = await SaxonJS.transform({
// stylesheetFileName: `${config.TMP_BASE_DIR}/data-quality/rules/iati.sef.json`,
stylesheetText: proc,
destination: 'serialized',
// sourceFileName: sourceFile,
sourceText: xmlString,
stylesheetParams: xsltParameter,
// logLevel: 10,
});
xmlOutput = result.principalResult;
} catch (error) {
logger.error('An error occurred while creating the user', error.message);
}
return xmlOutput;
} catch (error) {
logger.error(`An error occurred while indexing datsaet with publish_id ${dataset.publish_id}.`);
}
},
async indexDocument(dataset: Dataset, index_name: string): Promise<void> {
try {
const proc = readFileSync('public/assets2/solr.sef.json');
const doc: string = await this.getTransformedString(dataset, proc);
let document = JSON.parse(doc);
await this.client.index({
id: dataset.publish_id?.toString(),
index: index_name,
body: document,
refresh: true,
});
logger.info(`dataset with publish_id ${dataset.publish_id} successfully indexed`);
} catch (error) {
logger.error(`An error occurred while indexing datsaet with publish_id ${dataset.publish_id}.`);
}
},
async getTransformedString(dataset: Dataset, proc: Buffer): Promise<string> {
let xml = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');
const datasetNode = xml.root().ele('Dataset');
await createXmlRecord(dataset, datasetNode);
const xmlString = xml.end({ prettyPrint: false });
try {
const result = await SaxonJS.transform({
stylesheetText: proc,
destination: 'serialized',
sourceText: xmlString,
});
return result.principalResult;
} catch (error) {
logger.error(`An error occurred while creating the user, error: ${error.message},`);
return '';
}
},
};
/**
* Return the default global focus trap stack
*
* @return {import('focus-trap').FocusTrap[]}
*/
// export const indexDocument = async (dataset: Dataset, index_name: string, proc: Buffer): Promise<void> => {
// try {
// const doc = await getJsonString(dataset, proc);
// let document = JSON.parse(doc);
// await client.index({
// id: dataset.publish_id?.toString(),
// index: index_name,
// body: document,
// refresh: true,
// });
// Logger.info(`dataset with publish_id ${dataset.publish_id} successfully indexed`);
// } catch (error) {
// Logger.error(`An error occurred while indexing datsaet with publish_id ${dataset.publish_id}.`);
// }
// };
// const getJsonString = async (dataset, proc): Promise<string> => {
// let xml = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');
// const datasetNode = xml.root().ele('Dataset');
// await createXmlRecord(dataset, datasetNode);
// const xmlString = xml.end({ prettyPrint: false });
// try {
// const result = await transform({
// stylesheetText: proc,
// destination: 'serialized',
// sourceText: xmlString,
// });
// return result.principalResult;
// } catch (error) {
// Logger.error(`An error occurred while creating the user, error: ${error.message},`);
// return '';
// }
// };
const createXmlRecord = async (dataset: Dataset, datasetNode: XMLBuilder): Promise<void> => {
const domNode = await getDatasetXmlDomNode(dataset);
if (domNode) {
// add frontdoor url and data-type
dataset.publish_id && addLandingPageAttribute(domNode, dataset.publish_id.toString());
addSpecInformation(domNode, 'data-type:' + dataset.type);
if (dataset.collections) {
for (const coll of dataset.collections) {
const collRole = coll.collectionRole;
addSpecInformation(domNode, collRole.oai_name + ':' + coll.number);
}
}
datasetNode.import(domNode);
}
};
const getDatasetXmlDomNode = async (dataset: Dataset): Promise<XMLBuilder | null> => {
const xmlModel = new XmlModel(dataset);
// xmlModel.setModel(dataset);
xmlModel.excludeEmptyFields();
xmlModel.caching = true;
// const cache = dataset.xmlCache ? dataset.xmlCache : null;
// dataset.load('xmlCache');
await dataset.load('xmlCache');
if (dataset.xmlCache) {
xmlModel.xmlCache = dataset.xmlCache;
}
// return cache.getDomDocument();
const domDocument: XMLBuilder | null = await xmlModel.getDomDocument();
return domDocument;
};
const addLandingPageAttribute = (domNode: XMLBuilder, dataid: string) => {
const baseDomain = process.env.OAI_BASE_DOMAIN || 'localhost';
const url = 'https://' + getDomain(baseDomain) + '/dataset/' + dataid;
// add attribute du dataset xml element
domNode.att('landingpage', url);
};
const addSpecInformation= (domNode: XMLBuilder, information: string) => {
domNode.ele('SetSpec').att('Value', information);
};

View File

@ -1,8 +1,9 @@
import DocumentXmlCache from 'App/Models/DocumentXmlCache'; import DocumentXmlCache from '#models/DocumentXmlCache';
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
import Dataset from 'App/Models/Dataset'; import Dataset from '#models/dataset';
import Strategy from './Strategy'; import Strategy from './Strategy.js';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { builder } from 'xmlbuilder2';
/** /**
* This is the description of the interface * This is the description of the interface
@ -84,10 +85,21 @@ export default class XmlModel {
this.cache = this.cache || new DocumentXmlCache(); this.cache = this.cache || new DocumentXmlCache();
this.cache.document_id = dataset.id; this.cache.document_id = dataset.id;
this.cache.xml_version = 1; // (int)$this->strategy->getVersion(); this.cache.xml_version = 1; // (int)$this->strategy->getVersion();
// this.cache.server_date_modified = dataset.server_date_modified.toFormat("yyyy-MM-dd HH:mm:ss"); this.cache.server_date_modified = dataset.server_date_modified.toFormat('yyyy-MM-dd HH:mm:ss');
this.cache.xml_data = domDocument.end(); this.cache.xml_data = domDocument.end();
await this.cache.save(); await this.cache.save();
} }
const node = domDocument.find(
(n) => {
const test = n.node.nodeName == 'Rdr_Dataset';
return test;
},
false,
true,
)?.node;
if (node != undefined) {
domDocument = builder({ version: '1.0', encoding: 'UTF-8', standalone: true }, node);
}
} }
return domDocument; return domDocument;
} }

View File

@ -1,67 +0,0 @@
import { AuthenticationException } from '@adonisjs/auth/build/standalone';
import type { GuardsList } from '@ioc:Adonis/Addons/Auth';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
/**
* Auth middleware is meant to restrict un-authenticated access to a given route
* or a group of routes.
*
* You must register this middleware inside `start/kernel.ts` file under the list
* of named middleware.
*/
export default class AuthMiddleware {
/**
* The URL to redirect to when request is Unauthorized
*/
protected redirectTo = '/app/login';
/**
* Authenticates the current HTTP request against a custom set of defined
* guards.
*
* The authentication loop stops as soon as the user is authenticated using any
* of the mentioned guards and that guard will be used by the rest of the code
* during the current request.
*/
protected async authenticate(auth: HttpContextContract['auth'], guards: (keyof GuardsList)[]) {
/**
* Hold reference to the guard last attempted within the for loop. We pass
* the reference of the guard to the "AuthenticationException", so that
* it can decide the correct response behavior based upon the guard
* driver
*/
let guardLastAttempted: string | undefined;
for (let guard of guards) {
guardLastAttempted = guard;
if (await auth.use(guard).check()) {
/**
* Instruct auth to use the given guard as the default guard for
* the rest of the request, since the user authenticated
* succeeded here
*/
auth.defaultGuard = guard;
return true;
}
}
/**
* Unable to authenticate using any guard
*/
throw new AuthenticationException('Unauthorized access', 'E_UNAUTHORIZED_ACCESS', guardLastAttempted, this.redirectTo);
}
/**
* Handle request
*/
public async handle({ auth }: HttpContextContract, next: () => Promise<void>, customGuards: (keyof GuardsList)[]) {
/**
* Uses the user defined guards or the default guard mentioned in
* the config file
*/
const guards = customGuards.length ? customGuards : [auth.name];
await this.authenticate(auth, guards);
await next();
}
}

View File

@ -1,76 +0,0 @@
import { DateTime } from 'luxon';
import {
column,
hasMany,
HasMany,
belongsTo,
BelongsTo,
// manyToMany,
// ManyToMany,
SnakeCaseNamingStrategy,
} from '@ioc:Adonis/Lucid/Orm';
import HashValue from './HashValue';
import Dataset from './Dataset';
import BaseModel from './BaseModel';
export default class File extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy();
public static primaryKey = 'id';
public static table = 'document_files';
public static selfAssignPrimaryKey = false;
@column({
isPrimary: true,
})
public id: number;
@column({})
public document_id: number;
@column({})
public pathName: string;
@column()
public label: string;
@column()
public comment: string;
@column()
public mimeType: string;
@column()
public language: string;
@column()
public fileSize: number;
@column()
public visibleInOai: boolean;
@column()
public visibleInFrontdoor: boolean;
@column()
public sortOrder: number;
@column.dateTime({ autoCreate: true })
public createdAt: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime;
// public function dataset()
// {
// return $this->belongsTo(Dataset::class, 'document_id', 'id');
// }
@belongsTo(() => Dataset, {
foreignKey: 'document_id',
})
public dataset: BelongsTo<typeof Dataset>;
@hasMany(() => HashValue, {
foreignKey: 'file_id',
})
public hashvalues: HasMany<typeof HashValue>;
}

View File

@ -1,110 +0,0 @@
import { DateTime } from 'luxon';
import { column, beforeSave, manyToMany, ManyToMany, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm';
import Hash from '@ioc:Adonis/Core/Hash';
import Role from './Role';
import Database from '@ioc:Adonis/Lucid/Database';
import Config from '@ioc:Adonis/Core/Config';
import Dataset from './Dataset';
import BaseModel from './BaseModel';
// export default interface IUser {
// id: number;
// login: string;
// email: string;
// // password: string;
// // createdAt: DateTime;
// // updatedAt: DateTime;
// // async (user): Promise<void>;
// }
const permissionTable = Config.get('rolePermission.permission_table', 'permissions');
const rolePermissionTable = Config.get('rolePermission.role_permission_table', 'role_has_permissions');
const roleTable = Config.get('rolePermission.role_table', 'roles');
const userRoleTable = Config.get('rolePermission.user_role_table', 'link_accounts_roles');
export default class User extends BaseModel {
public static table = 'accounts';
@column({ isPrimary: true })
public id: number;
@column()
public login: string;
@column()
public email: string;
@column({ serializeAs: null })
public password: string;
@column.dateTime({ autoCreate: true })
public createdAt: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime;
@beforeSave()
public static async hashPassword(user) {
if (user.$dirty.password) {
user.password = await Hash.make(user.password);
}
}
@manyToMany(() => Role, {
pivotForeignKey: 'account_id',
pivotRelatedForeignKey: 'role_id',
pivotTable: 'link_accounts_roles',
})
public roles: ManyToMany<typeof Role>;
@hasMany(() => Dataset, {
foreignKey: 'account_id',
})
public datasets: HasMany<typeof Dataset>;
// https://github.com/adonisjs/core/discussions/1872#discussioncomment-132289
public async getRoles(this: User): Promise<string[]> {
const test = await this.related('roles').query();
return test.map((role) => role.name);
}
public async can(permissionNames: Array<string>): Promise<boolean> {
// const permissions = await this.getPermissions()
// return Acl.check(expression, operand => _.includes(permissions, operand))
const hasPermission = await this.checkHasPermissions(this, permissionNames);
return hasPermission;
}
private async checkHasPermissions(user: User, permissionNames: Array<string>): Promise<boolean> {
let permissionPlaceHolder = '(';
let placeholders = new Array(permissionNames.length).fill('?');
permissionPlaceHolder += placeholders.join(',');
permissionPlaceHolder += ')';
let {
rows: {
0: { permissioncount },
},
} = await Database.rawQuery(
'SELECT count("p"."name") as permissionCount FROM ' +
roleTable +
' r INNER JOIN ' +
userRoleTable +
' ur ON ur.role_id=r.id AND "ur"."account_id"=? ' +
' INNER JOIN ' +
rolePermissionTable +
' rp ON rp.role_id=r.id ' +
' INNER JOIN ' +
permissionTable +
' p ON rp.permission_id=p.id AND "p"."name" in ' +
permissionPlaceHolder +
' LIMIT 1',
[user.id, ...permissionNames],
);
return permissioncount > 0;
}
}
// export default User;

View File

@ -1,19 +0,0 @@
import Database, {
// DatabaseQueryBuilderContract,
QueryClientContract,
TransactionClientContract,
} from '@ioc:Adonis/Lucid/Database';
import Config from '@ioc:Adonis/Core/Config';
export function getUserRoles(userId: number, trx?: TransactionClientContract): Promise<Array<string>> {
const { userRole } = Config.get('acl.joinTables');
return ((trx || Database) as QueryClientContract | TransactionClientContract)
.query()
.from('roles')
.distinct('roles.slug')
.leftJoin(userRole, `${userRole}.role_id`, 'roles.id')
.where(`${userRole}.user_id`, userId)
.then((res) => {
return res.map((r) => r.slug);
});
}

View File

@ -1,46 +0,0 @@
import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export default class AuthValidator {
constructor(protected ctx: HttpContextContract) {}
/*
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
*
* For example:
* 1. The username must be of data type string. But then also, it should
* not contain special characters or numbers.
* ```
* schema.string({}, [ rules.alpha() ])
* ```
*
* 2. The email must be of data type string, formatted as a valid
* email. But also, not used by any other user.
* ```
* schema.string({}, [
* rules.email(),
* rules.unique({ table: 'users', column: 'email' }),
* ])
* ```
*/
public schema = schema.create({
email: schema.string({ trim: true }, [
rules.email(),
// rules.unique({ table: 'accounts', column: 'email' })
]),
password: schema.string({}, [rules.minLength(6)]),
});
/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages: CustomMessages = {};
}

View File

@ -1,178 +0,0 @@
import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import dayjs from 'dayjs';
import { TitleTypes, DescriptionTypes, RelationTypes, ReferenceIdentifierTypes, ContributorTypes } from 'Contracts/enums';
export default class CreateDatasetValidator {
constructor(protected ctx: HttpContextContract) {}
/*
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
*
* For example:
* 1. The username must be of data type string. But then also, it should
* not contain special characters or numbers.
* ```
* schema.string({}, [ rules.alpha() ])
* ```
*
* 2. The email must be of data type string, formatted as a valid
* email. But also, not used by any other user.
* ```
* schema.string({}, [
* rules.email(),
* rules.unique({ table: 'users', column: 'email' }),
* ])
* ```
*/
public schema = schema.create({
// first step
language: schema.string({ trim: true }, [
rules.regex(/^[a-zA-Z0-9-_]+$/), //Must be alphanumeric with hyphens or underscores
]),
licenses: schema.array([rules.minLength(1)]).members(schema.number()), // define at least one license for the new dataset
rights: schema.string([rules.equalTo('true')]),
// second step
type: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
creating_corporation: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
titles: schema.array([rules.minLength(1)]).members(
schema.object().members({
value: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
type: schema.enum(Object.values(TitleTypes)),
language: schema.string({ trim: true }, [
rules.minLength(2),
rules.maxLength(255),
rules.translatedLanguage('/language', 'type'),
]),
}),
),
descriptions: schema.array([rules.minLength(1)]).members(
schema.object().members({
value: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
type: schema.enum(Object.values(DescriptionTypes)),
language: schema.string({ trim: true }, [
rules.minLength(2),
rules.maxLength(255),
rules.translatedLanguage('/language', 'type'),
]),
}),
),
authors: schema.array([rules.minLength(1)]).members(schema.object().members({ email: schema.string({ trim: true }) })),
contributors: schema.array.optional().members(
schema.object().members({
email: schema.string({ trim: true }),
pivot_contributor_type: schema.enum(Object.keys(ContributorTypes)),
}),
),
// third step
project_id: schema.number.optional(),
embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]),
coverage: schema.object().members({
x_min: schema.number(),
x_max: schema.number(),
y_min: schema.number(),
y_max: schema.number(),
elevation_absolut: schema.number.optional(),
elevation_min: schema.number.optional([rules.requiredIfExists('elevation_max')]),
elevation_max: schema.number.optional([rules.requiredIfExists('elevation_min')]),
depth_absolut: schema.number.optional(),
depth_min: schema.number.optional([rules.requiredIfExists('depth_max')]),
depth_max: schema.number.optional([rules.requiredIfExists('depth_min')]),
}),
references: schema.array.optional([rules.uniqueArray('value')]).members(
schema.object().members({
value: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
type: schema.enum(Object.values(ReferenceIdentifierTypes)),
relation: schema.enum(Object.values(RelationTypes)),
label: schema.string({ trim: true }, [rules.minLength(2), rules.maxLength(255)]),
}),
),
subjects: schema.array([rules.minLength(3), rules.uniqueArray('value')]).members(
schema.object().members({
value: schema.string({ trim: true }, [
rules.minLength(3),
rules.maxLength(255),
// rules.unique({ table: 'dataset_subjects', column: 'value' }),
]),
// type: schema.enum(Object.values(TitleTypes)),
language: schema.string({ trim: true }, [rules.minLength(2), rules.maxLength(255)]),
}),
),
// file: schema.file({
// size: '100mb',
// extnames: ['jpg', 'gif', 'png'],
// }),
files: schema.array([rules.minLength(1)]).members(
schema.file({
size: '100mb',
extnames: ['jpg', 'gif', 'png', 'tif', 'pdf'],
}),
),
// upload: schema.object().members({
// label: schema.string({ trim: true }, [rules.maxLength(255)]),
// // label: schema.string({ trim: true }, [
// // // rules.minLength(3),
// // // rules.maxLength(255),
// // ]),
// }),
});
/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages: CustomMessages = {
'minLength': '{{ field }} must be at least {{ options.minLength }} characters long',
'maxLength': '{{ field }} must be less then {{ options.maxLength }} characters long',
'required': '{{ field }} is required',
'unique': '{{ field }} must be unique, and this value is already taken',
// 'confirmed': '{{ field }} is not correct',
'licenses.minLength': 'at least {{ options.minLength }} permission must be defined',
'licenses.*.number': 'Define roles as valid numbers',
'rights.equalTo': 'you must agree to continue',
'titles.0.value.minLength': 'Main Title must be at least {{ options.minLength }} characters long',
'titles.0.value.required': 'Main Title is required',
'titles.*.value.required': 'Additional title is required, if defined',
'titles.*.type.required': 'Additional title type is required',
'titles.*.language.required': 'Additional title language is required',
'titles.*.language.translatedLanguage': 'The language of the translated title must be different from the language of the dataset',
'descriptions.0.value.minLength': 'Main Abstract must be at least {{ options.minLength }} characters long',
'descriptions.0.value.required': 'Main Abstract is required',
'descriptions.*.value.required': 'Additional description is required, if defined',
'descriptions.*.type.required': 'Additional description type is required',
'descriptions.*.language.required': 'Additional description language is required',
'descriptions.*.language.translatedLanguage':
'The language of the translated description must be different from the language of the dataset',
'authors.minLength': 'at least {{ options.minLength }} author must be defined',
'contributors.*.pivot_contributor_type.required': 'contributor type is required, if defined',
'after': `{{ field }} must be older than ${dayjs().add(10, 'day')}`,
'subjects.minLength': 'at least {{ options.minLength }} keywords must be defined',
'subjects.uniqueArray': 'The {{ options.array }} array must have unique values based on the {{ options.field }} attribute.',
'subjects.*.value.required': 'keyword value is required',
'subjects.*.value.minLength': 'keyword value must be at least {{ options.minLength }} characters long',
'subjects.*.type.required': 'keyword type is required',
'subjects.*.language.required': 'language of keyword is required',
'references.*.value.required': 'Additional reference value is required, if defined',
'references.*.type.required': 'Additional reference identifier type is required',
'references.*.relation.required': 'Additional reference relation type is required',
'references.*.label.required': 'Additional reference label is required',
'files.minLength': 'At least {{ options.minLength }} file upload is required.',
'files.*.size': 'file size is to big',
'files.extnames': 'file extension is not supported',
};
}

View File

@ -1,69 +0,0 @@
import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export default class CreateRoleValidator {
constructor(protected ctx: HttpContextContract) {}
/*
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
*
* For example:
* 1. The username must be of data type string. But then also, it should
* not contain special characters or numbers.
* ```
* schema.string({}, [ rules.alpha() ])
* ```
*
* 2. The email must be of data type string, formatted as a valid
* email. But also, not used by any other user.
* ```
* schema.string({}, [
* rules.email(),
* rules.unique({ table: 'users', column: 'email' }),
* ])
* ```
*/
public schema = schema.create({
name: schema.string({ trim: true }, [
rules.minLength(3),
rules.maxLength(255),
rules.unique({ table: 'roles', column: 'name' }),
rules.regex(/^[a-zA-Z0-9-_]+$/), //Must be alphanumeric with hyphens or underscores
]),
display_name: schema.string.optional({ trim: true }, [
rules.minLength(3),
rules.maxLength(255),
rules.unique({ table: 'roles', column: 'name' }),
rules.regex(/^[a-zA-Z0-9-_]+$/), //Must be alphanumeric with hyphens or underscores
]),
description: schema.string.optional({}, [rules.minLength(3), rules.maxLength(255)]),
permissions: schema.array([rules.minLength(1)]).members(schema.number()), // define at least one role for the new role
});
// emails: schema
// .array([rules.minLength(1)])
// .members(
// schema.object().members({ email: schema.string({}, [rules.email()]) })
// ),
/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages: CustomMessages = {
'minLength': '{{ field }} must be at least {{ options.minLength }} characters long',
'maxLength': '{{ field }} must be less then {{ options.maxLength }} characters long',
'required': '{{ field }} is required',
'unique': '{{ field }} must be unique, and this value is already taken',
'confirmed': '{{ field }} is not correct',
'permissions.minLength': 'at least {{ options.minLength }} permission must be defined',
'permissions.*.number': 'Define roles as valid numbers',
};
}

View File

@ -1,64 +0,0 @@
import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export default class CreateUserValidator {
constructor(protected ctx: HttpContextContract) {}
/*
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
*
* For example:
* 1. The username must be of data type string. But then also, it should
* not contain special characters or numbers.
* ```
* schema.string({}, [ rules.alpha() ])
* ```
*
* 2. The email must be of data type string, formatted as a valid
* email. But also, not used by any other user.
* ```
* schema.string({}, [
* rules.email(),
* rules.unique({ table: 'users', column: 'email' }),
* ])
* ```
*/
public schema = schema.create({
login: schema.string({ trim: true }, [
rules.minLength(3),
rules.maxLength(50),
rules.unique({ table: 'accounts', column: 'login' }),
rules.regex(/^[a-zA-Z0-9-_]+$/), //Must be alphanumeric with hyphens or underscores
]),
email: schema.string({}, [rules.email(), rules.unique({ table: 'accounts', column: 'email' })]),
password: schema.string([rules.confirmed(), rules.minLength(6)]),
roles: schema.array([rules.minLength(1)]).members(schema.number()), // define at least one role for the new user
});
// emails: schema
// .array([rules.minLength(1)])
// .members(
// schema.object().members({ email: schema.string({}, [rules.email()]) })
// ),
/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages: CustomMessages = {
'minLength': '{{ field }} must be at least {{ options.minLength }} characters long',
'maxLength': '{{ field }} must be less then {{ options.maxLength }} characters long',
'required': '{{ field }} is required',
'unique': '{{ field }} must be unique, and this value is already taken',
'confirmed': '{{ field }} is not correct',
'roles.minLength': 'at least {{ options.minLength }} role must be defined',
'roles.*.number': 'Define roles as valid numbers',
};
}

View File

@ -1,97 +0,0 @@
import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
// import { Request } from '@adonisjs/core/build/standalone';
export default class UpdateRoleValidator {
protected ctx: HttpContextContract;
public schema;
constructor(ctx: HttpContextContract) {
this.ctx = ctx;
this.schema = this.createSchema();
}
// public get schema() {
// return this._schema;
// }
private createSchema() {
return schema.create({
name: schema.string({ trim: true }, [
rules.minLength(3),
rules.maxLength(50),
rules.unique({
table: 'roles',
column: 'name',
whereNot: { id: this.ctx?.params.id },
}),
rules.regex(/^[a-zA-Z0-9-_]+$/),
//Must be alphanumeric with hyphens or underscores
]),
description: schema.string.optional({}, [rules.minLength(3), rules.maxLength(255)]),
permissions: schema.array([rules.minLength(1)]).members(schema.number()), // define at least one permission for the new role
});
}
/*
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
*
* For example:
* 1. The username must be of data type string. But then also, it should
* not contain special characters or numbers.
* ```
* schema.string({}, [ rules.alpha() ])
* ```
*
* 2. The email must be of data type string, formatted as a valid
* email. But also, not used by any other user.
* ```
* schema.string({}, [
* rules.email(),
* rules.unique({ table: 'users', column: 'email' }),
* ])
* ```
*/
// public refs = schema.refs({
// id: this.ctx.params.id
// })
// public schema = schema.create({
// login: schema.string({ trim: true }, [
// rules.minLength(3),
// rules.maxLength(50),
// rules.unique({
// table: 'accounts',
// column: 'login',
// // whereNot: { id: this.refs.id }
// whereNot: { id: this.ctx?.params.id },
// }),
// // rules.regex(/^[a-zA-Z0-9-_]+$/),
// //Must be alphanumeric with hyphens or underscores
// ]),
// email: schema.string({}, [rules.email(), rules.unique({ table: 'accounts', column: 'email' })]),
// password: schema.string.optional([rules.confirmed(), rules.minLength(6)]),
// roles: schema.array([rules.minLength(1)]).members(schema.number()), // define at least one role for the new user
// });
/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages: CustomMessages = {
'minLength': '{{ field }} must be at least {{ options.minLength }} characters long',
'maxLength': '{{ field }} must be less then {{ options.maxLength }} characters long',
'required': '{{ field }} is required',
'unique': '{{ field }} must be unique, and this value is already taken',
'permissions.minLength': 'at least {{ options.minLength }} permission must be defined',
'permissions.*.number': 'Define permissions as valid numbers',
};
}

View File

@ -1,103 +0,0 @@
import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
// import { Request } from '@adonisjs/core/build/standalone';
export default class UpdateUserValidator {
protected ctx: HttpContextContract;
public schema;
constructor(ctx: HttpContextContract) {
this.ctx = ctx;
this.schema = this.createSchema();
}
// public get schema() {
// return this._schema;
// }
private createSchema() {
return schema.create({
login: schema.string({ trim: true }, [
rules.minLength(3),
rules.maxLength(50),
rules.unique({
table: 'accounts',
column: 'login',
// whereNot: { id: this.refs.id }
whereNot: { id: this.ctx?.params.id },
}),
// rules.regex(/^[a-zA-Z0-9-_]+$/),
//Must be alphanumeric with hyphens or underscores
]),
email: schema.string({}, [
rules.email(),
rules.unique({ table: 'accounts', column: 'email', whereNot: { id: this.ctx?.params.id } }),
]),
password: schema.string.optional([rules.confirmed(), rules.minLength(6)]),
roles: schema.array.optional([rules.minLength(1)]).members(schema.number()), // define at least one role for the new user
});
}
/*
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
*
* For example:
* 1. The username must be of data type string. But then also, it should
* not contain special characters or numbers.
* ```
* schema.string({}, [ rules.alpha() ])
* ```
*
* 2. The email must be of data type string, formatted as a valid
* email. But also, not used by any other user.
* ```
* schema.string({}, [
* rules.email(),
* rules.unique({ table: 'users', column: 'email' }),
* ])
* ```
*/
// public refs = schema.refs({
// id: this.ctx.params.id
// })
// public schema = schema.create({
// login: schema.string({ trim: true }, [
// rules.minLength(3),
// rules.maxLength(50),
// rules.unique({
// table: 'accounts',
// column: 'login',
// // whereNot: { id: this.refs.id }
// whereNot: { id: this.ctx?.params.id },
// }),
// // rules.regex(/^[a-zA-Z0-9-_]+$/),
// //Must be alphanumeric with hyphens or underscores
// ]),
// email: schema.string({}, [rules.email(), rules.unique({ table: 'accounts', column: 'email' })]),
// password: schema.string.optional([rules.confirmed(), rules.minLength(6)]),
// roles: schema.array([rules.minLength(1)]).members(schema.number()), // define at least one role for the new user
// });
/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages: CustomMessages = {
'minLength': '{{ field }} must be at least {{ options.minLength }} characters long',
'maxLength': '{{ field }} must be less then {{ options.maxLength }} characters long',
'required': '{{ field }} is required',
'unique': '{{ field }} must be unique, and this value is already taken',
'confirmed': '{{ field }} is not correct',
'roles.minLength': 'at least {{ options.minLength }} role must be defined',
'roles.*.number': 'Define roles as valid numbers',
};
}

View File

@ -0,0 +1,12 @@
class DoiClientException extends Error {
public status: number;
public message: string;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.message = message;
}
}
export default DoiClientException;

View File

@ -1,5 +1,5 @@
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import HTTPException from './HttpException'; import HTTPException from './HttpException.js';
class InternalServerErrorException extends HTTPException { class InternalServerErrorException extends HTTPException {
constructor(message?: string) { constructor(message?: string) {

View File

@ -1,4 +1,5 @@
import { Exception } from '@adonisjs/core/build/standalone'; import { Exception } from "@adonisjs/core/exceptions";
import { HttpContext } from "@adonisjs/core/http";
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -23,21 +24,21 @@ export default class InvalidCredentialException extends Exception {
* Unable to find user * Unable to find user
*/ */
public static invalidUid() { public static invalidUid() {
const error = new this('User not found', 400, 'E_INVALID_AUTH_UID'); const error = new this('User not found', {status: 400, code: 'E_INVALID_AUTH_UID'});
return error; return error;
} }
/** /**
* Invalid user password * Invalid user password
*/ */
public static invalidPassword() { public static invalidPassword() {
const error = new this('Password mis-match', 400, 'E_INVALID_AUTH_PASSWORD'); const error = new this('Password mis-match', {status: 400, code: 'E_INVALID_AUTH_PASSWORD'});
return error; return error;
} }
/** /**
* Flash error message and redirect the user back * Flash error message and redirect the user back
*/ */
private respondWithRedirect(error, ctx) { private respondWithRedirect(error: any, ctx: HttpContext) {
// if (!ctx.session) { // if (!ctx.session) {
// return ctx.response.status(this.status).send(this.responseText); // return ctx.response.status(this.status).send(this.responseText);
// } // }
@ -59,7 +60,7 @@ export default class InvalidCredentialException extends Exception {
* Handle this exception by itself * Handle this exception by itself
*/ */
public handle(error, ctx) { public handle(error: any, ctx: HttpContext) {
// return response.status(403).view.render("errors/unauthorized", { // return response.status(403).view.render("errors/unauthorized", {
// error: error, // error: error,
// }); // });

View File

@ -1,6 +1,6 @@
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
// import HTTPException from './HttpException'; // import HTTPException from './HttpException';
import { OaiErrorCodes } from './OaiErrorCodes'; import { OaiErrorCodes } from './OaiErrorCodes.js';
export class ErrorCode { export class ErrorCode {
public static readonly Unauthenticated = 'Unauthenticated'; public static readonly Unauthenticated = 'Unauthenticated';

125
app/exceptions/handler.ts Normal file
View File

@ -0,0 +1,125 @@
/*
|--------------------------------------------------------------------------
| Http Exception Handler
|--------------------------------------------------------------------------
|
| AdonisJs will forward all exceptions occurred during an HTTP request to
| the following class. You can learn more about exception handling by
| reading docs.
|
| The exception handler extends a base `HttpExceptionHandler` which is not
| mandatory, however it can do lot of heavy lifting to handle the errors
| properly.
|
*/
import app from '@adonisjs/core/services/app';
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http';
// import logger from '@adonisjs/core/services/logger';
import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http';
export default class HttpExceptionHandler extends ExceptionHandler {
/**
* In debug mode, the exception handler will display verbose errors
* with pretty printed stack traces.
*/
protected debug = !app.inProduction;
/**
* Status pages are used to display a custom HTML pages for certain error
* codes. You might want to enable them in production only, but feel
* free to enable them in development as well.
*/
protected renderStatusPages = true; //app.inProduction;
/**
* Status pages is a collection of error code range and a callback
* to return the HTML contents to send as a response.
*/
// protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
// '401..403': (error, { view }) => {
// return view.render('./errors/unauthorized', { error });
// },
// '404': (error, { view }) => {
// return view.render('./errors/not-found', { error });
// },
// '500..599': (error, { view }) => {
// return view.render('./errors/server-error', { error });
// },
// };
protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
'404': (error, { inertia }) => {
return inertia.render('Errors/ServerError', {
error: error.message,
code: error.status,
});
},
'401..403': async (error, { inertia }) => {
// session.flash('errors', error.message);
return inertia.render('Errors/ServerError', {
error: error.message,
code: error.status,
});
},
'500..599': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
};
// constructor() {
// super(logger);
// }
public async handle(error: any, ctx: HttpContext) {
const { response, request, session } = ctx;
/**
* Handle failed authentication attempt
*/
// if (['E_INVALID_AUTH_PASSWORD', 'E_INVALID_AUTH_UID'].includes(error.code)) {
// session.flash('errors', { login: error.message });
// return response.redirect('/login');
// }
// if ([401].includes(error.status)) {
// session.flash('errors', { login: error.message });
// return response.redirect('/dashboard');
// }
// https://github.com/inertiajs/inertia-laravel/issues/56
// let test = response.getStatus(); //200
// let header = request.header('X-Inertia'); // true
// if (request.header('X-Inertia') && [500, 503, 404, 403, 401, 200].includes(response.getStatus())) {
if (request.header('X-Inertia') && [422].includes(error.status)) {
// session.flash('errors', error.messages.errors);
session.flash('errors', error.messages);
return response.redirect().back();
// return inertia.render('errors/server_error', {
// return inertia.render('errors/server_error', {
// // status: response.getStatus(),
// error: error,
// });
// ->toResponse($request)
// ->setStatusCode($response->status());
}
// Dynamically change the error templates based on the absence of X-Inertia header
// if (!ctx.request.header('X-Inertia')) {
// this.statusPages = {
// '401..403': (error, { view }) => view.render('./errors/unauthorized', { error }),
// '404': (error, { view }) => view.render('./errors/not-found', { error }),
// '500..599': (error, { view }) => view.render('./errors/server-error', { error }),
// };
// }
/**
* Forward rest of the exceptions to the parent class
*/
return super.handle(error, ctx);
}
/**
* The method is used to report error to the logging service or
* the a third party error monitoring service.
*
* @note You should not attempt to send a response from this method.
*/
async report(error: unknown, ctx: HttpContext) {
return super.report(error, ctx);
}
}

View File

@ -1,11 +1,12 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import { HttpContext } from '@adonisjs/core/http';
import Config from '@ioc:Adonis/Core/Config'; // import Config from '@ioc:Adonis/Core/Config';
import Database from '@ioc:Adonis/Lucid/Database'; import config from '@adonisjs/core/services/config'
import User from 'App/Models/User'; import db from '@adonisjs/lucid/services/db';
import User from '#models/user';
// import { Exception } from '@adonisjs/core/build/standalone' // import { Exception } from '@adonisjs/core/build/standalone'
const roleTable = Config.get('rolePermission.role_table', 'roles'); const roleTable = config.get('rolePermission.role_table', 'roles');
const userRoleTable = Config.get('rolePermission.user_role_table', 'user_roles'); const userRoleTable = config.get('rolePermission.user_role_table', 'user_roles');
/** /**
* Role authentication to check if user has any of the specified roles * Role authentication to check if user has any of the specified roles
@ -16,7 +17,7 @@ export default class Is {
/** /**
* Handle request * Handle request
*/ */
public async handle({ auth, response }: HttpContextContract, next: () => Promise<void>, roleNames: string[]) { public async handle({ auth, response }: HttpContext, next: () => Promise<void>, roleNames: string[]) {
/** /**
* Check if user is logged-in or not. * Check if user is logged-in or not.
*/ */
@ -33,7 +34,8 @@ export default class Is {
// 401, // 401,
// "E_INVALID_AUTH_UID"); // "E_INVALID_AUTH_UID");
} }
await next(); // await next();
return next()
} }
private async checkHasRoles(user: User, roleNames: Array<string>): Promise<boolean> { private async checkHasRoles(user: User, roleNames: Array<string>): Promise<boolean> {
@ -46,7 +48,7 @@ export default class Is {
0: { 0: {
0: { roleCount }, 0: { roleCount },
}, },
} = await Database.rawQuery( } = await db.rawQuery(
'SELECT count(`ur`.`id`) as roleCount FROM ' + 'SELECT count(`ur`.`id`) as roleCount FROM ' +
userRoleTable + userRoleTable +
' ur INNER JOIN ' + ' ur INNER JOIN ' +

View File

@ -0,0 +1,25 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import type { Authenticators } from '@adonisjs/auth/types'
/**
* Auth middleware is used authenticate HTTP requests and deny
* access to unauthenticated users.
*/
export default class AuthMiddleware {
/**
* The URL to redirect to, when authentication fails
*/
redirectTo = '/app/login'
async handle(
ctx: HttpContext,
next: NextFn,
options: {
guards?: (keyof Authenticators)[]
} = {}
) {
await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
return next()
}
}

View File

@ -1,13 +1,14 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import { HttpContext } from '@adonisjs/core/http';
import Config from '@ioc:Adonis/Core/Config'; // import Config from '@ioc:Adonis/Core/Config';
import Database from '@ioc:Adonis/Lucid/Database'; import config from '@adonisjs/core/services/config';
import User from 'App/Models/User'; import db from '@adonisjs/lucid/services/db';
import { Exception } from '@adonisjs/core/build/standalone'; import User from '#models/user';
import { Exception } from '@adonisjs/core/exceptions';
const permissionTable = Config.get('rolePermission.permission_table', 'permissions'); const permissionTable = config.get('rolePermission.permission_table', 'permissions');
const rolePermissionTable = Config.get('rolePermission.role_permission_table', 'role_has_permissions'); const rolePermissionTable = config.get('rolePermission.role_permission_table', 'role_has_permissions');
const roleTable = Config.get('rolePermission.role_table', 'roles'); const roleTable = config.get('rolePermission.role_table', 'roles');
const userRoleTable = Config.get('rolePermission.user_role_table', 'link_accounts_roles'); const userRoleTable = config.get('rolePermission.user_role_table', 'link_accounts_roles');
/** /**
* Permission authentication to check if user has any of the specified permissions * Permission authentication to check if user has any of the specified permissions
@ -18,7 +19,7 @@ export default class Can {
/** /**
* Handle request * Handle request
*/ */
public async handle({ auth, response }: HttpContextContract, next: () => Promise<void>, permissionNames: string[]) { public async handle({ auth, response }: HttpContext, next: () => Promise<void>, permissionNames: string[]) {
/** /**
* Check if user is logged-in * Check if user is logged-in
*/ */
@ -31,9 +32,10 @@ export default class Can {
// return response.unauthorized({ // return response.unauthorized({
// error: `Doesn't have required role(s): ${permissionNames.join(',')}`, // error: `Doesn't have required role(s): ${permissionNames.join(',')}`,
// }); // });
throw new Exception(`Doesn't have required permission(s): ${permissionNames.join(',')}`, 401); throw new Exception(`Doesn't have required permission(s): ${permissionNames.join(',')}`, { status: 401 });
} }
await next(); // await next();
return next();
} }
private async checkHasPermissions(user: User, permissionNames: Array<string>): Promise<boolean> { private async checkHasPermissions(user: User, permissionNames: Array<string>): Promise<boolean> {
@ -66,7 +68,7 @@ export default class Can {
rows: { rows: {
0: { permissioncount }, 0: { permissioncount },
}, },
} = await Database.rawQuery( } = await db.rawQuery(
'SELECT count("p"."name") as permissionCount FROM ' + 'SELECT count("p"."name") as permissionCount FROM ' +
roleTable + roleTable +
' r INNER JOIN ' + ' r INNER JOIN ' +

View File

@ -0,0 +1,19 @@
import { Logger } from '@adonisjs/core/logger';
import { HttpContext } from '@adonisjs/core/http';
import { NextFn } from '@adonisjs/core/types/http';
/**
* The container bindings middleware binds classes to their request
* specific value using the container resolver.
*
* - We bind "HttpContext" class to the "ctx" object
* - And bind "Logger" class to the "ctx.logger" object
*/
export default class ContainerBindingsMiddleware {
handle(ctx: HttpContext, next: NextFn) {
ctx.containerResolver.bindValue(HttpContext, ctx);
ctx.containerResolver.bindValue(Logger, ctx.logger);
return next();
}
}

View File

@ -0,0 +1,27 @@
import type { HttpContext } from '@adonisjs/core/http';
import type { NextFn } from '@adonisjs/core/types/http';
import type { Authenticators } from '@adonisjs/auth/types';
/**
* Guest middleware is used to deny access to routes that should
* be accessed by unauthenticated users.
*
* For example, the login page should not be accessible if the user
* is already logged-in
*/
export default class GuestMiddleware {
/**
* The URL to redirect to when user is logged-in
*/
redirectTo = '/';
async handle(ctx: HttpContext, next: NextFn, options: { guards?: (keyof Authenticators)[] } = {}) {
for (let guard of options.guards || [ctx.auth.defaultGuard]) {
if (await ctx.auth.use(guard).check()) {
return ctx.response.redirect(this.redirectTo, true);
}
}
return next();
}
}

View File

@ -0,0 +1,43 @@
/*
* This middleware class normalizes newlines in the request input data by replacing
* all occurrences of '\r\n' with '\n' recursively for strings, arrays, and objects.
*/
import type { HttpContext } from '@adonisjs/core/http';
import type { NextFn } from '@adonisjs/core/types/http';
export default class NormalizeNewlinesMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
// Function to recursively normalize newlines
const normalizeNewlines = (input: any): any => {
if (typeof input === 'string') {
return input.replace(/\r\n/g, '\n');
} else if (Array.isArray(input)) {
return input.map((item) => normalizeNewlines(item));
} else if (typeof input === 'object' && input !== null) {
for (const key in input) {
input[key] = normalizeNewlines(input[key]);
}
return input;
}
return input;
};
/**
* Middleware logic goes here (before the next call)
*/
// console.log(ctx)
// Get all request input
const input = ctx.request.all();
// Normalize newlines in text inputs
const normalizedInput = normalizeNewlines(input);
// Replace request input with normalized data
ctx.request.updateBody(normalizedInput);
/**
* Call next method in the pipeline and return its output
*/
const output = await next();
return output;
}
}

View File

@ -1,23 +1,25 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
import Database from '@ioc:Adonis/Lucid/Database'; import db from '@adonisjs/lucid/services/db';
import Config from '@ioc:Adonis/Core/Config'; import config from '@adonisjs/core/services/config';
import User from 'app/Models/User'; import User from '#models/user';
import { Exception } from '@adonisjs/core/build/standalone'; import { Exception } from '@adonisjs/core/exceptions';
const roleTable = Config.get('rolePermission.role_table', 'roles'); // const roleTable = Config.get('rolePermission.role_table', 'roles');
const userRoleTable = Config.get('rolePermission.user_role_table', 'link_accounts_roles'); const roleTable = config.get('rolePermission.role_table', 'roles');
// const userRoleTable = Config.get('rolePermission.user_role_table', 'link_accounts_roles');
const userRoleTable = config.get('rolePermission.user_role_table', 'user_roles');
// node ace make:middleware role // node ace make:middleware role
export default class Role { export default class Role {
// .middleware(['auth', 'role:admin,moderator']) // .middleware(['auth', 'role:admin,moderator'])
public async handle({ auth, response }: HttpContextContract, next: () => Promise<void>, userRoles: string[]) { public async handle({ auth, response }: HttpContext, next: () => Promise<void>, userRoles: string[]) {
// Check if user is logged-in or not. // Check if user is logged-in or not.
// let expression = ""; // let expression = "";
// if (Array.isArray(args)) { // if (Array.isArray(args)) {
// expression = args.join(" || "); // expression = args.join(" || ");
// } // }
let user = await auth.user; let user = auth.user as User;
if (!user) { if (!user) {
return response.unauthorized({ error: 'Must be logged in' }); return response.unauthorized({ error: 'Must be logged in' });
} }
@ -28,7 +30,7 @@ export default class Role {
// error: `Doesn't have required role(s): ${userRoles.join(',')}`, // error: `Doesn't have required role(s): ${userRoles.join(',')}`,
// // error: `Doesn't have required role(s)`, // // error: `Doesn't have required role(s)`,
// }); // });
throw new Exception(`Doesn't have required role(s): ${userRoles.join(',')}`, 401); throw new Exception(`Doesn't have required role(s): ${userRoles.join(',')}`, { status: 401 });
} }
// code for middleware goes here. ABOVE THE NEXT CALL // code for middleware goes here. ABOVE THE NEXT CALL
@ -62,7 +64,7 @@ export default class Role {
rows: { rows: {
0: { rolecount }, 0: { rolecount },
}, },
} = await Database.rawQuery( } = await db.rawQuery(
'SELECT count("r"."id") as roleCount FROM ' + 'SELECT count("r"."id") as roleCount FROM ' +
roleTable + roleTable +
' r INNER JOIN ' + ' r INNER JOIN ' +

View File

@ -1,4 +1,4 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContext } from '@adonisjs/core/http';
/** /**
* Silent auth middleware can be used as a global middleware to silent check * Silent auth middleware can be used as a global middleware to silent check
@ -10,7 +10,7 @@ export default class SilentAuthMiddleware {
/** /**
* Handle request * Handle request
*/ */
public async handle({ auth }: HttpContextContract, next: () => Promise<void>) { public async handle({ auth }: HttpContext, next: () => Promise<void>) {
/** /**
* Check if user is logged-in or not. If yes, then `ctx.auth.user` will be * Check if user is logged-in or not. If yes, then `ctx.auth.user` will be
* set to the instance of the currently logged in user. * set to the instance of the currently logged in user.

View File

@ -0,0 +1,47 @@
import type { HttpContext } from '@adonisjs/core/http';
import type { NextFn } from '@adonisjs/core/types/http';
declare global {
function myFunction(): boolean;
var myVariable: number;
interface StardustData {
pathname?: string;
namedRoutes?: Record<string, string>;
}
var stardust: StardustData;
}
declare global {}
export default class StardustMiddleware {
async handle(ctx: HttpContext, next: NextFn): Promise<void> {
/**
* Middleware logic goes here (before the next call)
*/
// Check if the request is an API request
if (!ctx.request.url().startsWith('/api')) {
// Middleware logic for non-API requests
const { pathname } = new URL(ctx.request.completeUrl()); // '/', '/app/login'
globalThis.myFunction = () => {
return true;
};
globalThis.myVariable = 1;
globalThis.stardust = {
...globalThis.stardust,
pathname,
};
/**
* Call next method in the pipeline and return its output
*/
await next();
} else {
// Skip middleware for API requests
await next();
}
}
}

View File

@ -1,10 +1,10 @@
import { column, BaseModel, SnakeCaseNamingStrategy, belongsTo, BelongsTo } from '@ioc:Adonis/Lucid/Orm'; import { column, BaseModel, SnakeCaseNamingStrategy, belongsTo } from '@adonisjs/lucid/orm';
import Dataset from './Dataset'; import Dataset from './dataset.js';
import { builder, create } from 'xmlbuilder2'; import { builder, create } from 'xmlbuilder2';
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
import Database from '@ioc:Adonis/Lucid/Database'; import db from '@adonisjs/lucid/services/db';
import dayjs from 'dayjs';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
export default class DocumentXmlCache extends BaseModel { export default class DocumentXmlCache extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
@ -87,7 +87,7 @@ export default class DocumentXmlCache extends BaseModel {
// Assuming 'DocumentXmlCache' has a table with a 'server_date_modified' column in your database // Assuming 'DocumentXmlCache' has a table with a 'server_date_modified' column in your database
public static async hasValidEntry(datasetId: number, datasetServerDateModified: DateTime): Promise<boolean> { public static async hasValidEntry(datasetId: number, datasetServerDateModified: DateTime): Promise<boolean> {
const serverDateModifiedString: string = datasetServerDateModified.toFormat('yyyy-MM-dd HH:mm:ss'); // Convert DateTime to ISO string const serverDateModifiedString: string = datasetServerDateModified.toFormat('yyyy-MM-dd HH:mm:ss'); // Convert DateTime to ISO string
const query = Database.from(this.table) const query = db.from(this.table)
.where('document_id', datasetId) .where('document_id', datasetId)
.where('server_date_modified', '>=', serverDateModifiedString) // Check if server_date_modified is newer or equal .where('server_date_modified', '>=', serverDateModifiedString) // Check if server_date_modified is newer or equal
.first(); .first();

33
app/models/appconfig.ts Normal file
View File

@ -0,0 +1,33 @@
import BaseModel from './base_model.js';
import { column } from '@adonisjs/lucid/orm';
export default class AppConfig extends BaseModel {
public static table = 'appconfigs'; // Specify the table name if it differs from the model name
@column({ isPrimary: true })
public id: number;
@column()
public appid: string;
@column()
public configkey: string;
@column()
public configvalue: string | null;
@column()
public type: number;
@column()
public lazy: number;
// async function setConfig(key: string, value: string) {
// await this.updateOrCreate({ key }, { value })
// }
// async function getConfig(key: string) {
// const config = await this.findBy('key', key)
// return config ? config.value : null
// }
}

51
app/models/backup_code.ts Normal file
View File

@ -0,0 +1,51 @@
import BaseModel from './base_model.js';
import { column, SnakeCaseNamingStrategy, belongsTo } from '@adonisjs/lucid/orm';
import User from './user.js';
import type { BelongsTo } from '@adonisjs/lucid/types/relations';
import db from '@adonisjs/lucid/services/db';
import hash from '@adonisjs/core/services/hash';
export default class BackupCode extends BaseModel {
public static table = 'backupcodes';
public static namingStrategy = new SnakeCaseNamingStrategy();
@column({
isPrimary: true,
})
public id: number;
@column({})
public user_id: number;
@column({
// serializeAs: null,
// consume: (value: string) => (value ? JSON.parse(encryption.decrypt(value) ?? '{}') : null),
// prepare: (value: string) => encryption.encrypt(JSON.stringify(value)),
})
public code: string;
@column({})
public used: boolean;
@belongsTo(() => User, {
foreignKey: 'user_id',
})
public user: BelongsTo<typeof User>;
// public static async getBackupCodes(user: User): Promise<BackupCode[]> {
// return await db.from(this.table).select('id', 'user_id', 'code', 'used').where('user_id', user.id);
// }
public static async deleteCodes(user: User): Promise<void> {
await db.from(this.table).where('user_id', user.id).delete();
}
public static async deleteCodesByUserId(uid: string): Promise<void> {
await db.from(this.table).where('user_id', uid).delete();
}
// Method to verify password
public async verifyCode(plainCode: string) {
return await hash.verify(this.code, plainCode);
}
}

View File

@ -1,4 +1,4 @@
import { BaseModel as LucidBaseModel } from '@ioc:Adonis/Lucid/Orm'; import { BaseModel as LucidBaseModel } from '@adonisjs/lucid/orm';
// import { ManyToManyQueryClient } from '@ioc:Adonis/Lucid/Orm'; // import { ManyToManyQueryClient } from '@ioc:Adonis/Lucid/Orm';
// export class CustomManyToManyQueryClient extends ManyToManyQueryClient { // export class CustomManyToManyQueryClient extends ManyToManyQueryClient {
@ -13,7 +13,6 @@ import { BaseModel as LucidBaseModel } from '@ioc:Adonis/Lucid/Orm';
// } // }
// } // }
/** /**
* Helper to find if value is a valid Object or * Helper to find if value is a valid Object or
* not * not
@ -30,6 +29,8 @@ export default class BaseModel extends LucidBaseModel {
*/ */
// private fillInvoked: boolean = false; // private fillInvoked: boolean = false;
[key: string]: any;
public static fillable: string[] = []; public static fillable: string[] = [];
public fill(attributes: any, allowExtraProperties: boolean = false): this { public fill(attributes: any, allowExtraProperties: boolean = false): this {
@ -117,9 +118,10 @@ export default class BaseModel extends LucidBaseModel {
return this; return this;
} }
} }
// export class DatasetRelatedBaseModel extends LucidBaseModel { // export class DatasetRelatedBaseModel extends LucidBaseModel {
// public dataset: BelongsTo<typeof Dataset>; // public dataset: BelongsTo<typeof Dataset>;
// } // }

View File

@ -1,6 +1,9 @@
import { column, SnakeCaseNamingStrategy, manyToMany, ManyToMany } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy, manyToMany, belongsTo } from '@adonisjs/lucid/orm';
import Dataset from './Dataset'; import Dataset from './dataset.js';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import CollectionRole from './collection_role.js';
import type { ManyToMany } from "@adonisjs/lucid/types/relations";
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
export default class Collection extends BaseModel { export default class Collection extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
@ -43,4 +46,9 @@ export default class Collection extends BaseModel {
pivotTable: 'link_documents_collections', pivotTable: 'link_documents_collections',
}) })
public datasets: ManyToMany<typeof Dataset>; public datasets: ManyToMany<typeof Dataset>;
@belongsTo(() => CollectionRole, {
foreignKey: 'role_id',
})
public collectionRole: BelongsTo<typeof CollectionRole>;
} }

View File

@ -0,0 +1,39 @@
import { column, SnakeCaseNamingStrategy, hasMany } from '@adonisjs/lucid/orm';
import BaseModel from './base_model.js';
import Collection from './collection.js';
import type { HasMany } from "@adonisjs/lucid/types/relations";
export default class CollectionRole extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy();
public static primaryKey = 'id';
public static table = 'collections_roles';
public static fillable: string[] = ['name', 'oai_name', 'visible'];
@column({
isPrimary: true,
})
public id: number;
@column({})
public name: string;
@column({})
public oai_name?: string;
@column({})
public position: number;
@column({})
public visible: boolean;
@column({})
public visible_frontdoor: boolean;
@column({})
public visible_oai: boolean;
@hasMany(() => Collection, {
foreignKey: 'role_id',
})
public collections: HasMany<typeof Collection>;
}

View File

@ -1,7 +1,8 @@
import { column, SnakeCaseNamingStrategy, belongsTo, BelongsTo } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy, belongsTo } from '@adonisjs/lucid/orm';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import Dataset from './Dataset'; import Dataset from './dataset.js';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
export default class Coverage extends BaseModel { export default class Coverage extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();

View File

@ -2,31 +2,31 @@ import {
column, column,
SnakeCaseNamingStrategy, SnakeCaseNamingStrategy,
manyToMany, manyToMany,
ManyToMany,
belongsTo, belongsTo,
BelongsTo,
hasMany, hasMany,
HasMany,
computed, computed,
hasOne, hasOne
HasOne, } from '@adonisjs/lucid/orm';
} from '@ioc:Adonis/Lucid/Orm';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Person from './Person'; import Person from './person.js';
import User from './User'; import User from './user.js';
import Title from './Title'; import Title from './title.js';
import Description from './Description'; import Description from './description.js';
import License from './License'; import License from './license.js';
import Subject from './Subject'; import Subject from './subject.js';
import File from './File'; import File from './file.js';
import Coverage from './Coverage'; import Coverage from './coverage.js';
import DatasetReference from './DatasetReference'; import DatasetReference from './dataset_reference.js';
import Collection from './Collection'; import Collection from './collection.js';
import DatasetIdentifier from './DatasetIdentifier'; import DatasetIdentifier from './dataset_identifier.js';
import Project from './Project'; import Project from './project.js';
import DocumentXmlCache from './DocumentXmlCache'; import DocumentXmlCache from './DocumentXmlCache.js';
import DatasetExtension from 'App/Models/Traits/DatasetExtension'; // Adjust the import path 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";
export default class Dataset extends DatasetExtension { export default class Dataset extends DatasetExtension {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
@ -46,7 +46,12 @@ export default class Dataset extends DatasetExtension {
@column({ columnName: 'creating_corporation' }) @column({ columnName: 'creating_corporation' })
public creating_corporation: string; public creating_corporation: string;
@column.dateTime({ columnName: 'embargo_date' }) @column.dateTime({
columnName: 'embargo_date',
serialize: (value: Date | null) => {
return value ? dayjs(value).format('YYYY-MM-DD') : value;
},
})
public embargo_date: DateTime; public embargo_date: DateTime;
@column({}) @column({})
@ -55,7 +60,7 @@ export default class Dataset extends DatasetExtension {
@column({}) @column({})
public language: string; public language: string;
@column({}) @column({columnName: 'publish_id'})
public publish_id: number | null = null; public publish_id: number | null = null;
@column({}) @column({})
@ -95,7 +100,14 @@ export default class Dataset extends DatasetExtension {
}) })
public created_at: DateTime; public created_at: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true, columnName: 'server_date_modified' }) @column.dateTime({
serialize: (value: Date | null) => {
return value ? dayjs(value).format('MMMM D YYYY HH:mm a') : value;
},
autoCreate: true,
autoUpdate: true,
columnName: 'server_date_modified',
})
public server_date_modified: DateTime; public server_date_modified: DateTime;
@manyToMany(() => Person, { @manyToMany(() => Person, {
@ -188,6 +200,15 @@ export default class Dataset extends DatasetExtension {
return mainTitle ? mainTitle.value : null; return mainTitle ? mainTitle.value : null;
} }
@computed({
serializeAs: 'main_abstract',
})
public get mainAbstract() {
// return `${this.firstName} ${this.lastName}`;
const mainTitle = this.descriptions?.find((desc) => desc.type === 'Abstract');
return mainTitle ? mainTitle.value : null;
}
@manyToMany(() => Person, { @manyToMany(() => Person, {
pivotForeignKey: 'document_id', pivotForeignKey: 'document_id',
pivotRelatedForeignKey: 'person_id', pivotRelatedForeignKey: 'person_id',
@ -203,7 +224,7 @@ export default class Dataset extends DatasetExtension {
pivotForeignKey: 'document_id', pivotForeignKey: 'document_id',
pivotRelatedForeignKey: 'person_id', pivotRelatedForeignKey: 'person_id',
pivotTable: 'link_documents_persons', pivotTable: 'link_documents_persons',
pivotColumns: ['role', 'sort_order', 'allow_email_contact'], pivotColumns: ['role', 'sort_order', 'allow_email_contact', 'contributor_type'],
onQuery(query) { onQuery(query) {
query.wherePivot('role', 'contributor'); query.wherePivot('role', 'contributor');
}, },
@ -214,4 +235,44 @@ export default class Dataset extends DatasetExtension {
foreignKey: 'document_id', foreignKey: 'document_id',
}) })
public xmlCache: HasOne<typeof DocumentXmlCache>; public xmlCache: HasOne<typeof DocumentXmlCache>;
/**
* Get the account that the dataset belongs to
*/
@belongsTo(() => User, {
foreignKey: 'editor_id',
})
public editor: BelongsTo<typeof User>;
@belongsTo(() => User, {
foreignKey: 'reviewer_id',
})
public reviewer: BelongsTo<typeof User>;
static async earliestPublicationDate(): Promise<Dataset | null> {
const serverState = 'published';
const model = await this.query().where('server_state', serverState).orderBy('server_date_published', 'asc').first();
return model || null;
}
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',
})
public get remainingTime() {
const dateFuture = this.server_date_modified.plus({ days: 14 });
if (this.server_state === 'approved') {
const now = DateTime.now();
let duration = dateFuture.diff(now, ['days', 'hours', 'months']).toObject();
return duration.days;
} else {
return 0;
}
}
} }

View File

@ -1,7 +1,8 @@
import { column, SnakeCaseNamingStrategy, belongsTo, BelongsTo } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy, belongsTo } from '@adonisjs/lucid/orm';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import Dataset from './Dataset'; import Dataset from './dataset.js';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
export default class DatasetIdentifier extends BaseModel { export default class DatasetIdentifier extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
@ -20,6 +21,9 @@ export default class DatasetIdentifier extends BaseModel {
@column({}) @column({})
public type: string; public type: string;
@column({})
public status: string;
@column({}) @column({})
public value: string; public value: string;
@ -38,4 +42,9 @@ export default class DatasetIdentifier extends BaseModel {
foreignKey: 'dataset_id', foreignKey: 'dataset_id',
}) })
public dataset: BelongsTo<typeof Dataset>; public dataset: BelongsTo<typeof Dataset>;
// // Specify the relationships to touch when this model is updated
// public static get touches() {
// return ['dataset'];
// }
} }

View File

@ -1,7 +1,8 @@
import { column, SnakeCaseNamingStrategy, belongsTo, BelongsTo } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy, belongsTo } from '@adonisjs/lucid/orm';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import Dataset from './Dataset'; import Dataset from './dataset.js';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
export default class DatasetReference extends BaseModel { export default class DatasetReference extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();

View File

@ -1,6 +1,7 @@
import { column, belongsTo, BelongsTo } from '@ioc:Adonis/Lucid/Orm'; import { column, belongsTo } from '@adonisjs/lucid/orm';
import Dataset from './Dataset'; import Dataset from './dataset.js';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
export default class Description extends BaseModel { export default class Description extends BaseModel {
public static primaryKey = 'id'; public static primaryKey = 'id';
@ -9,6 +10,11 @@ export default class Description extends BaseModel {
public static timestamps = false; public static timestamps = false;
public static fillable: string[] = ['value', 'type', 'language']; public static fillable: string[] = ['value', 'type', 'language'];
@column({
isPrimary: true,
})
public id: number;
@column({}) @column({})
public document_id: number; public document_id: number;

184
app/models/file.ts Normal file
View File

@ -0,0 +1,184 @@
import { DateTime } from 'luxon';
import { column, hasMany, belongsTo, SnakeCaseNamingStrategy, computed } from '@adonisjs/lucid/orm';
import HashValue from './hash_value.js';
import Dataset from './dataset.js';
import BaseModel from './base_model.js';
// import { Buffer } from 'buffer';
import * as fs from 'fs';
import crypto from 'crypto';
// import Drive from '@ioc:Adonis/Core/Drive';
// import Drive from '@adonisjs/drive';
import drive from '#services/drive';
import type { HasMany } from "@adonisjs/lucid/types/relations";
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
// import { TransactionClientContract } from "@adonisjs/lucid/database";
import { TransactionClientContract } from '@adonisjs/lucid/types/database';
export default class File extends BaseModel {
// private readonly _data: Uint8Array;
// private readonly _type: string;
// private readonly _size: number;
public static namingStrategy = new SnakeCaseNamingStrategy();
public static primaryKey = 'id';
public static table = 'document_files';
public static selfAssignPrimaryKey = false;
@column({
isPrimary: true,
})
public id: number;
@column({})
public document_id: number;
@column({})
public pathName: string;
@column()
public label: string;
@column()
public comment: string;
@column()
public mimeType: string;
@column()
public language: string;
@column()
public fileSize: number;
@column()
public visibleInOai: boolean;
@column()
public visibleInFrontdoor: boolean;
@column()
public sortOrder: number;
@column.dateTime({ autoCreate: true })
public createdAt: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime;
// public function dataset()
// {
// return $this->belongsTo(Dataset::class, 'document_id', 'id');
// }
@belongsTo(() => Dataset, {
foreignKey: 'document_id',
})
public dataset: BelongsTo<typeof Dataset>;
@hasMany(() => HashValue, {
foreignKey: 'file_id',
})
public hashvalues: HasMany<typeof HashValue>;
@computed({
serializeAs: 'filePath',
})
public get filePath() {
return `/storage/app/public/${this.pathName}`;
// const mainTitle = this.titles?.find((title) => title.type === 'Main');
// return mainTitle ? mainTitle.value : null;
}
@computed({
serializeAs: 'size',
})
public get size() {
return this.fileSize;
}
@computed({
serializeAs: 'type',
})
public get type() {
return this.mimeType;
}
@computed({
serializeAs: 'name',
})
get name(): string {
return this.label;
}
@computed({
serializeAs: 'lastModified',
})
get lastModified(): number {
return this.updatedAt.toUnixInteger(); //.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
}
readonly webkitRelativePath: string = '';
// @computed({
// serializeAs: 'fileData',
// })
// public get fileData(): string {
// try {
// const fileContent: Buffer = fs.readFileSync(this.filePath);
// // Create a Blob from the file content
// // const blob = new Blob([fileContent], { type: this.type }); // Adjust
// // let fileSrc = URL.createObjectURL(blob);
// // return fileSrc;
// // create a JSON string that contains the data in the property "blob"
// const json = JSON.stringify({ blob: fileContent.toString('base64') });
// return json;
// } catch (err) {
// // console.error(`Error reading file: ${err}`);
// return '';
// }
// }
public async createHashValues(trx?: TransactionClientContract) {
const hashtypes: string[] = ['md5', 'sha512'];
for (const type of hashtypes) {
const hash = new HashValue();
hash.type = type;
const hashString = await this._checksumFile(this.filePath, type); // Assuming getRealHash is a method in the same model
hash.value = hashString;
// https://github.com/adonisjs/core/discussions/1872#discussioncomment-132289
const file: File = this;
if (trx) {
await file.useTransaction(trx).related('hashvalues').save(hash); // Save the hash value to the database
} else {
await file.related('hashvalues').save(hash); // Save the hash value to the database
}
}
}
public async delete() {
if (this.pathName) {
// Delete file from additional storage
await drive.delete(this.pathName);
}
// Call the original delete method of the BaseModel to remove the record from the database
await super.delete();
}
private async _checksumFile(path: string, hashName = 'md5'): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash(hashName);
const stream = fs.createReadStream(path);
stream.on('error', (err) => reject(err));
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
});
}
}

View File

@ -1,9 +1,10 @@
import { column, BaseModel, belongsTo, BelongsTo, SnakeCaseNamingStrategy } from '@ioc:Adonis/Lucid/Orm'; import { column, BaseModel, belongsTo, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import File from './File'; import File from './file.js';
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
export default class HashValue extends BaseModel { export default class HashValue extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
public static primaryKey = 'file_id, type'; // public static primaryKey = 'file_id,type';
public static table = 'file_hashvalues'; public static table = 'file_hashvalues';
// static get primaryKey () { // static get primaryKey () {
@ -20,10 +21,10 @@ export default class HashValue extends BaseModel {
// public id: number; // public id: number;
// Foreign key is still on the same model // Foreign key is still on the same model
@column({}) @column({ isPrimary: true })
public file_id: number; public file_id: number;
@column({}) @column({ isPrimary: true })
public type: string; public type: string;
@column() @column()

View File

@ -1,5 +1,5 @@
import { column, SnakeCaseNamingStrategy } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
// import { DateTime } from 'luxon'; // import { DateTime } from 'luxon';
export default class Language extends BaseModel { export default class Language extends BaseModel {

View File

@ -1,5 +1,5 @@
import { column, SnakeCaseNamingStrategy } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
export default class License extends BaseModel { export default class License extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();

40
app/models/mime_type.ts Normal file
View File

@ -0,0 +1,40 @@
import { column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import BaseModel from './base_model.js';
import { DateTime } from 'luxon';
export default class MimeType extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy();
public static primaryKey = 'id';
public static table = 'mime_types';
public static fillable: string[] = ['name', 'file_extension', 'enabled'];
@column({
isPrimary: true,
})
public id: number;
@column({})
public name: string;
@column({})
public file_extension: string;
@column({})
public enabled: boolean;
@column.dateTime({
autoCreate: true,
})
public created_at: DateTime;
@column.dateTime({
autoCreate: true,
autoUpdate: true,
})
public updated_at: DateTime;
// @hasMany(() => Collection, {
// foreignKey: 'role_id',
// })
// public collections: HasMany<typeof Collection>;
}

View File

@ -1,8 +1,9 @@
import { column, manyToMany, ManyToMany, SnakeCaseNamingStrategy, beforeUpdate, beforeCreate } from '@ioc:Adonis/Lucid/Orm'; import { column, manyToMany, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Role from 'App/Models/Role'; import Role from '#models/role';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import type { ManyToMany } from "@adonisjs/lucid/types/relations";
export default class Permission extends BaseModel { export default class Permission extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
@ -44,12 +45,12 @@ export default class Permission extends BaseModel {
}) })
public updated_at: DateTime; public updated_at: DateTime;
@beforeCreate() // @beforeCreate()
@beforeUpdate() // @beforeUpdate()
public static async resetDate(role) { // public static async resetDate(role) {
role.created_at = this.formatDateTime(role.created_at); // role.created_at = this.formatDateTime(role.created_at);
role.updated_at = this.formatDateTime(role.updated_at); // role.updated_at = this.formatDateTime(role.updated_at);
} // }
// public static boot() { // public static boot() {
// super.boot() // super.boot()
@ -64,22 +65,22 @@ export default class Permission extends BaseModel {
// }) // })
// } // }
private static formatDateTime(datetime) { // private static formatDateTime(datetime) {
let value = new Date(datetime); // let value = new Date(datetime);
return datetime // return datetime
? value.getFullYear() + // ? value.getFullYear() +
'-' + // '-' +
(value.getMonth() + 1) + // (value.getMonth() + 1) +
'-' + // '-' +
value.getDate() + // value.getDate() +
' ' + // ' ' +
value.getHours() + // value.getHours() +
':' + // ':' +
value.getMinutes() + // value.getMinutes() +
':' + // ':' +
value.getSeconds() // value.getSeconds()
: datetime; // : datetime;
} // }
// @belongsTo(() => Role) // @belongsTo(() => Role)
// public role: BelongsTo<typeof Role>; // public role: BelongsTo<typeof Role>;

View File

@ -1,8 +1,9 @@
import { column, SnakeCaseNamingStrategy, computed, manyToMany, ManyToMany } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy, computed, manyToMany } from '@adonisjs/lucid/orm';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Dataset from './Dataset'; import Dataset from './dataset.js';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import type { ManyToMany } from "@adonisjs/lucid/types/relations";
export default class Person extends BaseModel { export default class Person extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
@ -69,6 +70,12 @@ export default class Person extends BaseModel {
return stock; return stock;
} }
@computed()
public get pivot_contributor_type() {
const contributor_type = this.$extras.pivot_contributor_type; //my pivot column name was "stock"
return contributor_type;
}
@manyToMany(() => Dataset, { @manyToMany(() => Dataset, {
pivotForeignKey: 'person_id', pivotForeignKey: 'person_id',
pivotRelatedForeignKey: 'document_id', pivotRelatedForeignKey: 'document_id',

View File

@ -1,6 +1,6 @@
import { column, SnakeCaseNamingStrategy } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
export default class Project extends BaseModel { export default class Project extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();

View File

@ -1,10 +1,11 @@
import { column, SnakeCaseNamingStrategy, manyToMany, ManyToMany, beforeCreate, beforeUpdate } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy, manyToMany, beforeCreate, beforeUpdate } from '@adonisjs/lucid/orm';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
// import moment from 'moment'; // import moment from 'moment';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import User from './User'; import User from './user.js';
import Permission from 'App/Models/Permission'; import Permission from '#models/permission';
import type { ManyToMany } from "@adonisjs/lucid/types/relations";
export default class Role extends BaseModel { export default class Role extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
@ -46,7 +47,7 @@ export default class Role extends BaseModel {
@beforeCreate() @beforeCreate()
@beforeUpdate() @beforeUpdate()
public static async resetDate(role) { public static async resetDate(role: Role) {
role.created_at = this.formatDateTime(role.created_at); role.created_at = this.formatDateTime(role.created_at);
role.updated_at = this.formatDateTime(role.updated_at); role.updated_at = this.formatDateTime(role.updated_at);
} }
@ -64,7 +65,7 @@ export default class Role extends BaseModel {
// }); // });
// } // }
private static formatDateTime(datetime) { private static formatDateTime(datetime: any) {
let value = new Date(datetime); let value = new Date(datetime);
return datetime return datetime
? value.getFullYear() + ? value.getFullYear() +

View File

@ -1,9 +1,10 @@
import { column, SnakeCaseNamingStrategy, manyToMany, ManyToMany, beforeCreate, beforeUpdate } from '@ioc:Adonis/Lucid/Orm'; import { column, SnakeCaseNamingStrategy, manyToMany, computed} from '@adonisjs/lucid/orm';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Dataset from './Dataset'; import Dataset from './dataset.js';
import type { ManyToMany } from "@adonisjs/lucid/types/relations";
export default class Subject extends BaseModel { export default class Subject extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
@ -44,28 +45,33 @@ export default class Subject extends BaseModel {
}) })
public updated_at: DateTime; public updated_at: DateTime;
@beforeCreate() // @beforeCreate()
@beforeUpdate() // @beforeUpdate()
public static async resetDate(role) { // public static async resetDate(role) {
role.created_at = this.formatDateTime(role.created_at); // role.created_at = this.formatDateTime(role.created_at);
role.updated_at = this.formatDateTime(role.updated_at); // role.updated_at = this.formatDateTime(role.updated_at);
} // }
private static formatDateTime(datetime) { // private static formatDateTime(datetime) {
let value = new Date(datetime); // let value = new Date(datetime);
return datetime // return datetime
? value.getFullYear() + // ? value.getFullYear() +
'-' + // '-' +
(value.getMonth() + 1) + // (value.getMonth() + 1) +
'-' + // '-' +
value.getDate() + // value.getDate() +
' ' + // ' ' +
value.getHours() + // value.getHours() +
':' + // ':' +
value.getMinutes() + // value.getMinutes() +
':' + // ':' +
value.getSeconds() // value.getSeconds()
: datetime; // : datetime;
// }
@computed()
public get dataset_count() : number{
const count = this.$extras.datasets_count; //my pivot column name was "stock"
return count;
} }
@manyToMany(() => Dataset, { @manyToMany(() => Dataset, {

View File

@ -1,6 +1,8 @@
import { column, belongsTo, BelongsTo } from '@ioc:Adonis/Lucid/Orm'; import { column, belongsTo } from '@adonisjs/lucid/orm';
import Dataset from './Dataset'; import Dataset from './dataset.js';
import BaseModel from './BaseModel'; import BaseModel from './base_model.js';
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
// import { DatasetRelatedBaseModel } from './BaseModel'; // import { DatasetRelatedBaseModel } from './BaseModel';
export default class Title extends BaseModel { export default class Title extends BaseModel {
@ -10,6 +12,11 @@ export default class Title extends BaseModel {
public static timestamps = false; public static timestamps = false;
public static fillable: string[] = ['value', 'type', 'language']; public static fillable: string[] = ['value', 'type', 'language'];
@column({
isPrimary: true,
})
public id: number;
@column({}) @column({})
public document_id: number; public document_id: number;

65
app/models/totp_secret.ts Normal file
View File

@ -0,0 +1,65 @@
import { column, BaseModel, SnakeCaseNamingStrategy, belongsTo } from '@adonisjs/lucid/orm';
import User from './user.js';
import { DateTime } from 'luxon';
import dayjs from 'dayjs';
// import Encryption from '@ioc:Adonis/Core/Encryption';
import encryption from '@adonisjs/core/services/encryption';
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
export default class TotpSecret extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy();
public static table = 'totp_secrets';
// public static fillable: string[] = ['value', 'label', 'type', 'relation'];
@column({
isPrimary: true,
})
public id: number;
@column({})
public user_id: number;
// @column()
// public twoFactorSecret: string;
@column({
serializeAs: null,
consume: (value: string) => (value ? JSON.parse(encryption.decrypt(value) ?? '{}') : null),
prepare: (value: string) => encryption.encrypt(JSON.stringify(value)),
})
public twoFactorSecret?: string | null;
// serializeAs: null removes the model properties from the serialized output.
@column({
serializeAs: null,
consume: (value: string) => (value ? JSON.parse(encryption.decrypt(value) ?? '[]') : []),
prepare: (value: string[]) => encryption.encrypt(JSON.stringify(value)),
})
public twoFactorRecoveryCodes?: string[] | null;
@column({})
public state: number;
@column.dateTime({
serialize: (value: Date | null) => {
// return value ? moment(value).format('MMMM Do YYYY, HH:mm:ss') : value;
return value ? dayjs(value).format('MMMM D YYYY HH:mm a') : value;
},
autoCreate: true,
})
public created_at: DateTime;
@column.dateTime({
serialize: (value: Date | null) => {
return value ? dayjs(value).format('MMMM D YYYY HH:mm a') : value;
},
autoCreate: true,
autoUpdate: true,
})
public updated_at: DateTime;
@belongsTo(() => User, {
foreignKey: 'user_id',
})
public user: BelongsTo<typeof User>;
}

View File

@ -1,15 +1,15 @@
import Title from 'App/Models/Title'; import Title from '#models/title';
import Description from 'App/Models/Description'; import Description from '#models/description';
import License from 'App/Models/License'; import License from '#models/license';
import Person from 'App/Models/Person'; import Person from '#models/person';
import DatasetReference from 'App/Models/DatasetReference'; import DatasetReference from '#models/dataset_reference';
import DatasetIdentifier from 'App/Models/DatasetIdentifier'; import DatasetIdentifier from '#models/dataset_identifier';
import Subject from 'App/Models/Subject'; import Subject from '#models/subject';
import File from 'App/Models/File'; import File from '#models/file';
import Coverage from 'App/Models/Coverage'; import Coverage from '#models/coverage';
import Collection from 'App/Models/Collection'; import Collection from '#models/collection';
import { BaseModel as LucidBaseModel } from '@ioc:Adonis/Lucid/Orm'; import { BaseModel as LucidBaseModel } from '@adonisjs/lucid/orm';
import Field from 'App/Library/Field'; import Field from '#app/Library/Field';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
// @StaticImplements<LucidModel>() // @StaticImplements<LucidModel>()
@ -30,13 +30,13 @@ export type DatasetRelatedModel =
| typeof DatasetIdentifier | typeof DatasetIdentifier
| typeof File; | typeof File;
export default abstract class DatasetExtension extends LucidBaseModel { export default abstract class DatasetExtension extends LucidBaseModel {
public abstract id; public abstract id: number;
public externalFields: Record<string, any> = this.getExternalFields(); public externalFields: Record<string, any> = this.getExternalFields();
// which fields shoud#t be published // which fields shoud#t be published
protected internalFields: Record<string, any> = {}; protected internalFields: Record<string, any> = {};
protected fields: Record<string, any> = {}; protected fields: Record<string, any> = {};
[key: string]: any;
private getExternalFields(): Record<string, any> { private getExternalFields(): Record<string, any> {
// External fields definition // External fields definition
@ -83,7 +83,7 @@ export default abstract class DatasetExtension extends LucidBaseModel {
sort_order: 'sort_order', sort_order: 'sort_order',
allow_email_contact: 'allow_email_contact', allow_email_contact: 'allow_email_contact',
}, },
relation: 'persons', relation: 'contributors',
fetch: 'eager', fetch: 'eager',
}, },
Reference: { Reference: {
@ -160,7 +160,7 @@ export default abstract class DatasetExtension extends LucidBaseModel {
// // Initialize available date fields and set up date validator // // Initialize available date fields and set up date validator
// // if the particular field is present // // if the particular field is present
let dateFields = new Array<string>('EmbargoDate', 'CreatedAt', 'ServerDatePublished', 'ServerDateDeleted'); let dateFields = new Array<string>('EmbargoDate', 'CreatedAt', 'ServerDateModified', 'ServerDatePublished', 'ServerDateDeleted');
dateFields.forEach((fieldname) => { dateFields.forEach((fieldname) => {
let dateField = this.getField(fieldname); let dateField = this.getField(fieldname);
dateField instanceof Field && dateField.setValueModelClass(DateTime.now()); dateField instanceof Field && dateField.setValueModelClass(DateTime.now());
@ -323,7 +323,7 @@ export default abstract class DatasetExtension extends LucidBaseModel {
private convertColumnToFieldname(columnName: string): string { private convertColumnToFieldname(columnName: string): string {
return columnName return columnName
.split(/[-_]/) .split(/[-_]/)
.map((word) => (word.charAt(0).toUpperCase() + word.slice(1))) .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(''); .join('');
} }
} }

180
app/models/user.ts Normal file
View File

@ -0,0 +1,180 @@
import { DateTime } from 'luxon';
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid';
import { column, manyToMany, hasMany, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import hash from '@adonisjs/core/services/hash';
import Role from './role.js';
import db from '@adonisjs/lucid/services/db';
import config from '@adonisjs/core/services/config';
import Dataset from './dataset.js';
import BaseModel from './base_model.js';
// import Encryption from '@ioc:Adonis/Core/Encryption';
import encryption from '@adonisjs/core/services/encryption';
import { TotpState } from '#contracts/enums';
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';
const AuthFinder = withAuthFinder(() => hash.use('laravel'), {
uids: ['email'],
passwordColumnName: 'password',
});
// import TotpSecret from './TotpSecret';
// export default interface IUser {
// id: number;
// login: string;
// email: string;
// // password: string;
// // createdAt: DateTime;
// // updatedAt: DateTime;
// // async (user): Promise<void>;
// }
// const permissionTable = config.get('rolePermission.permission_table', 'permissions');
// const rolePermissionTable = config.get('rolePermission.role_permission_table', 'role_has_permissions');
// const roleTable = config.get('rolePermission.role_table', 'roles');
// const userRoleTable = config.get('rolePermission.user_role_table', 'link_accounts_roles');
export default class User extends compose(BaseModel, AuthFinder) {
// export default class User extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy();
public static table = 'accounts';
@column({ isPrimary: true })
public id: number;
@column()
public login: string;
@column()
public firstName: string;
@column()
public lastName: string;
@column()
public email: string;
@column({ serializeAs: null })
public password: string;
@column.dateTime({ autoCreate: true })
public createdAt: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime;
// serializeAs: null removes the model properties from the serialized output.
@column({
serializeAs: null,
consume: (value: string) => (value ? JSON.parse(encryption.decrypt(value) ?? '{}') : null),
prepare: (value: string) => encryption.encrypt(JSON.stringify(value)),
})
public twoFactorSecret?: string | null;
// serializeAs: null removes the model properties from the serialized output.
@column({
serializeAs: null,
consume: (value: string) => (value ? JSON.parse(encryption.decrypt(value) ?? '[]') : []),
prepare: (value: string[]) => encryption.encrypt(JSON.stringify(value)),
})
public twoFactorRecoveryCodes?: string[] | null;
@column({})
public state: number;
// @hasOne(() => TotpSecret, {
// foreignKey: 'user_id',
// })
// public totp_secret: HasOne<typeof TotpSecret>;
// @beforeSave()
// public static async hashPassword(user: User) {
// if (user.$dirty.password) {
// user.password = await hash.use('laravel').make(user.password);
// }
// }
public get isTwoFactorEnabled(): boolean {
return Boolean(this?.twoFactorSecret && this.state == TotpState.STATE_ENABLED);
// return Boolean(this.totp_secret?.twoFactorSecret);
}
@manyToMany(() => Role, {
pivotForeignKey: 'account_id',
pivotRelatedForeignKey: 'role_id',
pivotTable: 'link_accounts_roles',
})
public roles: ManyToMany<typeof Role>;
@hasMany(() => Dataset, {
foreignKey: 'account_id',
})
public datasets: HasMany<typeof Dataset>;
@hasMany(() => BackupCode, {
foreignKey: 'user_id',
})
public backupcodes: HasMany<typeof BackupCode>;
public async getBackupCodes(this: User): Promise<BackupCode[]> {
const test = await this.related('backupcodes').query();
// return test.map((role) => role.code);
return test;
}
// https://github.com/adonisjs/core/discussions/1872#discussioncomment-132289
public async getRoles(this: User): Promise<string[]> {
const test = await this.related('roles').query();
return test.map((role) => role.name);
}
public async can(permissionNames: Array<string>): Promise<boolean> {
// const permissions = await this.getPermissions()
// return Acl.check(expression, operand => _.includes(permissions, operand))
const hasPermission = await this.checkHasPermissions(this, permissionNames);
return hasPermission;
}
private async checkHasPermissions(user: User, permissionNames: Array<string>): Promise<boolean> {
const permissionTable = config.get('rolePermission.permission_table', 'permissions');
const rolePermissionTable = config.get('rolePermission.role_permission_table', 'role_has_permissions');
const roleTable = config.get('rolePermission.role_table', 'roles');
const userRoleTable = config.get('rolePermission.user_role_table', 'link_accounts_roles');
let permissionPlaceHolder = '(';
let placeholders = new Array(permissionNames.length).fill('?');
permissionPlaceHolder += placeholders.join(',');
permissionPlaceHolder += ')';
let {
rows: {
0: { permissioncount },
},
} = await db.rawQuery(
'SELECT count("p"."name") as permissionCount FROM ' +
roleTable +
' r INNER JOIN ' +
userRoleTable +
' ur ON ur.role_id=r.id AND "ur"."account_id"=? ' +
' INNER JOIN ' +
rolePermissionTable +
' rp ON rp.role_id=r.id ' +
' INNER JOIN ' +
permissionTable +
' p ON rp.permission_id=p.id AND "p"."name" in ' +
permissionPlaceHolder +
' LIMIT 1',
[user.id, ...permissionNames],
);
return permissioncount > 0;
}
}
// export default User;

View File

@ -1,8 +1,10 @@
import { column, BaseModel, belongsTo, BelongsTo, SnakeCaseNamingStrategy } from '@ioc:Adonis/Lucid/Orm'; import { column, BaseModel, belongsTo, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import User from 'App/Models/User'; import User from '#models/user';
import Role from 'App/Models/Role'; import Role from '#models/role';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
// import moment from 'moment' // import moment from 'moment'
export default class UserRole extends BaseModel { export default class UserRole extends BaseModel {
@ -49,7 +51,7 @@ export default class UserRole extends BaseModel {
}); });
} }
private static formatDateTime(datetime) { private static formatDateTime(datetime: any) {
let value = new Date(datetime); let value = new Date(datetime);
return datetime return datetime
? value.getFullYear() + ? value.getFullYear() +

View File

@ -0,0 +1,128 @@
// import Config from '@ioc:Adonis/Core/Config';
// import config from '@adonisjs/core/services/config'
import env from '#start/env';
import User from '#models/user';
import { generateSecret, verifyToken } from 'node-2fa/dist/index.js';
// import cryptoRandomString from 'crypto-random-string';
import QRCode from 'qrcode';
import crypto from 'crypto';
import { TotpState } from '#contracts/enums';
// npm install node-2fa --save
// npm install crypto-random-string --save
// import { cryptoRandomStringAsync } from 'crypto-random-string/index';
// npm install qrcode --save
// npm i --save-dev @types/qrcode
class TwoFactorAuthProvider {
private issuer: string = env.get('APP_NAME') || 'TethysCloud';
/**
* generateSecret will generate a user-specific 32-character secret.
* Were providing the name of the app and the users email as parameters for the function.
* This secret key will be used to verify whether the token provided by the user during authentication is valid or not.
*
* Return the default global focus trap stack *
* @param {User} user user for the secrect
* @return {string}
*/
public generateSecret(user: User) {
const secret = generateSecret({
name: this.issuer,
account: user.email,
});
return secret.secret;
}
/**
* We also generated recovery codes which can be used in case were unable to retrieve tokens from 2FA applications.
* We assign the user a list of recovery codes and each code can be used only once during the authentication process.
* The recovery codes are random strings generated using the cryptoRandomString library.
*
* Return recovery codes
* @return {string[]}
*/
public generateRecoveryCodes(): string[] {
const recoveryCodeLimit: number = 8;
const codes: string[] = [];
for (let i = 0; i < recoveryCodeLimit; i++) {
const recoveryCode: string = `${this.secureRandomString()}-${this.secureRandomString()}`;
codes.push(recoveryCode);
}
return codes;
}
private secureRandomString() {
// return await cryptoRandomString.async({ length: 10, type: 'hex' });
return this.generateRandomString(10, 'hex');
}
private generateRandomString(length: number, type: 'hex' | 'base64' | 'numeric' = 'hex'): string {
const byteLength = Math.ceil(length * 0.5); // For hex encoding, each byte generates 2 characters
const randomBytes = crypto.randomBytes(byteLength);
switch (type) {
case 'hex':
return randomBytes.toString('hex').slice(0, length);
case 'base64':
return randomBytes.toString('base64').slice(0, length);
case 'numeric':
return randomBytes
.toString('hex')
.replace(/[a-fA-F]/g, '') // Remove non-numeric characters
.slice(0, length);
default:
throw new Error('Invalid type specified');
}
}
// public async generateQrCode(user: User) : Promise<{svg: string; url: string; secret: string; }> {
// const issuer = encodeURIComponent(this.issuer); // 'TethysCloud'
// // const userName = encodeURIComponent(user.email); // 'rrqx9472%40tethys.at'
// const label = `${this.issuer}:${user.email}`;
// const algorithm = encodeURIComponent("SHA256");
// const query = `?secret=${user.twoFactorSecret}&issuer=${issuer}&algorithm=${algorithm}&digits=6`; // '?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud'
// const url = `otpauth://totp/${label}${query}`; // 'otpauth://totp/rrqx9472%40tethys.at?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud'
// const svg = await QRCode.toDataURL(url);
// const secret = user.twoFactorSecret as string;
// return { svg, url, secret };
// }
public async generateQrCode(user: User, twoFactorSecret?: string): Promise<{ svg: string; url: string; secret: string }> {
const issuer = encodeURIComponent(this.issuer); // 'TethysCloud'
// const userName = encodeURIComponent(user.email); // 'rrqx9472%40tethys.at'
const label = `${this.issuer}:${user.email}`;
// const algorithm = encodeURIComponent('SHA256');
const secret = twoFactorSecret ? twoFactorSecret : (user.twoFactorSecret as string);
const query = `?secret=${secret}&issuer=${issuer}&digits=6`; // '?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud'
const url = `otpauth://totp/${label}${query}`; // 'otpauth://totp/rrqx9472%40tethys.at?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud'
const svg = await QRCode.toDataURL(url);
return { svg, url, secret };
}
public async enable(user: User, token: string): Promise<boolean> {
const isValid = verifyToken(user.twoFactorSecret as string, token, 1);
if (!isValid) {
return false;
}
user.state = TotpState.STATE_ENABLED;
if (await user.save()) {
return true;
}
return false;
}
public async validate(user: User, token: string): Promise<boolean> {
const isValid = verifyToken(user.twoFactorSecret as string, token, 1);
if (isValid) {
return true;
}
return false;
}
}
export default new TwoFactorAuthProvider();

View File

@ -0,0 +1,136 @@
import User from '#models/user';
import BackupCode from '#models/backup_code';
import hash from '@adonisjs/core/services/hash';
export interface ISecureRandom {
CHAR_UPPER: string;
CHAR_LOWER: string;
CHAR_DIGITS: string;
CHAR_SYMBOLS: string;
CHAR_ALPHANUMERIC: string;
CHAR_HUMAN_READABLE: string;
/**
* Generate a random string of specified length.
* @param int $length The length of the generated string
* @param string $characters An optional list of characters to use if no character list is
* specified all valid base64 characters are used.
* @return string
* @since 8.0.0
*/
generate(length: number, characters?: string): string;
}
export class SecureRandom implements ISecureRandom {
CHAR_UPPER: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
CHAR_LOWER: string = 'abcdefghijklmnopqrstuvwxyz';
CHAR_DIGITS: string = '0123456789';
CHAR_SYMBOLS: string = '!"#$%&\\\'()*+,-./:;<=>?@[]^_`{|}~';
CHAR_ALPHANUMERIC: string = this.CHAR_UPPER + this.CHAR_LOWER + this.CHAR_DIGITS;
CHAR_HUMAN_READABLE: string = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789';
public generate(length: number, characters: string = this.CHAR_ALPHANUMERIC): string {
if (length <= 0) {
throw new Error('Invalid length specified: ' + length + ' must be bigger than 0');
}
const maxCharIndex: number = characters.length - 1;
let randomString: string = '';
while (length > 0) {
const randomNumber: number = Math.floor(Math.random() * (maxCharIndex + 1));
randomString += characters[randomNumber];
length--;
}
return randomString;
}
}
class BackupCodeStorage {
private static CODE_LENGTH: number = 16;
// private mapper: BackupCodeMapper;
// private hasher: IHasher;
private random: ISecureRandom;
// private eventDispatcher: IEventDispatcher;
// constructor(mapper: BackupCodeMapper, random: ISecureRandom, hasher: IHasher, eventDispatcher: IEventDispatcher) {
// this.mapper = mapper;
// this.hasher = hasher;
// this.random = random;
// this.eventDispatcher = eventDispatcher;
// }
constructor(random: ISecureRandom) {
// this.mapper = mapper;
// this.hasher = hasher;
this.random = random;
// this.eventDispatcher = eventDispatcher;
}
public async createCodes(user: User, number: number = 10): Promise<string[]> {
let results: string[] = [];
// this.mapper.deleteCodes(user);
await BackupCode.deleteCodes(user);
// user.twoFactorRecoveryCodes = [""];
// const uid = user.getUID();
for (let i = 1; i <= Math.min(number, 20); i++) {
const code = this.random.generate(BackupCodeStorage.CODE_LENGTH, this.random.CHAR_HUMAN_READABLE);
// const code = crypto
// .randomBytes(Math.ceil(BackupCodeStorage.CODE_LENGTH / 2))
// .toString('hex')
// .slice(0, BackupCodeStorage.CODE_LENGTH);
const dbCode = new BackupCode();
// dbCode.setUserId(uid);
// dbCode.setCode(this.hasher.hash(code));
dbCode.code = await hash.make(code);
// dbCode.setUsed(0);
dbCode.used = false;
// this.mapper.insert(dbCode);
// await dbCode.save();
await dbCode.related('user').associate(user); // speichert schon ab
results.push(code);
}
// this.eventDispatcher.dispatchTyped(new CodesGenerated(user));
return results;
}
public async hasBackupCodes(user: User): Promise<boolean> {
const codes = await user.getBackupCodes();
return codes.length > 0;
}
public async getBackupCodesState(user: User) {
// const codes = this.mapper.getBackupCodes(user);
// const codes = await user.related('backupcodes').query().exec();
const codes: BackupCode[] = await user.getBackupCodes();
const total = codes.length;
let used: number = 0;
codes.forEach((code) => {
if (code.used === true) {
used++;
}
});
return {
enabled: total > 0,
total: total,
used: used,
};
}
// public validateCode(user: User, code: string): boolean {
// const dbCodes = await user.getBackupCodes();
// for (const dbCode of dbCodes) {
// if (parseInt(dbCode.getUsed()) === 0 && this.hasher.verify(code, dbCode.getCode())) {
// dbCode.setUsed(1);
// this.mapper.update(dbCode);
// return true;
// }
// }
// return false;
// }
// public deleteCodes(user: User): void {
// this.mapper.deleteCodes(user);
// }
}
export default BackupCodeStorage;

13
app/services/drive.ts Normal file
View File

@ -0,0 +1,13 @@
// import app from './app.js';
import DriveManager from "#providers/drive/src/drive_manager";
import app from "@adonisjs/core/services/app";
let drive: DriveManager;
/**
* Returns a singleton instance of the router class from
* the container
*/
await app.booted(async () => {
drive = await app.container.make(DriveManager);
});
export { drive as default };

View File

@ -0,0 +1,26 @@
export function sum(a: number, b: number): number {
return a + b;
}
export function getDomain(host: string): string {
// $myhost = strtolower(trim($host));
let myHost: string = host.trim().toLocaleLowerCase();
// $count = substr_count($myhost, '.');
const count: number = myHost.split(',').length - 1;
if (count == 2) {
const words = myHost.split('.');
if (words[1].length > 3) {
myHost = myHost.split('.', 2)[1];
}
} else if (count > 2) {
myHost = getDomain(myHost.split('.', 2)[1]);
}
myHost = myHost.replace(new RegExp(/^.*:\/\//i, 'g'), '');
return myHost;
}
export function preg_match(regex: RegExp, str: string) {
const result: boolean = regex.test(str);
return result;
}

20
app/validators/auth.ts Normal file
View File

@ -0,0 +1,20 @@
import vine from '@vinejs/vine';
// public schema = schema.create({
// email: schema.string({ trim: true }, [
// rules.email(),
// // rules.unique({ table: 'accounts', column: 'email' })
// ]),
// password: schema.string({}, [rules.minLength(6)]),
// });
/**
* Validates the role's creation action
* node ace make:validator role
*/
export const authValidator = vine.compile(
vine.object({
email: vine.string().maxLength(255).email().normalizeEmail(),
password: vine.string().trim().minLength(6),
}),
);

354
app/validators/dataset.ts Normal file
View File

@ -0,0 +1,354 @@
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
import { TitleTypes, DescriptionTypes, ContributorTypes, ReferenceIdentifierTypes, RelationTypes } from '#contracts/enums';
import dayjs from 'dayjs';
// import MimeType from '#models/mime_type';
// const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec();
// const extensions = enabledExtensions
// .map((extension) => {
// return extension.file_extension.split('|');
// })
// .flat();
/**
* Validates the dataset's creation action
* node ace make:validator dataset
*/
export const createDatasetValidator = vine.compile(
vine.object({
// first step
language: vine
.string()
.trim()
.regex(/^[a-zA-Z0-9]+$/),
licenses: vine.array(vine.number()).minLength(1), // define at least one license for the new dataset
rights: vine.string().in(['true']),
// second step
type: vine.string().trim().minLength(3).maxLength(255),
creating_corporation: vine.string().trim().minLength(3).maxLength(255),
titles: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255),
type: vine.enum(Object.values(TitleTypes)),
language: vine
.string()
.trim()
.minLength(2)
.maxLength(255)
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
.minLength(1),
descriptions: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(2500),
type: vine.enum(Object.values(DescriptionTypes)),
language: vine
.string()
.trim()
.minLength(2)
.maxLength(255)
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
.minLength(1),
authors: vine
.array(
vine.object({
email: vine
.string()
.trim()
.maxLength(255)
.email()
.normalizeEmail()
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
}),
)
.minLength(1)
.distinct('email'),
contributors: vine
.array(
vine.object({
email: vine
.string()
.trim()
.maxLength(255)
.email()
.normalizeEmail()
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
}),
)
.distinct('email')
.optional(),
// third step
project_id: vine.number().optional(),
// embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]),
embargo_date: vine
.date({
formats: ['YYYY-MM-DD'],
})
.afterOrEqual((_field) => {
return dayjs().add(10, 'day').format('YYYY-MM-DD');
})
.optional(),
coverage: vine.object({
x_min: vine.number(),
x_max: vine.number(),
y_min: vine.number(),
y_max: vine.number(),
elevation_absolut: vine.number().positive().optional(),
elevation_min: vine.number().positive().optional().requiredIfExists('elevation_max'),
elevation_max: vine.number().positive().optional().requiredIfExists('elevation_min'),
// type: vine.enum(Object.values(DescriptionTypes)),
depth_absolut: vine.number().negative().optional(),
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
}),
references: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255),
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
relation: vine.enum(Object.values(RelationTypes)),
label: vine.string().trim().minLength(2).maxLength(255),
}),
)
.optional(),
subjects: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255),
// pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
language: vine.string().trim().minLength(2).maxLength(255),
}),
)
.minLength(3)
.distinct('value'),
// last step
files: vine
.array(
vine
.myfile({
size: '512mb',
//extnames: extensions,
})
.allowedMimetypeExtensions()
.filenameLength({ clientNameSizeLimit: 100 })
.fileScan({ removeInfected: true }),
)
.minLength(1),
}),
);
/**
* Validates the dataset's update action
*/
export const updateDatasetValidator = vine.compile(
vine.object({
// first step
language: vine
.string()
.trim()
.regex(/^[a-zA-Z0-9]+$/),
licenses: vine.array(vine.number()).minLength(1), // define at least one license for the new dataset
rights: vine.string().in(['true']),
// second step
type: vine.string().trim().minLength(3).maxLength(255),
creating_corporation: vine.string().trim().minLength(3).maxLength(255),
titles: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255),
type: vine.enum(Object.values(TitleTypes)),
language: vine
.string()
.trim()
.minLength(2)
.maxLength(255)
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
.minLength(1),
descriptions: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(2500),
type: vine.enum(Object.values(DescriptionTypes)),
language: vine
.string()
.trim()
.minLength(2)
.maxLength(255)
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
.minLength(1),
authors: vine
.array(
vine.object({
email: vine
.string()
.trim()
.maxLength(255)
.email()
.normalizeEmail()
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
}),
)
.minLength(1)
.distinct('email'),
contributors: vine
.array(
vine.object({
email: vine
.string()
.trim()
.maxLength(255)
.email()
.normalizeEmail()
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
}),
)
.distinct('email')
.optional(),
// third step
project_id: vine.number().optional(),
// embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]),
embargo_date: vine
.date({
formats: ['YYYY-MM-DD'],
})
.afterOrEqual((_field) => {
return dayjs().add(10, 'day').format('YYYY-MM-DD');
})
.optional(),
coverage: vine.object({
x_min: vine.number(),
x_max: vine.number(),
y_min: vine.number(),
y_max: vine.number(),
elevation_absolut: vine.number().positive().optional(),
elevation_min: vine.number().positive().optional().requiredIfExists('elevation_max'),
elevation_max: vine.number().positive().optional().requiredIfExists('elevation_min'),
// type: vine.enum(Object.values(DescriptionTypes)),
depth_absolut: vine.number().negative().optional(),
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
}),
references: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255),
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
relation: vine.enum(Object.values(RelationTypes)),
label: vine.string().trim().minLength(2).maxLength(255),
}),
)
.optional(),
subjects: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255),
// pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
language: vine.string().trim().minLength(2).maxLength(255),
}),
)
.minLength(3)
.distinct('value'),
// last step
files: vine
.array(
vine
.myfile({
size: '512mb',
//extnames: extensions,
})
.allowedMimetypeExtensions()
.filenameLength({ clientNameSizeLimit: 100 })
.fileScan({ removeInfected: true }),
).dependentArrayMinLength({ dependentArray: 'fileInputs', min: 1}),
fileInputs: vine
.array(
vine
.object({
label: vine.string().trim().maxLength(100),
//extnames: extensions,
})
),
}),
);
// files: schema.array([rules.minLength(1)]).members(
// schema.file({
// size: '512mb',
// extnames: ['jpg', 'gif', 'png', 'tif', 'pdf', 'zip', 'fgb', 'nc', 'qml', 'ovr', 'gpkg', 'gml', 'gpx', 'kml', 'kmz', 'json'],
// }),
// ),
let messagesProvider = new SimpleMessagesProvider({
'minLength': '{{ field }} must be at least {{ min }} characters long',
'maxLength': '{{ field }} must be less then {{ max }} characters long',
'required': '{{ field }} is required',
'unique': '{{ field }} must be unique, and this value is already taken',
// 'confirmed': '{{ field }} is not correct',
'licenses.minLength': 'at least {{ min }} permission must be defined',
'licenses.*.number': 'Define roles as valid numbers',
'rights.in': 'you must agree to continue',
'titles.0.value.minLength': 'Main Title must be at least {{ min }} characters long',
'titles.0.value.maxLength': 'Main Title must be less than {{ max }} characters long',
'titles.0.value.required': 'Main Title is required',
'titles.*.value.required': 'Additional title is required, if defined',
'titles.*.type.required': 'Additional title type is required',
'titles.*.language.required': 'Additional title language is required',
'titles.*.language.translatedLanguage': 'The language of the translated title must be different from the language of the dataset',
'descriptions.0.value.minLength': 'Main Abstract must be at least {{ min }} characters long',
'descriptions.0.value.maxLength': 'Main Abstract must be less than {{ max }} characters long',
'descriptions.0.value.required': 'Main Abstract is required',
'descriptions.*.value.required': 'Additional description is required, if defined',
'descriptions.*.type.required': 'Additional description type is required',
'descriptions.*.language.required': 'Additional description language is required',
'descriptions.*.language.translatedLanguage':
'The language of the translated description must be different from the language of the dataset',
'authors.array.minLength': 'at least {{ min }} author must be defined',
'authors.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.',
'authors.*.email.isUnique': 'the email of the new creator already exists in the database',
'contributors.*.pivot_contributor_type.required': 'contributor type is required, if defined',
'contributors.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.',
'after': `{{ field }} must be older than ${dayjs().add(10, 'day')}`,
'subjects.array.minLength': 'at least {{ min }} keywords must be defined',
'subjects.*.value.required': 'keyword value is required',
'subjects.*.value.minLength': 'keyword value must be at least {{ min }} characters long',
'subjects.*.type.required': 'keyword type is required',
'subjects.*.language.required': 'language of keyword is required',
'subjects.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.',
'references.*.value.required': 'Additional reference value is required, if defined',
'references.*.type.required': 'Additional reference identifier type is required',
'references.*.relation.required': 'Additional reference relation type is required',
'references.*.label.required': 'Additional reference label is required',
'files.array.minLength': 'At least {{ min }} file upload is required.',
'files.*.size': 'file size is to big',
'files.*.extnames': 'file extension is not supported',
});
createDatasetValidator.messagesProvider = messagesProvider;
updateDatasetValidator.messagesProvider = messagesProvider;
// export default createDatasetValidator;

64
app/validators/role.ts Normal file
View File

@ -0,0 +1,64 @@
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
/**
* Validates the role's creation action
* node ace make:validator role
*/
export const createRoleValidator = vine.compile(
vine.object({
name: vine
.string()
.isUnique({ table: 'roles', column: 'name' })
.trim()
.minLength(3)
.maxLength(255)
.regex(/^[a-zA-Z0-9]+$/), //Must be alphanumeric with hyphens or underscores
display_name: vine
.string()
.isUnique({ table: 'roles', column: 'display_name' })
.trim()
.minLength(3)
.maxLength(255)
.regex(/^[a-zA-Z0-9]+$/),
description: vine.string().trim().escape().minLength(3).maxLength(255).optional(),
permissions: vine.array(vine.number()).minLength(1), // define at least one permission for the new role
}),
);
export const updateRoleValidator = vine.withMetaData<{ roleId: number }>().compile(
vine.object({
name: vine
.string()
// .unique(async (db, value, field) => {
// const result = await db.from('roles').select('id').whereNot('id', field.meta.roleId).where('name', value).first();
// return result.length ? false : true;
// })
.isUnique({
table: 'roles',
column: 'name',
whereNot: (field) => field.meta.roleId,
})
.trim()
.minLength(3)
.maxLength(255),
description: vine.string().trim().escape().minLength(3).maxLength(255).optional(),
permissions: vine.array(vine.number()).minLength(1), // define at least one permission for the new role
}),
);
let messagesProvider = new SimpleMessagesProvider({
// Applicable for all fields
'required': 'The {{ field }} field is required',
'unique': '{{ field }} must be unique, and this value is already taken',
'string': 'The value of {{ field }} field must be a string',
'email': 'The value is not a valid email address',
// 'contacts.0.email.required': 'The primary email of the contact is required',
// 'contacts.*.email.required': 'Contact email is required',
'permissions.minLength': 'at least {{min }} permission must be defined',
'permissions.*.number': 'Define permissions as valid numbers',
});
createRoleValidator.messagesProvider = messagesProvider;
updateRoleValidator.messagesProvider = messagesProvider;

64
app/validators/user.ts Normal file
View File

@ -0,0 +1,64 @@
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
/**
* Validates the role's creation action
* node ace make:validator user
*/
export const createUserValidator = vine.compile(
vine.object({
login: vine
.string()
.trim()
.minLength(3)
.maxLength(20)
.isUnique({ table: 'accounts', column: 'login' })
.regex(/^[a-zA-Z0-9]+$/), //Must be alphanumeric with hyphens or underscores
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
email: vine.string().maxLength(255).email().normalizeEmail().isUnique({ table: 'accounts', column: 'email' }),
password: vine.string().confirmed().trim().minLength(3).maxLength(60),
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
}),
);
/**
* Validates the role's update action
* node ace make:validator user
*/
export const updateUserValidator = vine.withMetaData<{ objId: number }>().compile(
vine.object({
login: vine
.string()
.trim()
.minLength(3)
.maxLength(20)
.isUnique({ table: 'accounts', column: 'login', whereNot: (field) => field.meta.objId }),
// .regex(/^[a-zA-Z0-9]+$/), //Must be alphanumeric with hyphens or underscores
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
email: vine
.string()
.maxLength(255)
.email()
.normalizeEmail()
.isUnique({ table: 'accounts', column: 'email', whereNot: (field) => field.meta.objId }),
password: vine.string().confirmed().trim().minLength(3).maxLength(60).optional(),
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
}),
);
let messagesProvider = new SimpleMessagesProvider({
// Applicable for all fields
'required': 'The {{ field }} field is required',
'unique': '{{ field }} must be unique, and this value is already taken',
'string': 'The value of {{ field }} field must be a string',
'email': 'The value is not a valid email address',
'minLength': '{{ field }} must be at least {{ min }} characters long',
'maxLength': '{{ field }} must be less then {{ max }} characters long',
'confirmed': 'Oops! The confirmation of {{ field }} is not correct. Please double-check and ensure they match.',
'roles.minLength': 'at least {{ min }} role must be defined',
'roles.*.number': 'Define roles as valid numbers',
});
createUserValidator.messagesProvider = messagesProvider;
updateUserValidator.messagesProvider = messagesProvider;

Some files were not shown because too many files have changed in this diff Show More