diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6f313c6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ac941f3 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +APP_NAME="Lost & Found Backend" +APP_ENV=local +APP_KEY= +APP_DEBUG=false +APP_URL=http://localhost +APP_TIMEZONE=Europe/Berlin + +LOG_CHANNEL=stack +LOG_SLACK_WEBHOOK_URL= + +DB_CONNECTION=mysql +DB_HOST=dbserver +DB_PORT=3306 +DB_DATABASE=lostfound +DB_USERNAME=lostfound +DB_PASSWORD=lostfound + +CACHE_DRIVER=file +QUEUE_CONNECTION=sync diff --git a/.forgejo/issue_template/bug.yml b/.forgejo/issue_template/bug.yml deleted file mode 100644 index 8b227a0..0000000 --- a/.forgejo/issue_template/bug.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Bug Report -about: File a bug report -labels: - - Kind/Bug -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this bug report! - - type: textarea - id: what-happened - attributes: - label: What happened? - description: Also tell us, what did you expect to happen? - placeholder: Tell us what you see! - validations: - required: true - - type: dropdown - id: browsers - attributes: - label: What browsers are you seeing the problem on? - multiple: true - options: - - Firefox (Windows) - - Firefox (MacOS) - - Firefox (Linux) - - Firefox (Android) - - Firefox (iOS) - - Chrome (Windows) - - Chrome (MacOS) - - Chrome (Linux) - - Chrome (Android) - - Chrome (iOS) - - Safari - - Microsoft Edge \ No newline at end of file diff --git a/.forgejo/issue_template/feature.yml b/.forgejo/issue_template/feature.yml deleted file mode 100644 index c8cf794..0000000 --- a/.forgejo/issue_template/feature.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: 'New Feature' -about: 'This template is for new features' -labels: - - Kind/Feature -body: - - type: markdown - attributes: - value: | - Before creating a Feature Ticket, please check for duplicates. - - type: markdown - attributes: - value: | - ### Implementation Checklist - - [ ] concept - - [ ] frontend - - [ ] backend - - [ ] unittests - - [ ] tested on staging - visible: [ content ] - - type: textarea - id: description - attributes: - label: 'Feature Description' - description: 'Explain the the feature.' - placeholder: Description - validations: - required: true \ No newline at end of file diff --git a/.forgejo/workflows/deploy_staging.yml b/.forgejo/workflows/deploy_staging.yml deleted file mode 100644 index 4c5fcd0..0000000 --- a/.forgejo/workflows/deploy_staging.yml +++ /dev/null @@ -1,60 +0,0 @@ -on: - push: - branches: - - testing - -jobs: - test: - runs-on: docker - container: - image: ghcr.io/catthehacker/ubuntu:act-22.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache-dependency-path: '**/requirements.dev.txt' - - name: Install dependencies - working-directory: core - run: pip3 install -r requirements.dev.txt - - name: Run django tests - working-directory: core - run: python3 manage.py test - - deploy: - needs: [test] - runs-on: docker - steps: - - uses: actions/checkout@v4 - - name: Install ansible - run: | - apt update -y - apt install python3-pip -y - python3 -m pip install ansible - python3 -m pip install ansible-lint - - - name: Populate relevant files - run: | - mkdir -p ~/.ssh - echo "${{ secrets.C3LF_SSH_TESTING }}" > ~/.ssh/id_ed25519 - chmod 0600 ~/.ssh/id_ed25519 - ls -lah ~/.ssh - command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y ) - eval $(ssh-agent -s) - ssh-add ~/.ssh/id_ed25519 - echo "andromeda.lab.or.it ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDXPoO0PE+B9PYwbGaLo98zhbmjAkp6eBtVeZe43v/+T" >> ~/.ssh/known_hosts - mkdir -p /etc/ansible - echo "${{ secrets.C3LF_INVENTORY_TESTING }}" > /etc/ansible/hosts - - - name: Check ansible version - run: | - ansible --version - - - name: List ansible hosts - run: | - ansible -m ping Andromeda - - - name: Deploy testing - run: | - cd deploy/ansible - ansible-playbook playbooks/deploy-c3lf-sys3.yml diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml deleted file mode 100644 index 480e590..0000000 --- a/.forgejo/workflows/test.yml +++ /dev/null @@ -1,21 +0,0 @@ -on: - pull_request: - push: - -jobs: - test: - runs-on: docker - container: - image: ghcr.io/catthehacker/ubuntu:act-22.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache-dependency-path: '**/requirements.dev.txt' - - name: Install dependencies - working-directory: core - run: pip3 install -r requirements.dev.txt - - name: Run django tests - working-directory: core - run: python3 manage.py test diff --git a/.gitignore b/.gitignore index 9f86c99..159c028 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,15 @@ - +/vendor /.idea +Homestead.json +Homestead.yaml .env .local +/public/docs +/public/staticimages +/public/thumbnails +/resources/docs -staticfiles/ -userfiles/ -*.db \ No newline at end of file +composer.lock +composer.phar + +.phpunit.result.cache diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..62d2991 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,9 @@ +php: + preset: laravel + enabled: + - alpha_ordered_imports + disabled: + - length_ordered_imports + - unused_use +js: true +css: true diff --git a/README.md b/README.md deleted file mode 100644 index 3581cac..0000000 --- a/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# C3LF System3 - -the third try to automate lost&found organization for chaos events. not a complete rewrite, but instead building on top -of the web frontend of version 2. everything else is new but still API compatible. now with more monorepo. - -## Architecture - -C3LF System3 integrates a Django-Rest-Framework + WebSocket backend, Vue.js frontend SPA and a minimal LMTP mail server -integrated with the Django backend. It is additionally deployed with a Postfix mail server as Proxy in front of the -LMTP socket, a MariaDB database, a Redis cache and an Nginx reverse proxy that serves the static SPA frontend, proxies -the API requests to the backend and serves the media files in cooperation with the Django backend using the -`X-Accel-Redirect` header. - -The production deployment is automated using Ansible and there are some Docker Compose configurations for development. - -## Project Structure - -- `core/` Contains the Django backend with database models, API endpoints, migrations, API tests, and mail server - functionalities. -- `web/` Contains the Vue.js frontend application. -- `deploy/` Contains deployment configurations and Docker scripts for various development modes. - -For more information, see the README.md files in the respective directories. - -## Development Modes - -There are currently 4 development modes for this Project: - -- Frontend-Only -- Backend-API-Only -- Full-Stack-Lite 'dev' (docker) -- **[WIP]** Full-Stack 'testing' (docker) - -*Choose the one that is most suited to the feature you want to work on or ist easiest for you to set up ;)* - -For all modes it is assumed that you have `git` installed, have cloned the repository and are in the root directory of -the project. Use `git clone https://git.hannover.ccc.de/c3lf/c3lf-system-3.git` to get the official upstream repository. -The required packages for each mode are listed separately and also state the specific package name for Debian 12. - -### Frontend-Only - -This mode is for developing the frontend only. It uses the vue-cli-service (webpack) to serve the frontend and watches -for changes in the source code to provide hot reloading. The API requests are proxied to the staging backend. - -#### Requirements - -* Node.js (~20.19.0) (`nodejs`) -* npm (~9.2.0) (`npm`) - -*Note: The versions are not strict, but these are tested. Other versions might work as well.* - -#### Steps - -```bash -cd web -npm intall -npm run serve -``` - -Now you can access the frontend at `localhost:8080` and start editing the code in the `web` directory. -For more information, see the README.md file in the `web` directory. - -### Backend-API-Only - -This mode is for developing the backend API only. It also specifically excludes most WebSockets and mail server -functionalities. Use this mode to focus on the backend API and Database models. - -#### Requirements - -* Python (~3.11) (`python3`) -* pip (`python3-pip`) -* virtualenv (`python3-venv`) - -*Note: The versions are not strict, but these are tested. Other versions might work as well.* - -#### Steps - -``` -python -m venv venv -source venv/bin/activate -pip install -r core/requirements.dev.txt -cd core -python manage.py test -``` - -The tests should run successfully to start and you can now start the TDD workflow by adding new failing tests. -For more information about the backend and TDD, see the README.md file in the `core` directory. - -### Full-Stack-Lite 'dev' (docker) - -This mode is for developing the both frontend and backend backend at the same time in a containerized environment. It -uses the `docker-compose` command to build and run the application in a container. It specifically excludes all mail -server and most WebSocket functionalities. - -#### Requirements - -* Docker (`docker.io`) -* Docker Compose (`docker-compose`) - -*Note: Depending on your system, the `docker compose` command might be included in general `docker` or `docker-ce` -package, or you might want to use podman instead.* - -#### Steps - -```bash -docker-compose -f deploy/dev/docker-compose.yml up --build -``` - -The page should be available at [localhost:8080](http://localhost:8080) -This Mode provides a minimal set of testdata, including a user `testuser` with password `testuser`. The test dataset is -defined in deploy/testdata.py and can be extended there. - -You can now edit code in `/web` and `/core` and changes will be applied to the running page as soon as the file is -saved. - -For details about each part, read `/web/README.md` and `/core/README.md` respectively. To execute commands in the -container context use 'exec' like - -```bash -docker exec -it c3lf-sys3-dev-core-1 ./manage.py test` -``` - -### Full-Stack 'testing' (docker) - -**WORK IN PROGRESS** - -*will include postfix, mariadb, redis, nginx and the ability to test sending mails, receiving mail and websocket based -realiteme updates in the frontend. the last step in verification before deploying to the staging system using ansible* - -## Online Instances - -These are deployed using `deploy/ansible/playbooks/deploy-c3lf-sys3.yml` and follow a specific git branch. - -### 'live' - -| URL | [c3lf.de](https://c3lf.de) | -|----------------|----------------------------| -| **Branch** | live | -| **Host** | polaris.lab.or.it | -| **Debug Mode** | off | - -This is the **'production' system** and should strictly follow the staging system after all changes have been validated. - -### 'staging' - -| URL | [staging.c3lf.de](https://staging.c3lf.de) | -|----------------|--------------------------------------------| -| **Branch** | testing | -| **Host** | andromeda.lab.or.it | -| **Debug Mode** | on | - -This system ist automatically updated by [git.hannover.ccc.de](https://git.hannover.ccc.de/c3lf/c3lf-system-3/) whenever -a commit is pushed to the 'testing' branch and the backend tests passed. - -**WARNING: allthough this is the staging system, it is fully functional and contains a copy of the 'production' data, so -do not for example reply to tickets for testing purposes as the system WILL SEND AN EMAIL to the person who originally -created it. If you want to test something like that, first create you own test ticket by sending an email to -`@staging.c3lf.de`** \ No newline at end of file diff --git a/core/.local/.forgit_fordocker b/app/Console/Commands/.gitkeep similarity index 100% rename from core/.local/.forgit_fordocker rename to app/Console/Commands/.gitkeep diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 0000000..ad6e311 --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,29 @@ +select('containers.cid', 'name', DB::raw('count(items.iid) as itemCount')) + ->groupBy('containers.cid', 'name')->get(); + } + + static function find($id){ + return Container::leftJoin('items','items.cid','=','containers.cid') + ->select('containers.cid', 'name', DB::raw('count(items.iid) as itemCount')) + ->groupBy('containers.cid', 'name')->where(Container::primaryKey, '=', $id)->first(); + } +} diff --git a/app/Event.php b/app/Event.php new file mode 100644 index 0000000..2f99288 --- /dev/null +++ b/app/Event.php @@ -0,0 +1,27 @@ +create($attributes); + } + + + + +} diff --git a/app/Http/Controllers/ContainerController.php b/app/Http/Controllers/ContainerController.php new file mode 100644 index 0000000..7982612 --- /dev/null +++ b/app/Http/Controllers/ContainerController.php @@ -0,0 +1,46 @@ +json(Container::all()); + } + + public function showOneContainer($id) + { + return response()->json(Container::find($id)); + } + + public function create(Request $request) + { + $container = Container::create($request->all()); + + return response()->json($container, 201); + } + + public function update($id, Request $request) + { + $container = Container::findOrFail($id); + $container->update($request->all()); + + return response()->json($container, 200); + } + + public function delete($id) + { + Container::findOrFail($id)->delete(); + return response('Deleted Successfully', 200); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..0ccb918 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,10 @@ +json(Event::all()); + } + + public function showOneEvent($id) + { + return response()->json(Event::find($id)); + } + + public function create(Request $request) + { + $event = Event::create($request->all()); + + return response()->json($event, 201); + } + + public function update($id, Request $request) + { + $event = Event::findOrFail($id); + $event->update($request->all()); + + return response()->json($event, 200); + } + + public function delete($id) + { + Event::findOrFail($id)->delete(); + return response('Deleted Successfully', 200); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php new file mode 100644 index 0000000..61840db --- /dev/null +++ b/app/Http/Controllers/FileController.php @@ -0,0 +1,32 @@ +json(File::all()); + } + + public function showOneFile($id) + { + return response()->json(File::find($id)); + } + + public function create(Request $request) + { + $file = File::create($request->only(['data','iid'])); + return response()->json($file, 201); + } + + public function delete($id) + { + File::findOrFail($id)->delete(); + return response('Deleted Successfully', 200); + } +} diff --git a/app/Http/Controllers/ItemController.php b/app/Http/Controllers/ItemController.php new file mode 100644 index 0000000..1c94846 --- /dev/null +++ b/app/Http/Controllers/ItemController.php @@ -0,0 +1,83 @@ +json(Item::all()); + } + + public function showByEvent($event) + { + $eid = Event::where('slug','=',$event)->first()->eid; + $q = Item::byEvent($eid); + return response()->json($q->get()); + } + + public function searchByEvent($event, $query) + { + $eid = Event::where('slug','=',$event)->first()->eid; + $query_tokens = explode(" ",base64_decode ( $query , true)); + $q = Item::byEvent($eid); + foreach ($query_tokens as $token) + if(!empty($token)) + $q = $q->where('items.description','like','%'.$token.'%'); + return response()->json($q->get()); + } + + public function showOneItem($event, $id) + { + $eid = Event::where('slug','=',$event)->first()->eid; + return response()->json(Item::byEvent($eid)->where('uid', '=', $id)->first()); + } + + public function create($event, Request $request) + { + $eid = Event::where('slug','=',$event)->first()->eid; + $newitem = $request->except(['dataImage']); + $newitem['eid'] = "".$eid; + $item = Item::create($newitem); + + if($request->get('dataImage')) { + $file = File::create(array('data' => $request->get('dataImage'), 'iid' => $item['iid'])); + $item['file'] = $file['hash']; + } + + return response()->json($item, 201); + } + + public function update($event, $id, Request $request) + { + $eid = Event::where('slug', $event)->first()->eid; + $item = Item::where('eid', $eid)->where('uid', $id)->first(); + $item->update($request->except(['file', 'box', 'dataImage'])); + + if($request->get('returned')===true){ + $item->update(['returned_at' => DB::raw( 'current_timestamp' )]); + } + + if($request->get('dataImage')) { + $file = File::create(array('data' => $request->get('dataImage'), 'iid' => $item['iid'])); + $item['file'] = $file['hash']; + } + + return response()->json(Item::find($item['uid']), 200); + } + + public function delete($event, $id) + { + $eid = Event::where('slug','=',$event)->first()->eid; + Item::where('eid', $eid)->where('uid', $id)->first()->delete(); + return response()->json(array("status"=>'Deleted Successfully'), 200); + } +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php new file mode 100644 index 0000000..361a11e --- /dev/null +++ b/app/Http/Middleware/Authenticate.php @@ -0,0 +1,44 @@ +auth = $auth; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param string|null $guard + * @return mixed + */ + public function handle($request, Closure $next, $guard = null) + { + if ($this->auth->guard($guard)->guest()) { + return response('Unauthorized.', 401); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/ExampleMiddleware.php b/app/Http/Middleware/ExampleMiddleware.php new file mode 100644 index 0000000..166581c --- /dev/null +++ b/app/Http/Middleware/ExampleMiddleware.php @@ -0,0 +1,20 @@ +withTrashed()->where('eid',$attributes['eid'])->max('uid') + 1; + $attributes['uid'] = $uid; + $item = static::query()->create($attributes); + return Item::find($item->iid); + } + + protected static function extended($columns=Array()){ + return Item::whereNull('returned_at') + ->join('containers','items.cid','=','containers.cid') + ->leftJoin('currentfiles','items.iid','=','currentfiles.iid'); + } + + static function byEvent($eid){ + return Item::extended()->where('eid','=',$eid) + ->select('items.*','currentfiles.hash as file', 'containers.name as box'); + } + + static function all($columns=Array()){ + return Item::extended($columns) + ->select('items.*','currentfiles.hash as file', 'containers.name as box') + ->get(); + } + + static function find($id){ + return Item::extended() + ->select('items.*','currentfiles.hash as file', 'containers.name as box') + ->where('items.iid', '=', $id)->first(); + } +} diff --git a/app/Jobs/ExampleJob.php b/app/Jobs/ExampleJob.php new file mode 100644 index 0000000..7b65bb4 --- /dev/null +++ b/app/Jobs/ExampleJob.php @@ -0,0 +1,26 @@ +app['auth']->viaRequest('api', function ($request) { + if ($request->input('api_token')) { + return User::where('api_token', $request->input('api_token'))->first(); + } + }); + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php new file mode 100644 index 0000000..a3d284f --- /dev/null +++ b/app/Providers/EventServiceProvider.php @@ -0,0 +1,19 @@ + [ + 'App\Listeners\ExampleListener', + ], + ]; +} diff --git a/app/User.php b/app/User.php new file mode 100644 index 0000000..0284e3d --- /dev/null +++ b/app/User.php @@ -0,0 +1,32 @@ +make( + 'Illuminate\Contracts\Console\Kernel' +); + +exit($kernel->handle(new ArgvInput, new ConsoleOutput)); diff --git a/backup.sh b/backup.sh new file mode 100644 index 0000000..bb4b4ad --- /dev/null +++ b/backup.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +#backup.sh /var/www/lfbackend OPTION +#OPTION +# - F FULL BACKUP +# - T Only save images from today +# - I Incremental Backup (Not implemented yet) +# (OPTIONS T and I only apply for images) + +#CRON +#17 * * * * root cd /tmp && /var/www/lfbackend/backup.sh /var/www/lfbackend T +#45 5 * * * root cd /tmp && /var/www/lfbackend/backup.sh /var/www/lfbackend F + +OPTION=$2 + +source $1/.env + +TS=`date +%Y%m%d%H%M%S` + +mysqldump -u $DB_USERNAME -p$DB_PASSWORD -h $DB_HOST $DB_DATABASE > database.sql +if [ "$OPTION" == "T" ] +then + tar -N "`date +%Y-%m-%d`" -zcvf images.tar.gz -C $1/public/staticimages/ . +elif [ "$OPTION" == "I" ] +then + tar -zcvf images.tar.gz -C $1/public/staticimages/ . +else + tar -zcvf images.tar.gz -C $1/public/staticimages/ . +fi + +gzip -f database.sql +tar -cvf backup_${TS}_${OPTION}.tar database.sql.gz images.tar.gz + diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..48de537 --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,102 @@ +bootstrap(); + +/* +|-------------------------------------------------------------------------- +| Create The Application +|-------------------------------------------------------------------------- +| +| Here we will load the environment and create the application instance +| that serves as the central piece of this framework. We'll use this +| application as an "IoC" container and router for this framework. +| +*/ + +$app = new Laravel\Lumen\Application( + dirname(__DIR__) +); + +$app->withFacades(); + +$app->withEloquent(); + +/* +|-------------------------------------------------------------------------- +| Register Container Bindings +|-------------------------------------------------------------------------- +| +| Now we will register a few bindings in the service container. We will +| register the exception handler and the console kernel. You may add +| your own bindings here if you like or you can make another file. +| +*/ + +$app->singleton( + Illuminate\Contracts\Debug\ExceptionHandler::class, + App\Exceptions\Handler::class +); + +$app->singleton( + Illuminate\Contracts\Console\Kernel::class, + App\Console\Kernel::class +); + +/* +|-------------------------------------------------------------------------- +| Register Middleware +|-------------------------------------------------------------------------- +| +| Next, we will register the middleware with the application. These can +| be global middleware that run before and after each request into a +| route or middleware that'll be assigned to some specific routes. +| +*/ + +// $app->middleware([ +// App\Http\Middleware\ExampleMiddleware::class +// ]); + +// $app->routeMiddleware([ +// 'auth' => App\Http\Middleware\Authenticate::class, +// ]); + +/* +|-------------------------------------------------------------------------- +| Register Service Providers +|-------------------------------------------------------------------------- +| +| Here we will register all of the application's service providers which +| are used to bind services into the container. Service providers are +| totally optional, so you are not required to uncomment this line. +| +*/ + +// $app->register(App\Providers\AppServiceProvider::class); +// $app->register(App\Providers\AuthServiceProvider::class); +// $app->register(App\Providers\EventServiceProvider::class); +$app->register(\Mpociot\ApiDoc\ApiDocGeneratorServiceProvider::class); +$app->configure('apidoc'); + +/* +|-------------------------------------------------------------------------- +| Load The Application Routes +|-------------------------------------------------------------------------- +| +| Next we will include the routes file so that they can all be added to +| the application. This will provide all of the URLs the application +| can respond to, as well as the controllers that may handle them. +| +*/ + +$app->router->group([ + 'namespace' => 'App\Http\Controllers', +], function ($router) { + require __DIR__.'/../routes/web.php'; +}); + +return $app; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4ee5c32 --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "laravel/lumen", + "description": "The Laravel Lumen Framework.", + "keywords": ["framework", "laravel", "lumen"], + "license": "MIT", + "type": "project", + "require": { + "php": "^7.2", + "doctrine/dbal": "^2.10", + "laravel/lumen-framework": "^6.0", + "mpociot/laravel-apidoc-generator": "^4.0" + }, + "require-dev": { + "fzaninotto/faker": "^1.4", + "phpunit/phpunit": "^8.0", + "mockery/mockery": "^1.0" + }, + "autoload": { + "classmap": [ + "database/seeds", + "database/factories" + ], + "psr-4": { + "App\\": "app/" + } + }, + "autoload-dev": { + "classmap": [ + "tests/" + ] + }, + "scripts": { + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ] + }, + "config": { + "preferred-install": "dist", + "sort-packages": true, + "optimize-autoloader": true + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/apidoc.php b/config/apidoc.php new file mode 100644 index 0000000..cb46b7f --- /dev/null +++ b/config/apidoc.php @@ -0,0 +1,245 @@ + 'static', + + /* + * The router to be used (Laravel or Dingo). + */ + 'router' => 'laravel', + + /* + * The base URL to be used in examples and the Postman collection. + * By default, this will be the value of config('app.url'). + */ + 'base_url' => null, + + /* + * Generate a Postman collection in addition to HTML docs. + * For 'static' docs, the collection will be generated to public/docs/collection.json. + * For 'laravel' docs, it will be generated to storage/app/apidoc/collection.json. + * The `ApiDoc::routes()` helper will add routes for both the HTML and the Postman collection. + */ + 'postman' => [ + /* + * Specify whether the Postman collection should be generated. + */ + 'enabled' => true, + + /* + * The name for the exported Postman collection. Default: config('app.name')." API" + */ + 'name' => null, + + /* + * The description for the exported Postman collection. + */ + 'description' => null, + ], + + /* + * The routes for which documentation should be generated. + * Each group contains rules defining which routes should be included ('match', 'include' and 'exclude' sections) + * and rules which should be applied to them ('apply' section). + */ + 'routes' => [ + [ + /* + * Specify conditions to determine what routes will be parsed in this group. + * A route must fulfill ALL conditions to pass. + */ + 'match' => [ + + /* + * Match only routes whose domains match this pattern (use * as a wildcard to match any characters). + */ + 'domains' => [ + '*', + // 'domain1.*', + ], + + /* + * Match only routes whose paths match this pattern (use * as a wildcard to match any characters). + */ + 'prefixes' => [ + '*', + // 'users/*', + ], + + /* + * Match only routes registered under this version. This option is ignored for Laravel router. + * Note that wildcards are not supported. + */ + 'versions' => [ + 'v1', + ], + ], + + /* + * Include these routes when generating documentation, + * even if they did not match the rules above. + * Note that the route must be referenced by name here (wildcards are supported). + */ + 'include' => [ + // 'users.index', 'healthcheck*' + ], + + /* + * Exclude these routes when generating documentation, + * even if they matched the rules above. + * Note that the route must be referenced by name here (wildcards are supported). + */ + 'exclude' => [ + // 'users.create', 'admin.*' + ], + + /* + * Specify rules to be applied to all the routes in this group when generating documentation + */ + 'apply' => [ + /* + * Specify headers to be added to the example requests + */ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + // 'Authorization' => 'Bearer {token}', + // 'Api-Version' => 'v2', + ], + + /* + * If no @response or @transformer declarations are found for the route, + * we'll try to get a sample response by attempting an API call. + * Configure the settings for the API call here. + */ + 'response_calls' => [ + /* + * API calls will be made only for routes in this group matching these HTTP methods (GET, POST, etc). + * List the methods here or use '*' to mean all methods. Leave empty to disable API calls. + */ + 'methods' => ['GET'], + + /* + * Laravel config variables which should be set for the API call. + * This is a good place to ensure that notifications, emails + * and other external services are not triggered + * during the documentation API calls + */ + 'config' => [ + 'app.env' => 'documentation', + 'app.debug' => false, + // 'service.key' => 'value', + ], + + /* + * Cookies which should be sent with the API call. + */ + 'cookies' => [ + // 'name' => 'value' + ], + + /* + * Query parameters which should be sent with the API call. + */ + 'queryParams' => [ + // 'key' => 'value', + ], + + /* + * Body parameters which should be sent with the API call. + */ + 'bodyParams' => [ + // 'key' => 'value', + ], + ], + ], + ], + ], + + 'strategies' => [ + 'metadata' => [ + \Mpociot\ApiDoc\Extracting\Strategies\Metadata\GetFromDocBlocks::class, + ], + 'urlParameters' => [ + \Mpociot\ApiDoc\Extracting\Strategies\UrlParameters\GetFromUrlParamTag::class, + ], + 'queryParameters' => [ + \Mpociot\ApiDoc\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class, + ], + 'headers' => [ + \Mpociot\ApiDoc\Extracting\Strategies\RequestHeaders\GetFromRouteRules::class, + ], + 'bodyParameters' => [ + \Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class, + ], + 'responses' => [ + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseTransformerTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseFileTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\ResponseCalls::class, + ], + ], + + /* + * Custom logo path. The logo will be copied from this location + * during the generate process. Set this to false to use the default logo. + * + * Change to an absolute path to use your custom logo. For example: + * 'logo' => resource_path('views') . '/api/logo.png' + * + * If you want to use this, please be aware of the following rules: + * - the image size must be 230 x 52 + */ + 'logo' => false, + + /* + * Name for the group of routes which do not have a @group set. + */ + 'default_group' => 'general', + + /* + * Example requests for each endpoint will be shown in each of these languages. + * Supported options are: bash, javascript, php, python + * You can add a language of your own, but you must publish the package's views + * and define a corresponding view for it in the partials/example-requests directory. + * See https://laravel-apidoc-generator.readthedocs.io/en/latest/generating-documentation.html + * + */ + 'example_languages' => [ + 'bash', + 'javascript', + ], + + /* + * Configure how responses are transformed using @transformer and @transformerCollection + * Requires league/fractal package: composer require league/fractal + * + */ + 'fractal' => [ + /* If you are using a custom serializer with league/fractal, + * you can specify it here. + * + * Serializers included with league/fractal: + * - \League\Fractal\Serializer\ArraySerializer::class + * - \League\Fractal\Serializer\DataArraySerializer::class + * - \League\Fractal\Serializer\JsonApiSerializer::class + * + * Leave as null to use no serializer or return a simple JSON. + */ + 'serializer' => null, + ], + + /* + * If you would like the package to generate the same example values for parameters on each run, + * set this to any number (eg. 1234) + * + */ + 'faker_seed' => null, +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..5c44334 --- /dev/null +++ b/config/database.php @@ -0,0 +1,42 @@ + env('DB_CONNECTION', 'sqlite'), + 'connections' => [ + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => env('DB_DATABASE', database_path('default.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'default_db'), + 'username' => env('DB_USERNAME', 'default_user'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + 'sqlite_testing' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ] + ], + 'fetch' => PDO::FETCH_CLASS, // Returns DB objects in an array format. + 'migrations' => 'migrations' +]; + +?> diff --git a/container/db/01_init.sql b/container/db/01_init.sql new file mode 100644 index 0000000..72a43cf --- /dev/null +++ b/container/db/01_init.sql @@ -0,0 +1,6 @@ +DROP DATABASE IF EXISTS lostfound; +CREATE DATABASE lostfound; + +CREATE OR REPLACE USER lostfound IDENTIFIED BY 'lostfound'; + +GRANT ALL privileges ON `lostfound`.* TO 'lostfound'; diff --git a/container/web/general.conf b/container/web/general.conf new file mode 100644 index 0000000..3e00bb2 --- /dev/null +++ b/container/web/general.conf @@ -0,0 +1,9 @@ +add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;"; +add_header X-Content-Type-Options nosniff; +add_header X-XSS-Protection "1; mode=block"; +add_header X-Robots-Tag none; +add_header X-Download-Options noopen; +add_header X-Permitted-Cross-Domain-Policies none; +add_header Access-Control-Allow-Origin '*'; + +client_max_body_size 50m; diff --git a/container/web/images.conf b/container/web/images.conf new file mode 100644 index 0000000..1d36242 --- /dev/null +++ b/container/web/images.conf @@ -0,0 +1,7 @@ +rewrite '^/1/images/([0-9a-fA-F]{32})/?$' /staticimages/$1 last; +rewrite '^/1/thumbs/([0-9a-fA-F]{32})/?$' /thumbnails/$1 last; +# rewrite '^/thumbnails/([0-9a-fA-F]{32})$' /thumbnail.php?id=$1 last; + +location /thumbnails/ { + try_files $uri /thumbnail.php?id=$uri; +} diff --git a/container/web/init.sh b/container/web/init.sh new file mode 100644 index 0000000..0176b21 --- /dev/null +++ b/container/web/init.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +cd /app +echo "executing COMPOSER UPDATE" +composer update +echo "executing DATABASE MIGRATE" +php artisan migrate --force diff --git a/container/web/location-root.conf b/container/web/location-root.conf new file mode 100644 index 0000000..a8b6892 --- /dev/null +++ b/container/web/location-root.conf @@ -0,0 +1,15 @@ +location / { + if ($request_method = OPTIONS) { + add_header Content-Length 0; + add_header Content-Type text/plain; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; + add_header Access-Control-Allow-Origin '*'; #$http_origin; + add_header Access-Control-Allow-Headers "Authorization, Content-Type"; + add_header Access-Control-Allow-Credentials true; + return 200; + } + + try_files $uri $uri/ /index.php?$query_string; + + +} diff --git a/core/.coveragerc b/core/.coveragerc deleted file mode 100644 index 14c1fba..0000000 --- a/core/.coveragerc +++ /dev/null @@ -1,14 +0,0 @@ -[run] -source = . - -[report] -fail_under = 100 -show_missing = True -skip_covered = True -omit = - */tests/* - */migrations/* - core/asgi.py - core/wsgi.py - core/settings.py - manage.py \ No newline at end of file diff --git a/core/.gitignore b/core/.gitignore deleted file mode 100644 index 9fe17bc..0000000 --- a/core/.gitignore +++ /dev/null @@ -1,129 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ \ No newline at end of file diff --git a/core/README.md b/core/README.md deleted file mode 100644 index f9780e0..0000000 --- a/core/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Core - -This directory contains the backend of the C3LF System3 project, which is built using Django and Django Rest Framework. - -## Modules - -- `authentication`: Handles user authentication and authorization. -- `files`: Manages file uploads and related operations. -- `inventory`: Handles inventory management, including events, containers and items. -- `mail`: Manages email-related functionalities, including sending and receiving emails. -- `notify_sessions`: Handles real-time notifications and WebSocket sessions. -- `tickets`: Manages the ticketing system for issue tracking. - -## Modules Structure - -Most modules follow a similar structure, including the following components: - -- `/models.py`: Contains the database models for the module. -- `/serializers.py`: Contains the serializers for the module models. -- `/api_.py`: Contains the API views and endpoints for the module. -- `/migrations/`: Contains database migration files. Needs to contain an `__init__.py` file to be recognized as - a Python package and automatically migration creation to work. -- `/tests//test_.py`: Contains the test cases for the module. - -## Development Setup - -follow the instructions under 'Backend-API-Only' or 'Fullstack-Lite' in the root level `README.md` to set up a -development environment. - -## Test-Driven Development (TDD) Workflow - -The project follows a TDD workflow to ensure code quality and reliability. Here is a step-by-step guide to the TDD -process: - -1. **Write a Test**: Start by writing a test case for the new feature or bug fix. Place the test case in the appropriate - module within the `/tests//test_.py` file. - -2. **Run the Test**: Execute the test to ensure it fails, confirming that the feature is not yet implemented or the bug - exists. - ```bash - python manage.py test - ``` - -3. **Write the Code**: Implement the code required to pass the test. Write the code in the appropriate module within the - project. - -4. **Run the Test Again**: Execute the test again to ensure it passes. - ```bash - python manage.py test - ``` - -5. **Refactor**: Refactor the code to improve its structure and readability while ensuring that all tests still pass. - -6. **Repeat**: Repeat the process for each new feature or bug fix. - -## Measuring Test Coverage - -The project uses the `coverage` package to measure test coverage. To generate a coverage report, run the following -command: - -```bash -coverage run --source='.' manage.py test -coverage report -``` - -## Additional Information - -For more detailed information on the project structure and development modes, refer to the root level `README.md`. \ No newline at end of file diff --git a/core/authentication/admin.py b/core/authentication/admin.py deleted file mode 100644 index 0024cb0..0000000 --- a/core/authentication/admin.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.contrib import admin -from django.contrib.auth.admin import UserAdmin - -from authentication.models import ExtendedUser - - -class ExtendedUserAdmin(UserAdmin): - list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_superuser') - search_fields = ('username', 'email', 'first_name', 'last_name') - ordering = ('username',) - filter_horizontal = ('groups', 'user_permissions', 'permissions') - - -admin.site.register(ExtendedUser, ExtendedUserAdmin) diff --git a/core/authentication/api_v2.py b/core/authentication/api_v2.py deleted file mode 100644 index 2547b6d..0000000 --- a/core/authentication/api_v2.py +++ /dev/null @@ -1,89 +0,0 @@ -from rest_framework import routers, viewsets, serializers, permissions -from rest_framework.decorators import api_view, permission_classes, authentication_classes -from rest_framework.authtoken.serializers import AuthTokenSerializer -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from django.contrib.auth import login -from django.urls import path -from django.dispatch import receiver -from django.db.models.signals import post_save -from django.contrib.auth.models import Group -from knox.models import AuthToken -from knox.views import LoginView as KnoxLoginView - -from authentication.models import ExtendedUser -from authentication.serializers import UserSerializer, GroupSerializer - - -class UserViewSet(viewsets.ModelViewSet): - queryset = ExtendedUser.objects.all() - serializer_class = UserSerializer - - -class GroupViewSet(viewsets.ModelViewSet): - queryset = Group.objects.all() - serializer_class = GroupSerializer - - -@receiver(post_save, sender=ExtendedUser) -def create_auth_token(sender, instance=None, created=False, **kwargs): - if created: - AuthToken.objects.create(user=instance) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def selfUser(request): - serializer = UserSerializer(request.user) - return Response(serializer.data, status=200) - - -@api_view(['POST']) -@permission_classes([]) -@authentication_classes([]) -def registerUser(request): - try: - username = request.data.get('username') - password = request.data.get('password') - email = request.data.get('email') - - errors = {} - if not username: - errors['username'] = 'Username is required' - if not password: - errors['password'] = 'Password is required' - if not email: - errors['email'] = 'Email is required' - if ExtendedUser.objects.filter(email=email).exists(): - errors['email'] = 'Email already exists' - if ExtendedUser.objects.filter(username=username).exists(): - errors['username'] = 'Username already exists' - if errors: - return Response({'errors': errors}, status=400) - user = ExtendedUser.objects.create_user(username, email, password) - return Response({'username': user.username, 'email': user.email}, status=201) - except Exception as e: - return Response({'errors': str(e)}, status=400) - - -class LoginView(KnoxLoginView): - permission_classes = (permissions.AllowAny,) - authentication_classes = () - - def post(self, request, format=None): - serializer = AuthTokenSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = serializer.validated_data['user'] - login(request, user) - return super(LoginView, self).post(request, format=None) - - -router = routers.SimpleRouter() -router.register(r'users', UserViewSet, basename='users') -router.register(r'groups', GroupViewSet, basename='groups') - -urlpatterns = router.urls + [ - path('self/', selfUser), - path('login/', LoginView.as_view()), - path('register/', registerUser), -] diff --git a/core/authentication/migrations/0001_initial.py b/core/authentication/migrations/0001_initial.py deleted file mode 100644 index d659962..0000000 --- a/core/authentication/migrations/0001_initial.py +++ /dev/null @@ -1,67 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-11 21:10 - -from django.conf import settings -import django.contrib.auth.models -import django.contrib.auth.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('inventory', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='ExtendedUser', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ], - options={ - 'verbose_name': 'Extended user', - 'verbose_name_plural': 'Extended users', - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='EventPermission', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.event')), - ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'unique_together': {('user', 'permission', 'event')}, - }, - ), - migrations.AddField( - model_name='extendeduser', - name='permissions', - field=models.ManyToManyField(through='authentication.EventPermission', to='auth.permission'), - ), - migrations.AddField( - model_name='extendeduser', - name='user_permissions', - field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), - ), - ] diff --git a/core/authentication/migrations/0002_authtokeneventpermissions_extendedauthtoken_and_more.py b/core/authentication/migrations/0002_authtokeneventpermissions_extendedauthtoken_and_more.py deleted file mode 100644 index 383aafd..0000000 --- a/core/authentication/migrations/0002_authtokeneventpermissions_extendedauthtoken_and_more.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-11 21:11 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0001_initial'), - ('knox', '0008_remove_authtoken_salt'), - ('authentication', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='AuthTokenEventPermissions', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.event')), - ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='authentication.eventpermission')), - ], - ), - migrations.CreateModel( - name='ExtendedAuthToken', - fields=[ - ('authtoken_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='knox.authtoken')), - ('permissions', models.ManyToManyField(through='authentication.AuthTokenEventPermissions', to='authentication.eventpermission')), - ], - options={ - 'verbose_name': 'Extended auth token', - 'verbose_name_plural': 'Extended auth tokens', - }, - bases=('knox.authtoken',), - ), - migrations.AddField( - model_name='authtokeneventpermissions', - name='token', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='authentication.extendedauthtoken'), - ), - migrations.AlterUniqueTogether( - name='authtokeneventpermissions', - unique_together={('token', 'permission', 'event')}, - ), - ] diff --git a/core/authentication/migrations/0003_groups.py b/core/authentication/migrations/0003_groups.py deleted file mode 100644 index c8f299d..0000000 --- a/core/authentication/migrations/0003_groups.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.2.7 on 2023-11-26 00:16 - -from django.conf import settings -from django.db import migrations -from django.contrib.auth.models import Permission, Group - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('authentication', '0002_authtokeneventpermissions_extendedauthtoken_and_more'), - ('tickets', '0001_initial'), - ] - - def create_groups(apps, schema_editor): - admins = Group.objects.create(name='Admin') - orga = Group.objects.create(name='Orga') - team = Group.objects.create(name='Team') - users = Group.objects.create(name='User') - admins.permissions.add(*Permission.objects.all()) - users.permissions.add(*Permission.objects.filter(codename__in= - ['view_item', 'add_item', 'change_item', 'match_item'])) - team.permissions.add(*Permission.objects.filter(codename__in= - ['delete_item', 'view_issuethread', 'add_issuethread', - 'change_issuethread', 'delete_issuethread', 'send_mail']), - *users.permissions.all()) - orga.permissions.add(*Permission.objects.filter(codename__in=['add_event']), - *team.permissions.all()) - - operations = [ - migrations.RunPython(create_groups), - ] diff --git a/core/authentication/migrations/0004_legacy_user.py b/core/authentication/migrations/0004_legacy_user.py deleted file mode 100644 index 76f62dd..0000000 --- a/core/authentication/migrations/0004_legacy_user.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.conf import settings -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('authentication', '0003_groups'), - ] - - def create_legacy_user(apps, schema_editor): - ExtendedUser = apps.get_model('authentication', 'ExtendedUser') - ExtendedUser.objects.create_user(settings.LEGACY_USER_NAME, 'mail@' + settings.MAIL_DOMAIN, - settings.LEGACY_USER_PASSWORD) - - operations = [ - migrations.RunPython(create_legacy_user) - ] diff --git a/core/authentication/migrations/0005_alter_eventpermission_event_and_more.py b/core/authentication/migrations/0005_alter_eventpermission_event_and_more.py deleted file mode 100644 index 85258af..0000000 --- a/core/authentication/migrations/0005_alter_eventpermission_event_and_more.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-13 16:28 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0001_initial'), - ('authentication', '0004_legacy_user'), - ] - - operations = [ - migrations.AlterField( - model_name='eventpermission', - name='event', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='inventory.event'), - ), - migrations.AlterField( - model_name='eventpermission', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_permissions', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/core/authentication/models.py b/core/authentication/models.py deleted file mode 100644 index 0de8e6a..0000000 --- a/core/authentication/models.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.db import models -from django.contrib.auth.models import Permission, AbstractUser -from knox.models import AuthToken - -from inventory.models import Event - - -class ExtendedUser(AbstractUser): - permissions = models.ManyToManyField(Permission, through='EventPermission', through_fields=('user', 'permission')) - - class Meta: - verbose_name = 'Extended user' - verbose_name_plural = 'Extended users' - - def get_permissions(self): - if self.is_superuser: - for permission in Permission.objects.all(): - yield "*:" + permission.codename - for permission in self.user_permissions.all(): - yield "*:" + permission.codename - for group in self.groups.all(): - for permission in group.permissions.all(): - yield "*:" + permission.codename - for permission in self.event_permissions.all(): - yield permission.event.slug + ":" + permission.permission.codename - - def has_event_perm(self, event, permission): - if self.is_superuser: - return True - permissions = set(self.get_permissions()) - if "*:" + permission in permissions: - return True - if event.slug + ":" + permission in permissions: - return True - return False - - -class ExtendedAuthToken(AuthToken): - permissions = models.ManyToManyField('EventPermission', through='AuthTokenEventPermissions', - through_fields=('token', 'permission')) - - class Meta: - verbose_name = 'Extended auth token' - verbose_name_plural = 'Extended auth tokens' - - -class EventPermission(models.Model): - user = models.ForeignKey(ExtendedUser, on_delete=models.CASCADE, related_name='event_permissions') - permission = models.ForeignKey(Permission, on_delete=models.CASCADE) - event = models.ForeignKey(Event, on_delete=models.CASCADE, null=True, blank=True) - - class Meta: - unique_together = ('user', 'permission', 'event') - - -class AuthTokenEventPermissions(models.Model): - token = models.ForeignKey(ExtendedAuthToken, on_delete=models.CASCADE) - permission = models.ForeignKey(EventPermission, on_delete=models.CASCADE) - event = models.ForeignKey(Event, on_delete=models.CASCADE) - - class Meta: - unique_together = ('token', 'permission', 'event') diff --git a/core/authentication/serializers.py b/core/authentication/serializers.py deleted file mode 100644 index 0581865..0000000 --- a/core/authentication/serializers.py +++ /dev/null @@ -1,32 +0,0 @@ -from rest_framework import serializers -from django.contrib.auth.models import Group - -from authentication.models import ExtendedUser - - -class UserSerializer(serializers.ModelSerializer): - permissions = serializers.SerializerMethodField() - groups = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name') - - class Meta: - model = ExtendedUser - fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups') - read_only_fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups') - - def get_permissions(self, obj): - return list(set(obj.get_permissions())) - - -class GroupSerializer(serializers.ModelSerializer): - permissions = serializers.SerializerMethodField() - members = serializers.SerializerMethodField() - - class Meta: - model = Group - fields = ('id', 'name', 'permissions', 'members') - - def get_permissions(self, obj): - return ["*:" + p.codename for p in obj.permissions.all()] - - def get_members(self, obj): - return [u.username for u in obj.user_set.all()] diff --git a/core/authentication/tests/__init__.py b/core/authentication/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/authentication/tests/v2/__init__.py b/core/authentication/tests/v2/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/authentication/tests/v2/test_permissions.py b/core/authentication/tests/v2/test_permissions.py deleted file mode 100644 index 0a00fcd..0000000 --- a/core/authentication/tests/v2/test_permissions.py +++ /dev/null @@ -1,90 +0,0 @@ -from django.test import TestCase, Client -from django.contrib.auth.models import Permission -from knox.models import AuthToken - -from authentication.models import EventPermission, ExtendedUser -from inventory.models import Event - - -class PermissionsTestCase(TestCase): - def setUp(self): - super().setUp() - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - event1 = Event.objects.create(slug='testevent1', name='testevent1') - event2 = Event.objects.create(slug='testevent2', name='testevent2') - permission1 = Permission.objects.get(codename='view_event') - EventPermission.objects.create(user=self.user, permission=permission1, event=event1) - EventPermission.objects.create(user=self.user, permission=permission1, event=event2) - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - self.newuser = ExtendedUser.objects.create_user('newuser', 'test', 'test') - self.newuser_token = AuthToken.objects.create(user=self.newuser) - self.newuser_client = Client(headers={'Authorization': 'Token ' + self.newuser_token[1]}) - - def test_user_permissions(self): - """ - Test that a user can only access their own data. - """ - response = self.client.get('/api/2/users/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 3) - self.assertEqual(response.json()[0]['username'], 'legacy_user') - self.assertEqual(response.json()[0]['email'], 'mail@localhost') - self.assertEqual(response.json()[0]['first_name'], '') - self.assertEqual(response.json()[0]['last_name'], '') - self.assertEqual(response.json()[0]['id'], 1) - self.assertEqual(response.json()[1]['username'], 'testuser') - self.assertEqual(response.json()[1]['email'], 'test') - self.assertEqual(response.json()[1]['first_name'], '') - self.assertEqual(response.json()[1]['last_name'], '') - - def test_user_permission(self): - """ - Test that a user can only access their own data. - """ - #ä['add_logentry', 'change_logentry', 'delete_logentry', 'view_logentry', 'add_group', 'change_group', - #ä 'delete_group', 'view_group', 'add_permission', 'change_permission', 'delete_permission', 'view_permission', - #ä 'add_authtokeneventpermissions', 'change_authtokeneventpermissions', 'delete_authtokeneventpermissions', - #ä 'view_authtokeneventpermissions', 'add_eventpermission', 'change_eventpermission', 'delete_eventpermission', - #ä 'view_eventpermission', 'add_extendedauthtoken', 'change_extendedauthtoken', 'delete_extendedauthtoken', - #ä 'view_extendedauthtoken', 'add_extendeduser', 'change_extendeduser', 'delete_extendeduser', - #ä 'view_extendeduser', 'add_contenttype', 'change_contenttype', 'delete_contenttype', 'view_contenttype', - #ä 'add_file', 'change_file', 'delete_file', 'view_file', 'add_container', 'change_container', 'delete_container', - #ä 'view_container', 'add_event', 'change_event', 'delete_event', 'view_event', 'add_item', 'change_item', - #ä 'delete_item', 'match_item', 'view_item', 'add_authtoken', 'change_authtoken', 'delete_authtoken', - #ä 'view_authtoken', 'add_email', 'change_email', 'delete_email', 'view_email', 'add_eventaddress', - #ä 'change_eventaddress', 'delete_eventaddress', 'view_eventaddress', 'add_systemevent', 'change_systemevent', - #ä 'delete_systemevent', 'view_systemevent', 'add_session', 'change_session', 'delete_session', 'view_session', - #ä 'add_comment', 'change_comment', 'delete_comment', 'view_comment', 'add_issuethread', 'change_issuethread', - #ä 'delete_issuethread', 'send_mail', 'view_issuethread', 'add_statechange', 'change_statechange', - #ä 'delete_statechange', 'view_statechange'] - - user = ExtendedUser.objects.create_user('testuser2', 'test', 'test') - user.event_permissions.create(permission=Permission.objects.get(codename='view_item'), event=Event.objects.get(slug='testevent1')) - user.event_permissions.create(permission=Permission.objects.get(codename='view_item'), event=Event.objects.get(slug='testevent2')) - user.event_permissions.create(permission=Permission.objects.get(codename='add_item'), event=Event.objects.get(slug='testevent1')) - user.save() - #self.assertTrue(user.has_perm('inventory.view_event', Event.objects.get(slug='testevent1'))) - #self.assertTrue(user.has_perm('inventory.view_event', Event.objects.get(slug='testevent2'))) - #self.assertFalse(user.has_perm('inventory.add_event', Event.objects.get(slug='testevent1'))) - #self.assertFalse(user.has_perm('inventory.add_event', Event.objects.get(slug='testevent2'))) - - def test_item_api_permissions(self): - """ - Test that a user can only access their own data. - """ - response = self.client.get('/api/2/testevent1/items/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 0) - - response = self.client.get('/api/2/testevent2/items/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 0) - - response = self.newuser_client.get('/api/2/testevent1/items/') - self.assertEqual(response.status_code, 403) - - response = self.newuser_client.get('/api/2/testevent2/items/') - self.assertEqual(response.status_code, 403) - diff --git a/core/authentication/tests/v2/test_users.py b/core/authentication/tests/v2/test_users.py deleted file mode 100644 index 125be9b..0000000 --- a/core/authentication/tests/v2/test_users.py +++ /dev/null @@ -1,183 +0,0 @@ -from django.test import TestCase, Client -from django.contrib.auth.models import Permission, Group - -from knox.models import AuthToken - -from authentication.models import ExtendedUser, EventPermission -from core import settings -from inventory.models import Event - - -class UserApiTest(TestCase): - - def setUp(self): - self.event = Event.objects.create(name='testevent', slug='testevent') - self.group1 = Group.objects.create(name='testgroup1') - self.group2 = Group.objects.create(name='testgroup2') - self.group1.permissions.add(Permission.objects.get(codename='add_item')) - self.group1.permissions.add(Permission.objects.get(codename='view_item')) - self.group2.permissions.add(Permission.objects.get(codename='view_event')) - self.group2.permissions.add(Permission.objects.get(codename='view_item')) - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(Permission.objects.get(codename='add_event')) - self.user.groups.add(self.group1) - self.user.groups.add(self.group2) - self.user.save() - EventPermission.objects.create(event=self.event, user=self.user, - permission=Permission.objects.get(codename='delete_item')) - self.user.save() - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - def test_users(self): - response = self.client.get('/api/2/users/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 2) - self.assertEqual(response.json()[0]['username'], settings.LEGACY_USER_NAME) - self.assertEqual(response.json()[0]['email'], 'mail@' + settings.MAIL_DOMAIN) - self.assertEqual(response.json()[0]['first_name'], '') - self.assertEqual(response.json()[0]['last_name'], '') - self.assertEqual(response.json()[0]['id'], 1) - self.assertEqual(response.json()[0]['groups'], []) - self.assertEqual(response.json()[1]['username'], 'testuser') - self.assertEqual(response.json()[1]['email'], 'test') - self.assertEqual(response.json()[1]['first_name'], '') - self.assertEqual(response.json()[1]['last_name'], '') - self.assertEqual(response.json()[1]['id'], 2) - self.assertEqual(response.json()[1]['groups'], ['testgroup1', 'testgroup2']) - - def test_self_user(self): - response = self.client.get('/api/2/self/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['username'], 'testuser') - self.assertEqual(response.json()['email'], 'test') - self.assertEqual(response.json()['first_name'], '') - self.assertEqual(response.json()['last_name'], '') - permissions = response.json()['permissions'] - self.assertEqual(len(permissions), 5) - self.assertTrue('*:add_item' in permissions) - self.assertTrue('*:view_item' in permissions) - self.assertTrue('*:view_event' in permissions) - self.assertTrue('testevent:delete_item' in permissions) - self.assertTrue('*:add_event' in permissions) - - def test_register_user(self): - anonymous = Client() - response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test', 'email': 'test2'}, - content_type='application/json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['username'], 'testuser2') - self.assertEqual(response.json()['email'], 'test2') - self.assertEqual(len(ExtendedUser.objects.all()), 3) - self.assertEqual(ExtendedUser.objects.get(username='testuser2').email, 'test2') - self.assertTrue(ExtendedUser.objects.get(username='testuser2').check_password('test')) - - def test_register_user_duplicate(self): - anonymous = Client() - response = anonymous.post('/api/2/register/', {'username': 'testuser', 'password': 'test', 'email': 'test2'}, - content_type='application/json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json()['errors']['username'], 'Username already exists') - self.assertEqual(len(ExtendedUser.objects.all()), 2) - - def test_register_user_no_username(self): - anonymous = Client() - response = anonymous.post('/api/2/register/', {'password': 'test', 'email': 'test2'}, - content_type='application/json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json()['errors']['username'], 'Username is required') - self.assertEqual(len(ExtendedUser.objects.all()), 2) - - def test_register_user_no_password(self): - anonymous = Client() - response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'email': 'test2'}, - content_type='application/json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json()['errors']['password'], 'Password is required') - self.assertEqual(len(ExtendedUser.objects.all()), 2) - - def test_register_user_no_email(self): - anonymous = Client() - response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test'}, - content_type='application/json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json()['errors']['email'], 'Email is required') - self.assertEqual(len(ExtendedUser.objects.all()), 2) - - def test_register_user_duplicate_email(self): - anonymous = Client() - response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test', 'email': 'test'}, - content_type='application/json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json()['errors']['email'], 'Email already exists') - self.assertEqual(len(ExtendedUser.objects.all()), 2) - - def test_get_token(self): - anonymous = Client() - response = anonymous.post('/api/2/login/', {'username': 'testuser', 'password': 'test'}, - content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertTrue('token' in response.json()) - - def test_legacy_user(self): - response = self.client.get('/api/2/users/1/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['username'], settings.LEGACY_USER_NAME) - self.assertEqual(response.json()['email'], 'mail@' + settings.MAIL_DOMAIN) - self.assertEqual(response.json()['first_name'], '') - self.assertEqual(response.json()['last_name'], '') - self.assertEqual(response.json()['id'], 1) - - def test_get_legacy_user_token(self): - anonymous = Client() - response = anonymous.post('/api/2/login/', { - 'username': settings.LEGACY_USER_NAME, 'password': settings.LEGACY_USER_PASSWORD}, - content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertTrue('token' in response.json()) - - -class GroupApiTest(TestCase): - - def setUp(self): - self.event = Event.objects.create(name='testevent', slug='testevent') - # Admin, Orga, Team, User are created by default - self.group1 = Group.objects.create(name='testgroup1') - self.group2 = Group.objects.create(name='testgroup2') - self.group1.permissions.add(Permission.objects.get(codename='add_item')) - self.group1.permissions.add(Permission.objects.get(codename='view_item')) - self.group2.permissions.add(Permission.objects.get(codename='view_event')) - self.group2.permissions.add(Permission.objects.get(codename='view_item')) - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(Permission.objects.get(codename='add_event')) - self.user.groups.add(self.group1) - self.user.groups.add(self.group2) - self.user.save() - EventPermission.objects.create(event=self.event, user=self.user, - permission=Permission.objects.get(codename='delete_item')) - self.user.save() - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - def test_groups(self): - response = self.client.get('/api/2/groups/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 6) - self.assertEqual(response.json()[0]['name'], 'Admin') - self.assertEqual(response.json()[1]['name'], 'Orga') - self.assertEqual(response.json()[2]['name'], 'Team') - self.assertEqual(response.json()[3]['name'], 'User') - self.assertEqual(response.json()[4]['name'], 'testgroup1') - self.assertEqual(response.json()[5]['name'], 'testgroup2') - - def test_group(self): - response = self.client.get('/api/2/groups/5/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['name'], 'testgroup1') - permissions = response.json()['permissions'] - self.assertEqual(len(permissions), 2) - self.assertTrue('*:add_item' in permissions) - self.assertTrue('*:view_item' in permissions) - members = response.json()['members'] - self.assertEqual(len(members), 1) - self.assertEqual(members[0], 'testuser') diff --git a/core/core/__init__.py b/core/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/core/asgi.py b/core/core/asgi.py deleted file mode 100644 index 9d7fde2..0000000 --- a/core/core/asgi.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -ASGI config for core project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ -""" - -import os - -from channels.auth import AuthMiddlewareStack -from channels.routing import ProtocolTypeRouter, URLRouter -from channels.security.websocket import AllowedHostsOriginValidator -from django.core.asgi import get_asgi_application -from notify_sessions.routing import websocket_urlpatterns - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') - -django_asgi_app = get_asgi_application() - - -class TokenAuthMiddleware: - """ - Token authorization middleware for Django Channels 2 - """ - - def __init__(self, inner): - self.inner = inner - - def __call__(self, scope): - import base64 - headers = dict(scope['headers']) - if b'authorization' in headers: - try: - token_name, token_key = headers[b'authorization'].decode().split() - if token_name == 'Basic': - b64 = base64.b64decode(token_key) - user = b64.decode().split(':')[0] - password = b64.decode().split(':')[1] - print(user, password) - else: - print("Token name is not Basic") - scope['user'] = None - except: - print("Token is not valid") - scope['user'] = None - else: - print("Token is not in headers") - scope['user'] = None - - -TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner)) - -websocket_asgi_app = AllowedHostsOriginValidator( - AuthMiddlewareStack( - URLRouter( - websocket_urlpatterns - ) - ) -) - -application = ProtocolTypeRouter({ - "http": django_asgi_app, - "websocket": websocket_asgi_app, -}) diff --git a/core/core/globals.py b/core/core/globals.py deleted file mode 100644 index d84a7a0..0000000 --- a/core/core/globals.py +++ /dev/null @@ -1,29 +0,0 @@ -import asyncio -import logging -import signal - -loop = asyncio.get_event_loop() - - -def create_task(coro): - global loop - loop.create_task(coro) - - -async def shutdown(sig, loop): - log = logging.getLogger() - log.info(f"Received exit signal {sig.name}...") - tasks = [t for t in asyncio.all_tasks() if t is not - asyncio.current_task()] - [task.cancel() for task in tasks] - log.info(f"Cancelling {len(tasks)} outstanding tasks") - await asyncio.wait_for(loop.shutdown_asyncgens(), timeout=10) - loop.stop() - log.info("Shutdown complete.") - - -def init_loop(): - global loop - loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(shutdown(signal.SIGTERM, loop))) - loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(signal.SIGINT, loop))) - return loop diff --git a/core/core/metrics.py b/core/core/metrics.py deleted file mode 100644 index d973b0d..0000000 --- a/core/core/metrics.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.apps import apps -from prometheus_client.core import CounterMetricFamily, REGISTRY -from django.db.models import Case, Value, When, BooleanField, Count -from inventory.models import Item - - -class ItemCountCollector(object): - - def collect(self): - try: - counter = CounterMetricFamily("item_count", "Current number of items", labels=['event', 'returned_state']) - - yield counter - - if not apps.models_ready or not apps.apps_ready: - return - - queryset = ( - Item.all_objects - .annotate( - returned=Case( - When(returned_at__isnull=True, then=Value(False)), - default=Value(True), - output_field=BooleanField() - ) - ) - .values('event__slug', 'returned', 'event_id') - .annotate(amount=Count('id')) - .order_by('event__slug', 'returned') # Optional: order by slug and returned - ) - - for e in queryset: - counter.add_metric([e["event__slug"].lower(), str(e["returned"])], e["amount"]) - - yield counter - except: - pass - - -REGISTRY.register(ItemCountCollector()) diff --git a/core/core/settings.py b/core/core/settings.py deleted file mode 100644 index 805a27b..0000000 --- a/core/core/settings.py +++ /dev/null @@ -1,235 +0,0 @@ -""" -Django settings for core project. - -Generated by 'django-admin startproject' using Django 4.2.7. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.2/ref/settings/ -""" -import os -import sys - -import dotenv -from pathlib import Path - - -def truthy_str(s): - return s.lower() in ['true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'sure', 'positive', 'uh-huh', '👍'] - - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -dotenv.load_dotenv(BASE_DIR / '.env') - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-tm*$w_14iqbiy-!7(8#ba7j+_@(7@rf2&a^!=shs&$03b%2*rv') - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = truthy_str(os.getenv('DEBUG_MODE_ACTIVE', 'False')) - -PRIMARY_HOST = os.getenv('HTTP_HOST', 'localhost') - -ALLOWED_HOSTS = [PRIMARY_HOST] - -MAIL_DOMAIN = os.getenv('MAIL_DOMAIN', 'localhost') - -CSRF_TRUSTED_ORIGINS = ["https://" + host for host in ALLOWED_HOSTS] - -LEGACY_USER_NAME = os.getenv('LEGACY_API_USER', 'legacy_user') -LEGACY_USER_PASSWORD = os.getenv('LEGACY_API_PASSWORD', 'legacy_password') - -SYSTEM3_VERSION = "0.0.0-dev.0" - -ACTIVE_SPAM_TRAINING = truthy_str(os.getenv('ACTIVE_SPAM_TRAINING', 'False')) - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django_extensions', - 'django_prometheus', - 'rest_framework', - 'knox', - 'drf_yasg', - 'channels', - 'authentication', - 'files', - 'tickets', - 'inventory', - 'mail', - 'notify_sessions', -] - -REST_FRAMEWORK = { - 'TEST_REQUEST_DEFAULT_FORMAT': 'json', - 'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication',), - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.DjangoModelPermissions'), -} - -AUTH_USER_MODEL = 'authentication.ExtendedUser' - -SWAGGER_SETTINGS = { - 'SECURITY_DEFINITIONS': { - 'api_key': { - 'type': 'apiKey', - 'in': 'header', - 'name': 'Authorization' - } - }, - 'USE_SESSION_AUTH': False, - 'JSON_EDITOR': True, - 'DEFAULT_INFO': 'core.urls.openapi_info', -} - -MIDDLEWARE = [ - 'django_prometheus.middleware.PrometheusBeforeMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django_prometheus.middleware.PrometheusAfterMiddleware', -] - -ROOT_URLCONF = 'core.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -ASGI_APPLICATION = 'core.asgi.application' - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -if os.getenv('DB_HOST') is not None: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'HOST': os.getenv('DB_HOST', 'localhost'), - 'PORT': os.getenv('DB_PORT', '3306'), - 'NAME': os.getenv('DB_NAME', 'system3'), - 'USER': os.getenv('DB_USER', 'system3'), - 'PASSWORD': os.getenv('DB_PASSWORD', 'system3'), - 'OPTIONS': { - 'charset': 'utf8mb4', - 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" - } - }, - } -elif os.getenv('DB_FILE') is not None: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.getenv('DB_FILE', 'local.db'), - } - } -else: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - } - } - -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_ROOT = os.getenv('STATIC_ROOT', 'staticfiles') -STATIC_URL = '/static/' - -MEDIA_ROOT = os.getenv('MEDIA_ROOT', 'userfiles') -MEDIA_URL = '/media/' - -STORAGES = { - 'default': { - 'BACKEND': 'django.core.files.storage.FileSystemStorage', - 'OPTIONS': { - 'base_url': MEDIA_URL, - 'location': BASE_DIR / MEDIA_ROOT - }, - }, - 'staticfiles': { - 'BACKEND': 'django.core.files.storage.FileSystemStorage', - 'OPTIONS': { - 'base_url': STATIC_URL, - 'location': BASE_DIR / STATIC_ROOT - }, - }, -} -DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024 * 128 # 128 MB - -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - 'hosts': [(os.getenv('REDIS_HOST', 'localhost'), 6379)], - }, - } - -} - -PROMETHEUS_METRIC_NAMESPACE = 'c3lf' - -TEST_RUNNER = 'core.test_runner.FastTestRunner' diff --git a/core/core/test_runner.py b/core/core/test_runner.py deleted file mode 100644 index bd7f9eb..0000000 --- a/core/core/test_runner.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.conf import settings -from django.test.runner import DiscoverRunner - - -class FastTestRunner(DiscoverRunner): - def setup_test_environment(self): - super(FastTestRunner, self).setup_test_environment() - # Don't write files - settings.STORAGES = { - 'default': { - 'BACKEND': 'django.core.files.storage.InMemoryStorage', - 'OPTIONS': { - 'base_url': '/media/', - 'location': '', - }, - }, - } - # Bonus: Use a faster password hasher. This REALLY helps. - settings.PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.MD5PasswordHasher', - ) - - settings.CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels.layers.InMemoryChannelLayer' - } - } - settings.DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - } - } diff --git a/core/core/urls.py b/core/core/urls.py deleted file mode 100644 index 2386891..0000000 --- a/core/core/urls.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -URL configuration for core project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path, include - -from .version import get_info - -from .metrics import * - -urlpatterns = [ - path('djangoadmin/', admin.site.urls), - path('api/2/', include('inventory.api_v2')), - path('api/2/', include('files.api_v2')), - path('media/2/', include('files.media_v2')), - path('api/2/', include('tickets.api_v2')), - path('api/2/', include('mail.api_v2')), - path('api/2/', include('notify_sessions.api_v2')), - path('api/2/', include('authentication.api_v2')), - path('api/', get_info), - path('', include('django_prometheus.urls')), -] diff --git a/core/core/version.py b/core/core/version.py deleted file mode 100644 index 64d3bde..0000000 --- a/core/core/version.py +++ /dev/null @@ -1,15 +0,0 @@ -from rest_framework.decorators import api_view, permission_classes, authentication_classes -from rest_framework.response import Response - -from .settings import SYSTEM3_VERSION - - -@api_view(['GET']) -@permission_classes([]) -@authentication_classes([]) -def get_info(request): - return Response({ - "framework_version": SYSTEM3_VERSION, - "api_min_version": "1.0", - "api_max_version": "1.0", - }) diff --git a/core/files/__init__.py b/core/files/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/files/admin.py b/core/files/admin.py deleted file mode 100644 index 3584ed5..0000000 --- a/core/files/admin.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.contrib import admin - -from files.models import File - - -class FileAdmin(admin.ModelAdmin): - pass - - -admin.site.register(File, FileAdmin) \ No newline at end of file diff --git a/core/files/api_v2.py b/core/files/api_v2.py deleted file mode 100644 index a0962f0..0000000 --- a/core/files/api_v2.py +++ /dev/null @@ -1,24 +0,0 @@ -from rest_framework import serializers, viewsets, routers - -from files.models import File - - -class FileSerializer(serializers.ModelSerializer): - data = serializers.CharField(max_length=1000000, write_only=True) - - class Meta: - model = File - fields = ['hash', 'data'] - read_only_fields = ['hash'] - - -class FileViewSet(viewsets.ModelViewSet): - serializer_class = FileSerializer - queryset = File.objects.all() - lookup_field = 'hash' - - -router = routers.SimpleRouter() -router.register(r'files', FileViewSet, basename='files') - -urlpatterns = router.urls diff --git a/core/files/media_v2.py b/core/files/media_v2.py deleted file mode 100644 index 00161b3..0000000 --- a/core/files/media_v2.py +++ /dev/null @@ -1,96 +0,0 @@ -from datetime import datetime, timedelta - -import os -from django.http import HttpResponse -from django.urls import path -from drf_yasg.utils import swagger_auto_schema -from rest_framework import status -from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from core.settings import MEDIA_ROOT -from files.models import File -from mail.models import EmailAttachment - - -@swagger_auto_schema(method='GET', auto_schema=None) -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def media_urls(request, hash): - try: - if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash: - return HttpResponse(status=status.HTTP_304_NOT_MODIFIED) - - file = File.objects.filter(hash=hash).first() - attachment = EmailAttachment.objects.filter(hash=hash).first() - file = file if file else attachment - if not file: - return Response(status=status.HTTP_404_NOT_FOUND) - hash_path = file.file - return HttpResponse(status=status.HTTP_200_OK, - content_type=file.mime_type, - headers={ - 'X-Accel-Redirect': f'/redirect_media/{hash_path}', - 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'max-age=31536000, private, immutable', - 'Expires': datetime.utcnow() + timedelta(days=365), - 'Age': 0, - 'ETag': file.hash, - }) - except FileNotFoundError: - return Response(status=status.HTTP_404_NOT_FOUND) - except File.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - except EmailAttachment.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - - -@swagger_auto_schema(method='GET', auto_schema=None) -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def thumbnail_urls(request, size, hash): - if size not in [32, 64, 256]: - return Response(status=status.HTTP_404_NOT_FOUND) - if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash + "_" + str(size): - return HttpResponse(status=status.HTTP_304_NOT_MODIFIED) - try: - file = File.objects.filter(hash=hash).first() - attachment = EmailAttachment.objects.filter(hash=hash).first() - file = file if file else attachment - if not file: - return Response(status=status.HTTP_404_NOT_FOUND) - hash_path = file.file - if not os.path.exists(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}'): - from PIL import Image - image = Image.open(file.file) - image.thumbnail((size, size)) - rgb_image = image.convert('RGB') - thumb_dir = os.path.dirname(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}') - if not os.path.exists(thumb_dir): - os.makedirs(thumb_dir) - rgb_image.save(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}', 'jpeg', quality=90) - - return HttpResponse(status=status.HTTP_200_OK, - content_type="image/jpeg", - headers={ - 'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}', - 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'max-age=31536000, private, immutable', - 'Expires': datetime.utcnow() + timedelta(days=365), - 'Age': 0, - 'ETag': file.hash + "_" + str(size), - }) - - except FileNotFoundError: - return Response(status=status.HTTP_404_NOT_FOUND) - except File.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - except EmailAttachment.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - - -urlpatterns = [ - path('//', thumbnail_urls), - path('/', media_urls), -] diff --git a/core/files/migrations/0001_initial.py b/core/files/migrations/0001_initial.py deleted file mode 100644 index 2c15fff..0000000 --- a/core/files/migrations/0001_initial.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-09 02:13 - -from django.db import migrations, models -import django.db.models.deletion -import files.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('inventory', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='File', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(blank=True, null=True)), - ('updated_at', models.DateTimeField(blank=True, null=True)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('file', models.ImageField(upload_to=files.models.hash_upload)), - ('mime_type', models.CharField(max_length=255)), - ('hash', models.CharField(max_length=64, unique=True)), - ('item', models.ForeignKey(blank=True, db_column='iid', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='inventory.item')), - ], - ), - ] diff --git a/core/files/migrations/0002_alter_file_file.py b/core/files/migrations/0002_alter_file_file.py deleted file mode 100644 index 6a0c33e..0000000 --- a/core/files/migrations/0002_alter_file_file.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-10 19:04 - -from django.db import migrations, models -import files.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('files', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='file', - name='file', - field=models.FileField(upload_to=files.models.hash_upload), - ), - ] diff --git a/core/files/migrations/0003_ensure_creation_date.py b/core/files/migrations/0003_ensure_creation_date.py deleted file mode 100644 index 63e5760..0000000 --- a/core/files/migrations/0003_ensure_creation_date.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.7 on 2024-11-21 22:40 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ('files', '0002_alter_file_file'), - ] - - def set_creation_date(apps, schema_editor): - File = apps.get_model('files', 'File') - for file in File.objects.all(): - if file.created_at is None: - if not file.item.created_at is None: - file.created_at = file.item.created_at - else: - file.created_at = max(File.objects.filter( - id__lt=file.id, created_at__isnull=False).values_list('created_at', flat=True)) - file.save() - - operations = [ - migrations.RunPython(set_creation_date), - ] diff --git a/core/files/migrations/__init__.py b/core/files/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/files/models.py b/core/files/models.py deleted file mode 100644 index a8eb775..0000000 --- a/core/files/models.py +++ /dev/null @@ -1,80 +0,0 @@ -from django.core.files.base import ContentFile -from django.db import models, IntegrityError - -from inventory.models import Item - - -def hash_upload(instance, filename): - return f"{instance.hash[:2]}/{instance.hash[2:4]}/{instance.hash[4:6]}/{instance.hash[6:]}" - - -class FileManager(models.Manager): - - def __file_data_helper(self, **kwargs): - if 'data' in kwargs and type(kwargs['data']) == str: - import base64 - from hashlib import sha256 - raw = kwargs['data'] - if not raw.startswith('data:'): - raise ValueError('data must be a base64 encoded string or file and hash must be provided') - raw = raw.split(';base64,') - if len(raw) != 2: - raise ValueError('data must be a base64 encoded string or file and hash must be provided') - mime_type = raw[0].split(':')[1] - content = base64.b64decode(raw[1], validate=True) - kwargs.pop('data') - content_hash = sha256(content).hexdigest() - kwargs['file'] = ContentFile(content, content_hash) - kwargs['hash'] = content_hash - kwargs['mime_type'] = mime_type - elif 'file' in kwargs and 'hash' in kwargs and type(kwargs['file']) == ContentFile and 'mime_type' in kwargs: - pass - else: - raise ValueError('data must be a base64 encoded string or file and hash must be provided') - return kwargs - - def get_or_create(self, **kwargs): - kwargs = self.__file_data_helper(**kwargs) - try: - return self.get(hash=kwargs['hash']), False - except self.model.DoesNotExist: - obj = super().create(**kwargs) - obj.file.save(content=kwargs['file'], name=kwargs['hash']) - return obj, True - - def create(self, **kwargs): - kwargs = self.__file_data_helper(**kwargs) - if not self.filter(hash=kwargs['hash']).exists(): - obj = super().create(**kwargs) - obj.file.save(content=kwargs['file'], name=kwargs['hash']) - return obj - else: - raise IntegrityError('File with this hash already exists') - - -class AbstractFile(models.Model): - created_at = models.DateTimeField(blank=True, null=True) - updated_at = models.DateTimeField(blank=True, null=True) - deleted_at = models.DateTimeField(blank=True, null=True) - file = models.FileField(upload_to=hash_upload) - mime_type = models.CharField(max_length=255, null=False, blank=False) - hash = models.CharField(max_length=64, null=False, blank=False, unique=True) - - objects = FileManager() - - def save(self, *args, **kwargs): - from django.utils import timezone - if not self.created_at: - self.created_at = timezone.now() - self.updated_at = timezone.now() - super().save(*args, **kwargs) - - class Meta: - abstract = True - - -class File(AbstractFile): - item = models.ForeignKey(Item, models.CASCADE, db_column='iid', null=True, blank=True, related_name='files') - - def __str__(self): - return self.hash diff --git a/core/files/tests/__init__.py b/core/files/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/files/tests/v2/__init__.py b/core/files/tests/v2/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/files/tests/v2/test_files.py b/core/files/tests/v2/test_files.py deleted file mode 100644 index dedd647..0000000 --- a/core/files/tests/v2/test_files.py +++ /dev/null @@ -1,55 +0,0 @@ -from django.test import TestCase, Client -from django.contrib.auth.models import Permission - -from authentication.models import ExtendedUser -from files.models import File -from inventory.models import Event, Container, Item -from knox.models import AuthToken - - -class FileTestCase(TestCase): - - def setUp(self): - super().setUp() - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.user.save() - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - self.event = Event.objects.create(slug='EVENT', name='Event') - self.box = Container.objects.create(name='BOX') - - def test_list_files(self): - import base64 - item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) - response = self.client.get('/api/2/files/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()[0]['hash'], item.hash) - self.assertEqual(len(response.json()[0]['hash']), 64) - - def test_one_file(self): - import base64 - item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) - response = self.client.get(f'/api/2/files/{item.hash}/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['hash'], item.hash) - self.assertEqual(len(response.json()['hash']), 64) - - def test_create_file(self): - import base64 - Item.objects.create(container=self.box, event=self.event, description='1') - item = Item.objects.create(container=self.box, event=self.event, description='2') - response = self.client.post('/api/2/files/', - {'data': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')}, - content_type='application/json') - self.assertEqual(response.status_code, 201) - self.assertEqual(len(response.json()['hash']), 64) - - def test_delete_file(self): - import base64 - item = Item.objects.create(container=self.box, event=self.event, description='1') - File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) - file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8')) - self.assertEqual(len(File.objects.all()), 2) - response = self.client.delete(f'/api/2/files/{file.hash}/') - self.assertEqual(response.status_code, 204) diff --git a/core/helper.py b/core/helper.py deleted file mode 100644 index ae3c53b..0000000 --- a/core/helper.py +++ /dev/null @@ -1,30 +0,0 @@ -import asyncio -import logging -import signal - -loop = None - - -def create_task(coro): - global loop - loop.create_task(coro) - - -async def shutdown(sig, loop): - log = logging.getLogger() - log.info(f"Received exit signal {sig.name}...") - tasks = [t for t in asyncio.all_tasks() if t is not - asyncio.current_task()] - [task.cancel() for task in tasks] - log.info(f"Cancelling {len(tasks)} outstanding tasks") - await asyncio.wait_for(loop.shutdown_asyncgens(), timeout=10) - loop.stop() - log.info("Shutdown complete.") - - -def init_loop(): - global loop - loop = asyncio.get_event_loop() - loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(shutdown(signal.SIGTERM, loop))) - loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(signal.SIGINT, loop))) - return loop diff --git a/core/inventory/__init__.py b/core/inventory/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/inventory/admin.py b/core/inventory/admin.py deleted file mode 100644 index 24cd14a..0000000 --- a/core/inventory/admin.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.contrib import admin - -from inventory.models import Item, Container, ItemPlacement, Comment, Event - - -class ItemAdmin(admin.ModelAdmin): - pass - - -admin.site.register(Item, ItemAdmin) - - -class ContainerAdmin(admin.ModelAdmin): - pass - - -admin.site.register(Container, ContainerAdmin) - - -class EventAdmin(admin.ModelAdmin): - pass - - -admin.site.register(Event, EventAdmin) - - -class ItemPlacementAdmin(admin.ModelAdmin): - pass - - -admin.site.register(ItemPlacement, ItemPlacementAdmin) - - -class CommentAdmin(admin.ModelAdmin): - pass - - -admin.site.register(Comment, CommentAdmin) diff --git a/core/inventory/api_v2.py b/core/inventory/api_v2.py deleted file mode 100644 index 04c1722..0000000 --- a/core/inventory/api_v2.py +++ /dev/null @@ -1,205 +0,0 @@ -from django.urls import re_path -from django.contrib.auth.decorators import permission_required -from rest_framework import routers, viewsets, status -from rest_framework.decorators import api_view, permission_classes -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated - -from inventory.models import Event, Container, Item, Comment -from inventory.serializers import EventSerializer, ContainerSerializer, CommentSerializer, ItemSerializer, \ - SearchResultSerializer - -from base64 import b64decode - - -class EventViewSet(viewsets.ModelViewSet): - serializer_class = EventSerializer - queryset = Event.objects.all() - permission_classes = [] - - -class ContainerViewSet(viewsets.ModelViewSet): - serializer_class = ContainerSerializer - queryset = Container.objects.all() - - -class ItemViewSet(viewsets.ModelViewSet): - serializer_class = ItemSerializer - - def prefetch_queryset(self, queryset): - serializer = self.get_serializer_class() - if hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'prefetch_related_fields'): - queryset = queryset.prefetch_related(*serializer.Meta.prefetch_related_fields) - return queryset - - def get_queryset(self): - queryset = Item.objects.all() - return self.prefetch_queryset(queryset) - - -def filter_items(items, query): - query_tokens = query.split(' ') - matches = [] - for item in items: - value = 0 - if "I#" + str(item.id) in query: - value += 12 - matches.append( - {'type': 'item_id', 'text': f'is exactly {item.id} and matched "I#{item.id}"'}) - elif "#" + str(item.id) in query: - value += 11 - matches.append( - {'type': 'item_id', 'text': f'is exactly {item.id} and matched "#{item.id}"'}) - elif str(item.id) in query: - value += 10 - matches.append({'type': 'item_id', 'text': f'is exactly {item.id}'}) - for issue in item.related_issues: - if "T#" + issue.short_uuid() in query: - value += 8 - matches.append({'type': 'ticket_uuid', - 'text': f'is exactly {issue.short_uuid()} and matched "T#{issue.short_uuid()}"'}) - elif "#" + issue.short_uuid() in query: - value += 5 - matches.append({'type': 'ticket_uuid', - 'text': f'is exactly {issue.short_uuid()} and matched "#{issue.short_uuid()}"'}) - elif issue.short_uuid() in query: - value += 3 - matches.append({'type': 'ticket_uuid', 'text': f'is exactly {issue.short_uuid()}'}) - if "T#" + str(issue.id) in query: - value += 8 - matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id} and matched "T#{issue.id}"'}) - elif "#" + str(issue.id) in query: - value += 5 - matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id} and matched "#{issue.id}"'}) - elif str(issue.id) in query: - value += 3 - matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id}'}) - for comment in issue.comments.all(): - for token in query_tokens: - if token in comment.comment: - value += 1 - matches.append({'type': 'ticket_comment', 'text': f'contains {token}'}) - for token in query_tokens: - if token in issue.name: - value += 1 - matches.append({'type': 'ticket_name', 'text': f'contains {token}'}) - for token in query_tokens: - if token in item.description: - value += 1 - matches.append({'type': 'item_description', 'text': f'contains {token}'}) - for comment in item.comments.all(): - for token in query_tokens: - if token in comment.comment: - value += 1 - matches.append({'type': 'comment', 'text': f'contains {token}'}) - if value > 0: - yield {'search_score': value, 'item': item, 'search_matches': matches} - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def search_items(request, event_slug, query): - try: - event = Event.objects.get(slug=event_slug) - if not request.user.has_event_perm(event, 'view_item'): - return Response(status=403) - items = filter_items(Item.objects.filter(event=event), b64decode(query).decode('utf-8')) - return Response(SearchResultSerializer(items, many=True).data) - except Event.DoesNotExist: - return Response(status=404) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -def item(request, event_slug): - vs = ItemViewSet() - try: - event = None - if event_slug != 'none': - event = Event.objects.get(slug=event_slug) - if request.method == 'GET': - if not request.user.has_event_perm(event, 'view_item'): - return Response(status=403) - return Response(ItemSerializer(vs.prefetch_queryset(Item.objects.filter(event=event)), many=True).data) - elif request.method == 'POST': - if not request.user.has_event_perm(event, 'add_item'): - return Response(status=403) - validated_data = ItemSerializer(data=request.data) - if validated_data.is_valid(): - validated_data.save(event=event) - return Response(validated_data.data, status=201) - return Response(status=400) - except Event.DoesNotExist: - return Response(status=404) - except KeyError: - return Response(status=400) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@permission_required('tickets.add_comment', raise_exception=True) -def add_comment(request, event_slug, id): - event = None - if event_slug != 'none': - event = Event.objects.get(slug=event_slug) - item = Item.objects.get(event=event, id=id) - if not request.user.has_event_perm(event, 'view_item'): - return Response(status=403) - if 'comment' not in request.data or request.data['comment'] == '': - return Response({'status': 'error', 'message': 'missing comment'}, status=status.HTTP_400_BAD_REQUEST) - comment = Comment.objects.create( - item=item, - comment=request.data['comment'], - ) - return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED) - - -@api_view(['GET', 'PUT', 'DELETE', 'PATCH']) -@permission_classes([IsAuthenticated]) -def item_by_id(request, event_slug, id): - try: - event = Event.objects.get(slug=event_slug) - item = Item.objects.get(event=event, id=id) - if request.method == 'GET': - if not request.user.has_event_perm(event, 'view_item'): - return Response(status=403) - return Response(ItemSerializer(item).data) - elif request.method == 'PUT': - if not request.user.has_event_perm(event, 'change_item'): - return Response(status=403) - validated_data = ItemSerializer(item, data=request.data) - if validated_data.is_valid(): - validated_data.save() - return Response(validated_data.data) - return Response(validated_data.errors, status=400) - elif request.method == 'PATCH': - if not request.user.has_event_perm(event, 'change_item'): - return Response(status=403) - validated_data = ItemSerializer(item, data=request.data, partial=True) - if validated_data.is_valid(): - validated_data.save() - return Response(validated_data.data) - return Response(validated_data.errors, status=400) - elif request.method == 'DELETE': - if not request.user.has_event_perm(event, 'delete_item'): - return Response(status=403) - item.delete() - return Response(status=204) - except Item.DoesNotExist: - return Response(status=404) - except Event.DoesNotExist: - return Response(status=404) - - -router = routers.SimpleRouter() -router.register(r'events', EventViewSet, basename='events') -router.register(r'boxes', ContainerViewSet, basename='boxes') -router.register(r'box', ContainerViewSet, basename='boxes') - -urlpatterns = router.urls + [ - re_path(r'^(?P[\w-]+)/items/$', item, name='item'), - re_path(r'^(?P[\w-]+)/items/(?P[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'), - re_path(r'^(?P[\w-]+)/item/$', item, name='item'), - re_path(r'^(?P[\w-]+)/item/(?P\d+)/comment/$', add_comment, name='add_comment'), - re_path(r'^(?P[\w-]+)/item/(?P\d+)/$', item_by_id, name='item_by_id'), -] diff --git a/core/inventory/migrations/0001_initial.py b/core/inventory/migrations/0001_initial.py deleted file mode 100644 index bd08ef3..0000000 --- a/core/inventory/migrations/0001_initial.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 4.2.7 on 2023-11-18 11:28 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Container', - fields=[ - ('cid', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=255)), - ('created_at', models.DateTimeField(blank=True, null=True)), - ('updated_at', models.DateTimeField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='Event', - fields=[ - ('eid', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=255)), - ('slug', models.CharField(max_length=255, unique=True)), - ('start', models.DateTimeField(blank=True, null=True)), - ('end', models.DateTimeField(blank=True, null=True)), - ('pre_start', models.DateTimeField(blank=True, null=True)), - ('post_end', models.DateTimeField(blank=True, null=True)), - ('created_at', models.DateTimeField(blank=True, null=True)), - ('updated_at', models.DateTimeField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='Item', - fields=[ - ('iid', models.AutoField(primary_key=True, serialize=False)), - ('uid', models.IntegerField()), - ('description', models.TextField()), - ('returned_at', models.DateTimeField(blank=True, null=True)), - ('created_at', models.DateTimeField(blank=True, null=True)), - ('updated_at', models.DateTimeField(blank=True, null=True)), - ('container', models.ForeignKey(db_column='cid', on_delete=django.db.models.deletion.CASCADE, to='inventory.container')), - ('event', models.ForeignKey(db_column='eid', on_delete=django.db.models.deletion.CASCADE, to='inventory.event')), - ], - options={ - 'unique_together': {('uid', 'event')}, - }, - ), - ] diff --git a/core/inventory/migrations/0002_container_deleted_at_container_is_deleted_and_more.py b/core/inventory/migrations/0002_container_deleted_at_container_is_deleted_and_more.py deleted file mode 100644 index 28523a7..0000000 --- a/core/inventory/migrations/0002_container_deleted_at_container_is_deleted_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.2.7 on 2023-11-20 11:23 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='container', - name='deleted_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='container', - name='is_deleted', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='item', - name='deleted_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='item', - name='is_deleted', - field=models.BooleanField(default=False), - ), - ] diff --git a/core/inventory/migrations/0003_alter_item_options.py b/core/inventory/migrations/0003_alter_item_options.py deleted file mode 100644 index e20b525..0000000 --- a/core/inventory/migrations/0003_alter_item_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-07 18:46 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0002_container_deleted_at_container_is_deleted_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='item', - options={'permissions': [('match_item', 'Can match item')]}, - ), - ] diff --git a/core/inventory/migrations/0004_alter_event_created_at_alter_item_created_at.py b/core/inventory/migrations/0004_alter_event_created_at_alter_item_created_at.py deleted file mode 100644 index b5fd81a..0000000 --- a/core/inventory/migrations/0004_alter_event_created_at_alter_item_created_at.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-22 16:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0003_alter_item_options'), - ] - - operations = [ - migrations.AlterField( - model_name='event', - name='created_at', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='item', - name='created_at', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - ] diff --git a/core/inventory/migrations/0005_rename_cid_container_id_rename_eid_event_id_and_more.py b/core/inventory/migrations/0005_rename_cid_container_id_rename_eid_event_id_and_more.py deleted file mode 100644 index fcd4b8d..0000000 --- a/core/inventory/migrations/0005_rename_cid_container_id_rename_eid_event_id_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.2.7 on 2024-11-19 22:56 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0004_alter_event_created_at_alter_item_created_at'), - ] - - operations = [ - migrations.RenameField( - model_name='container', - old_name='cid', - new_name='id', - ), - migrations.RenameField( - model_name='event', - old_name='eid', - new_name='id', - ), - migrations.RenameField( - model_name='item', - old_name='iid', - new_name='id', - ), - migrations.RenameField( - model_name='item', - old_name='uid', - new_name='uid_deprecated', - ), - migrations.AlterUniqueTogether( - name='item', - unique_together=set(), - ), - migrations.AlterField( - model_name='item', - name='container', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.container'), - ), - migrations.AlterField( - model_name='item', - name='event', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.event'), - ), - migrations.AlterUniqueTogether( - name='item', - unique_together={('uid_deprecated', 'event')}, - ), - ] diff --git a/core/inventory/migrations/0006_alter_event_table.py b/core/inventory/migrations/0006_alter_event_table.py deleted file mode 100644 index 2fa421a..0000000 --- a/core/inventory/migrations/0006_alter_event_table.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.7 on 2024-11-20 01:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0005_rename_cid_container_id_rename_eid_event_id_and_more'), - ] - - operations = [ - migrations.AlterModelTable( - name='event', - table='common_event', - ), - ] diff --git a/core/inventory/migrations/0007_rename_container_item_container_old_itemplacement_and_more.py b/core/inventory/migrations/0007_rename_container_item_container_old_itemplacement_and_more.py deleted file mode 100644 index 918f636..0000000 --- a/core/inventory/migrations/0007_rename_container_item_container_old_itemplacement_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.2.7 on 2024-11-23 15:27 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - dependencies = [ - ('inventory', '0006_alter_event_table'), - ] - - def set_initial_container(apps, schema_editor): - Item = apps.get_model('inventory', 'Item') - for item in Item.objects.all(): - item.container_history.get_or_create(container=item.container_old) - item.save() - - operations = [ - migrations.RenameField( - model_name='item', - old_name='container', - new_name='container_old', - ), - migrations.CreateModel( - name='ItemPlacement', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('container', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_history', - to='inventory.container')), - ('item', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='container_history', - to='inventory.item')), - ], - ), - migrations.CreateModel( - name='Comment', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('comment', models.TextField()), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', - to='inventory.item')), - ], - ), - migrations.RunPython(set_initial_container), - migrations.RemoveField( - model_name='item', - name='container_old', - ), - ] diff --git a/core/inventory/migrations/__init__.py b/core/inventory/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/inventory/models.py b/core/inventory/models.py deleted file mode 100644 index c782bcb..0000000 --- a/core/inventory/models.py +++ /dev/null @@ -1,125 +0,0 @@ -from itertools import groupby - -from django.db import models -from django.db.models.signals import pre_save -from django.dispatch import receiver -from django.utils import timezone -from django_softdelete.models import SoftDeleteModel, SoftDeleteManager - - -class ItemManager(SoftDeleteManager): - - def create(self, **kwargs): - container = kwargs.pop('container') - if 'uid_deprecated' in kwargs: - raise ValueError('uid_deprecated must not be set manually') - uid_deprecated = Item.all_objects.filter(event=kwargs['event']).count() + 1 - kwargs['uid_deprecated'] = uid_deprecated - item = super().create(**kwargs) - item.container = container - return item - - def get_queryset(self): - return super().get_queryset().filter(returned_at__isnull=True) - - -class Item(SoftDeleteModel): - id = models.AutoField(primary_key=True) - uid_deprecated = models.IntegerField() - description = models.TextField() - event = models.ForeignKey('Event', models.CASCADE) - returned_at = models.DateTimeField(blank=True, null=True) - created_at = models.DateTimeField(null=True, auto_now_add=True) - updated_at = models.DateTimeField(blank=True, null=True) - - @property - def container(self): - try: - history = sorted(self.container_history.all(), key=lambda x: x.timestamp, reverse=True) - if history: - return history[0].container - else: - return None - except AttributeError: - return None - - @container.setter - def container(self, value): - if self.container == value: - return - self.container_history.create(container=value) - - @property - def related_issues(self): - groups = groupby(self.issue_relation_changes.all(), lambda rel: rel.issue_thread.id) - return [sorted(v, key=lambda r: r.timestamp)[0].issue_thread for k, v in groups] - - objects = ItemManager() - all_objects = models.Manager() - - class Meta: - unique_together = (('uid_deprecated', 'event'),) - permissions = [ - ('match_item', 'Can match item') - ] - - def __str__(self): - return '[' + str(self.id) + ']' + self.description - - -@receiver(pre_save, sender=Item) -def item_updated(sender, instance, **kwargs): - instance.updated_at = timezone.now() - - -class Container(SoftDeleteModel): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=255) - created_at = models.DateTimeField(blank=True, null=True) - updated_at = models.DateTimeField(blank=True, null=True) - - @property - def items(self): - try: - history = self.item_history.order_by('-timestamp').all() - return [v for k, v in groupby(history, key=lambda item: item.item.id)] - except AttributeError: - return [] - - def __str__(self): - return '[' + str(self.id) + ']' + self.name - - -class ItemPlacement(models.Model): - id = models.AutoField(primary_key=True) - item = models.ForeignKey('Item', models.CASCADE, related_name='container_history') - container = models.ForeignKey('Container', models.CASCADE, related_name='item_history') - timestamp = models.DateTimeField(auto_now_add=True) - - -class Comment(models.Model): - id = models.AutoField(primary_key=True) - item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='comments') - comment = models.TextField() - timestamp = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return str(self.item) + ' comment #' + str(self.id) - - -class Event(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=255) - slug = models.CharField(max_length=255, unique=True) - start = models.DateTimeField(blank=True, null=True) - end = models.DateTimeField(blank=True, null=True) - pre_start = models.DateTimeField(blank=True, null=True) - post_end = models.DateTimeField(blank=True, null=True) - created_at = models.DateTimeField(null=True, auto_now_add=True) - updated_at = models.DateTimeField(blank=True, null=True) - - def __str__(self): - return '[' + str(self.slug) + ']' + self.name - - class Meta: - db_table = 'common_event' diff --git a/core/inventory/serializers.py b/core/inventory/serializers.py deleted file mode 100644 index 0661476..0000000 --- a/core/inventory/serializers.py +++ /dev/null @@ -1,164 +0,0 @@ -from django.utils import timezone -from rest_framework import serializers -from rest_framework.relations import SlugRelatedField - -from files.models import File -from inventory.models import Event, Container, Item, Comment -from inventory.shared_serializers import BasicItemSerializer -from mail.models import EventAddress -from tickets.shared_serializers import BasicIssueSerializer - - -class EventSerializer(serializers.ModelSerializer): - addresses = SlugRelatedField(many=True, slug_field='address', queryset=EventAddress.objects.all()) - - class Meta: - model = Event - fields = ['id', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end', 'addresses'] - read_only_fields = ['id'] - - def to_internal_value(self, data): - data = data.copy() - addresses = data.pop('addresses', None) - dict = super().to_internal_value(data) - if addresses: - dict['addresses'] = [EventAddress.objects.get_or_create(address=x)[0] for x in addresses] - return dict - - -class ContainerSerializer(serializers.ModelSerializer): - itemCount = serializers.SerializerMethodField() - - class Meta: - model = Container - fields = ['id', 'name', 'itemCount'] - read_only_fields = ['id', 'itemCount'] - - def get_itemCount(self, instance): - return len(instance.items) - - -class CommentSerializer(serializers.ModelSerializer): - - def validate(self, attrs): - if 'comment' not in attrs or attrs['comment'] == '': - raise serializers.ValidationError('comment cannot be empty') - return attrs - - class Meta: - model = Comment - fields = ('id', 'comment', 'timestamp', 'item') - - -class ItemSerializer(BasicItemSerializer): - timeline = serializers.SerializerMethodField() - dataImage = serializers.CharField(write_only=True, required=False) - related_issues = BasicIssueSerializer(many=True, read_only=True) - - class Meta: - model = Item - fields = ['cid', 'box', 'id', 'description', 'file', 'dataImage', 'returned', 'event', 'related_issues', - 'timeline'] - read_only_fields = ['id'] - prefetch_related_fields = ['comments', 'issue_relation_changes', 'container_history', - 'container_history__container', 'files', 'event', - 'issue_relation_changes__issue_thread', - 'issue_relation_changes__issue_thread__event', - 'issue_relation_changes__issue_thread__state_changes', - 'issue_relation_changes__issue_thread__assignments'] - - def to_internal_value(self, data): - container = None - returned = False - if 'cid' in data: - container = Container.objects.get(id=data['cid']) - if 'returned' in data: - returned = data['returned'] - internal = super().to_internal_value(data) - if container: - internal['container'] = container - if returned: - internal['returned_at'] = timezone.now() - return internal - - def validate(self, attrs): - if not 'container' in attrs and not self.partial: - raise serializers.ValidationError("This field cannot be empty.") - return super().validate(attrs) - - def create(self, validated_data): - if 'dataImage' in validated_data: - file = File.objects.create(data=validated_data['dataImage']) - validated_data.pop('dataImage') - item = Item.objects.create(**validated_data) - item.files.set([file]) - return item - return Item.objects.create(**validated_data) - - def update(self, instance, validated_data): - if 'returned' in validated_data: - if validated_data['returned']: - validated_data['returned_at'] = timezone.now() - validated_data.pop('returned') - if 'dataImage' in validated_data: - file = File.objects.create(data=validated_data['dataImage']) - validated_data.pop('dataImage') - instance.files.add(file) - return super().update(instance, validated_data) - - @staticmethod - def get_timeline(obj): - timeline = [] - for comment in obj.comments.all(): - timeline.append({ - 'type': 'comment', - 'id': comment.id, - 'timestamp': comment.timestamp, - 'comment': comment.comment, - }) - for relation in (obj.issue_relation_changes.all()): - timeline.append({ - 'type': 'issue_relation', - 'id': relation.id, - 'status': relation.status, - 'timestamp': relation.timestamp, - 'issue_thread': BasicIssueSerializer(relation.issue_thread).data, - }) - for placement in (obj.container_history.all()): - timeline.append({ - 'type': 'placement', - 'id': placement.id, - 'timestamp': placement.timestamp, - 'cid': placement.container.id, - 'box': placement.container.name - }) - - if obj.created_at: - timeline.append({ - 'type': 'created', - 'timestamp': obj.created_at, - }) - if obj.returned_at: - timeline.append({ - 'type': 'returned', - 'timestamp': obj.returned_at, - }) - if obj.deleted_at: - timeline.append({ - 'type': 'deleted', - 'timestamp': obj.deleted_at, - }) - return sorted(timeline, key=lambda x: x['timestamp']) - - -class SearchResultSerializer(serializers.Serializer): - search_score = serializers.IntegerField() - search_matches = serializers.ListField(child=serializers.DictField()) - item = ItemSerializer() - - def to_representation(self, instance): - return {**ItemSerializer(instance['item']).data, 'search_score': instance['search_score'], - 'search_matches': instance['search_matches']} - - class Meta: - model = Item diff --git a/core/inventory/shared_serializers.py b/core/inventory/shared_serializers.py deleted file mode 100644 index 2cafd5f..0000000 --- a/core/inventory/shared_serializers.py +++ /dev/null @@ -1,31 +0,0 @@ -from rest_framework import serializers - -from inventory.models import Event, Item - - -class BasicItemSerializer(serializers.ModelSerializer): - cid = serializers.SerializerMethodField() - box = serializers.SerializerMethodField() - file = serializers.SerializerMethodField() - returned = serializers.SerializerMethodField(required=False) - event = serializers.SlugRelatedField(slug_field='slug', queryset=Event.objects.all(), - allow_null=True, required=False) - - class Meta: - model = Item - fields = ['cid', 'box', 'id', 'description', 'file', 'returned', 'event'] - read_only_fields = ['id'] - - def get_cid(self, instance): - return instance.container.id if instance.container else None - - def get_box(self, instance): - return instance.container.name if instance.container else None - - def get_file(self, instance): - if len(instance.files.all()) > 0: - return sorted(instance.files.all(), key=lambda x: x.created_at, reverse=True)[0].hash - return None - - def get_returned(self, instance): - return instance.returned_at is not None diff --git a/core/inventory/tests/__init__.py b/core/inventory/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/inventory/tests/v2/__init__.py b/core/inventory/tests/v2/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/inventory/tests/v2/test_api.py b/core/inventory/tests/v2/test_api.py deleted file mode 100644 index 6904164..0000000 --- a/core/inventory/tests/v2/test_api.py +++ /dev/null @@ -1,44 +0,0 @@ -from django.test import TestCase, Client -from django.contrib.auth.models import Permission -from knox.models import AuthToken - -from authentication.models import ExtendedUser - - -class ApiTest(TestCase): - - def setUp(self): - super().setUp() - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.user.save() - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - def test_root(self): - from core.settings import SYSTEM3_VERSION - response = self.client.get('/api/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()["framework_version"], SYSTEM3_VERSION) - - def test_events(self): - response = self.client.get('/api/2/events/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_containers(self): - response = self.client.get('/api/2/boxes/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_files(self): - response = self.client.get('/api/2/files/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_items(self): - from inventory.models import Event - Event.objects.create(slug='TEST1', name='Event') - response = self.client.get('/api/2/TEST1/items/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) diff --git a/core/inventory/tests/v2/test_containers.py b/core/inventory/tests/v2/test_containers.py deleted file mode 100644 index b74a4a7..0000000 --- a/core/inventory/tests/v2/test_containers.py +++ /dev/null @@ -1,66 +0,0 @@ -from django.test import TestCase, Client -from django.contrib.auth.models import Permission -from knox.models import AuthToken - -from authentication.models import ExtendedUser -from inventory.models import Container - - -class ContainerTestCase(TestCase): - - def setUp(self): - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - def test_empty(self): - response = self.client.get('/api/2/boxes/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_members(self): - Container.objects.create(name='BOX') - response = self.client.get('/api/2/boxes/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['id'], 1) - self.assertEqual(response.json()[0]['name'], 'BOX') - self.assertEqual(response.json()[0]['itemCount'], 0) - - def test_multi_members(self): - Container.objects.create(name='BOX 1') - Container.objects.create(name='BOX 2') - Container.objects.create(name='BOX 3') - response = self.client.get('/api/2/boxes/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 3) - - def test_create_container(self): - response = self.client.post('/api/2/box/', {'name': 'BOX'}) - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['id'], 1) - self.assertEqual(response.json()['name'], 'BOX') - self.assertEqual(response.json()['itemCount'], 0) - self.assertEqual(len(Container.objects.all()), 1) - self.assertEqual(Container.objects.all()[0].id, 1) - self.assertEqual(Container.objects.all()[0].name, 'BOX') - - def test_update_container(self): - box = Container.objects.create(name='BOX 1') - response = self.client.put(f'/api/2/box/{box.id}/', {'name': 'BOX 2'}, content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['id'], 1) - self.assertEqual(response.json()['name'], 'BOX 2') - self.assertEqual(response.json()['itemCount'], 0) - self.assertEqual(len(Container.objects.all()), 1) - self.assertEqual(Container.objects.all()[0].id, 1) - self.assertEqual(Container.objects.all()[0].name, 'BOX 2') - - def test_delete_container(self): - box = Container.objects.create(name='BOX 1') - Container.objects.create(name='BOX 2') - self.assertEqual(len(Container.objects.all()), 2) - response = self.client.delete(f'/api/2/box/{box.id}/') - self.assertEqual(response.status_code, 204) - self.assertEqual(len(Container.objects.all()), 1) diff --git a/core/inventory/tests/v2/test_events.py b/core/inventory/tests/v2/test_events.py deleted file mode 100644 index bd58515..0000000 --- a/core/inventory/tests/v2/test_events.py +++ /dev/null @@ -1,94 +0,0 @@ -from django.test import TestCase, Client -from inventory.models import Event - -client = Client() - - -class EventTestCase(TestCase): - - def test_empty(self): - response = client.get('/api/2/events/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_members(self): - Event.objects.create(slug='EVENT', name='Event') - response = client.get('/api/2/events/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['slug'], 'EVENT') - self.assertEqual(response.json()[0]['name'], 'Event') - - def test_multi_members(self): - Event.objects.create(slug='EVENT1', name='Event 1') - Event.objects.create(slug='EVENT2', name='Event 2') - Event.objects.create(slug='EVENT3', name='Event 3') - response = client.get('/api/2/events/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 3) - - def test_create_event(self): - response = client.post('/api/2/events/', {'slug': 'EVENT', 'name': 'Event'}) - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['slug'], 'EVENT') - self.assertEqual(response.json()['name'], 'Event') - self.assertEqual(len(Event.objects.all()), 1) - self.assertEqual(Event.objects.all()[0].slug, 'EVENT') - self.assertEqual(Event.objects.all()[0].name, 'Event') - - def test_update_event(self): - from rest_framework.test import APIClient - event = Event.objects.create(slug='EVENT1', name='Event 1') - response = APIClient().put(f'/api/2/events/{event.id}/', {'slug': 'EVENT2', 'name': 'Event 2 new'}) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['slug'], 'EVENT2') - self.assertEqual(response.json()['name'], 'Event 2 new') - self.assertEqual(len(Event.objects.all()), 1) - self.assertEqual(Event.objects.all()[0].slug, 'EVENT2') - self.assertEqual(Event.objects.all()[0].name, 'Event 2 new') - - def test_update_event(self): - from rest_framework.test import APIClient - event = Event.objects.create(slug='EVENT1', name='Event 1') - response = APIClient().patch(f'/api/2/events/{event.id}/', {'addresses': ['foo@bar.baz', 'foo1@bar.baz']}) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['slug'], 'EVENT1') - self.assertEqual(response.json()['name'], 'Event 1') - self.assertEqual(2, len(response.json()['addresses'])) - self.assertEqual('foo@bar.baz', response.json()['addresses'][0]) - self.assertEqual('foo1@bar.baz', response.json()['addresses'][1]) - self.assertEqual(len(Event.objects.all()), 1) - self.assertEqual(Event.objects.all()[0].slug, 'EVENT1') - self.assertEqual(Event.objects.all()[0].name, 'Event 1') - - def test_remove_event(self): - event = Event.objects.create(slug='EVENT1', name='Event 1') - Event.objects.create(slug='EVENT2', name='Event 2') - self.assertEqual(len(Event.objects.all()), 2) - response = client.delete(f'/api/2/events/{event.id}/') - self.assertEqual(response.status_code, 204) - self.assertEqual(len(Event.objects.all()), 1) - - def test_event_with_address(self): - from mail.models import EventAddress - event1 = Event.objects.create(slug='TEST1', name='Event') - EventAddress.objects.create(event=event1, address='foo@bar.baz') - response = self.client.get('/api/2/events/') - self.assertEqual(response.status_code, 200) - self.assertEqual(1, len(response.json())) - self.assertEqual('TEST1', response.json()[0]['slug']) - self.assertEqual('Event', response.json()[0]['name']) - self.assertEqual(1, len(response.json()[0]['addresses'])) - - def test_items_remove_addresss(self): - from mail.models import EventAddress - from rest_framework.test import APIClient - event1 = Event.objects.create(slug='TEST1', name='Event') - EventAddress.objects.create(event=event1, address='foo@bar.baz') - EventAddress.objects.create(event=event1, address='fo1o@bar.baz') - response = APIClient().patch(f'/api/2/events/{event1.id}/', {'addresses': ['foo1@bar.baz']}) - self.assertEqual(response.status_code, 200) - self.assertEqual('TEST1', response.json()['slug']) - self.assertEqual('Event', response.json()['name']) - self.assertEqual(1, len(response.json()['addresses'])) - self.assertEqual('foo1@bar.baz', response.json()['addresses'][0]) diff --git a/core/inventory/tests/v2/test_items.py b/core/inventory/tests/v2/test_items.py deleted file mode 100644 index 34c4739..0000000 --- a/core/inventory/tests/v2/test_items.py +++ /dev/null @@ -1,345 +0,0 @@ -from datetime import datetime, timedelta - -from django.utils import timezone -from django.test import TestCase, Client -from django.contrib.auth.models import Permission -from knox.models import AuthToken - -from authentication.models import ExtendedUser -from files.models import File -from inventory.models import Event, Container, Item, Comment, ItemPlacement - -from base64 import b64encode - -from tickets.models import IssueThread, ItemRelation - - -class ItemTestCase(TestCase): - - def setUp(self): - super().setUp() - self.event = Event.objects.create(slug='EVENT', name='Event') - self.box1 = Container.objects.create(name='BOX1') - self.box2 = Container.objects.create(name='BOX2') - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - self.issue = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - - def test_empty(self): - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, b'[]') - - def test_members_and_timeline(self): - now = datetime.now() - item = Item.objects.create(container=self.box1, event=self.event, description='1') - comment = Comment.objects.create( - item=item, - comment="test", - timestamp=now + timedelta(seconds=3), - ) - match = ItemRelation.objects.create( - issue_thread=self.issue, - item=item, - timestamp=now + timedelta(seconds=4), - ) - placement = ItemPlacement.objects.create( - container=self.box2, - item=item, - timestamp=now + timedelta(seconds=5), - ) - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['id'], item.id) - self.assertEqual(response.json()[0]['description'], '1') - self.assertEqual(response.json()[0]['box'], 'BOX2') - self.assertEqual(response.json()[0]['cid'], self.box2.id) - self.assertEqual(response.json()[0]['file'], None) - self.assertEqual(response.json()[0]['returned'], False) - self.assertEqual(response.json()[0]['event'], self.event.slug) - self.assertEqual(len(response.json()[0]['timeline']), 5) - self.assertEqual(response.json()[0]['timeline'][0]['type'], 'created') - self.assertEqual(response.json()[0]['timeline'][1]['type'], 'placement') - self.assertEqual(response.json()[0]['timeline'][2]['type'], 'comment') - self.assertEqual(response.json()[0]['timeline'][3]['type'], 'issue_relation') - self.assertEqual(response.json()[0]['timeline'][4]['type'], 'placement') - self.assertEqual(response.json()[0]['timeline'][2]['id'], comment.id) - self.assertEqual(response.json()[0]['timeline'][3]['id'], match.id) - self.assertEqual(response.json()[0]['timeline'][4]['id'], placement.id) - self.assertEqual(response.json()[0]['timeline'][1]['box'], 'BOX1') - self.assertEqual(response.json()[0]['timeline'][1]['cid'], self.box1.id) - self.assertEqual(response.json()[0]['timeline'][0]['timestamp'], item.created_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][2]['comment'], 'test') - self.assertEqual(response.json()[0]['timeline'][2]['timestamp'], comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][3]['status'], 'possible') - self.assertEqual(response.json()[0]['timeline'][3]['timestamp'], match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][3]['issue_thread']['name'], "test issue") - self.assertEqual(response.json()[0]['timeline'][3]['issue_thread']['event'], "EVENT") - self.assertEqual(response.json()[0]['timeline'][3]['issue_thread']['state'], "pending_new") - self.assertEqual(response.json()[0]['timeline'][4]['box'], 'BOX2') - self.assertEqual(response.json()[0]['timeline'][4]['cid'], self.box2.id) - self.assertEqual(response.json()[0]['timeline'][4]['timestamp'], - placement.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(len(response.json()[0]['related_issues']), 1) - self.assertEqual(response.json()[0]['related_issues'][0]['name'], "test issue") - self.assertEqual(response.json()[0]['related_issues'][0]['event'], "EVENT") - self.assertEqual(response.json()[0]['related_issues'][0]['state'], "pending_new") - - def test_members_with_file(self): - import base64 - item = Item.objects.create(container=self.box1, event=self.event, description='1') - file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['id'], item.id) - self.assertEqual(response.json()[0]['description'], '1') - self.assertEqual(response.json()[0]['box'], 'BOX1') - self.assertEqual(response.json()[0]['cid'], self.box1.id) - self.assertEqual(response.json()[0]['file'], file.hash) - self.assertEqual(response.json()[0]['returned'], False) - self.assertEqual(response.json()[0]['event'], self.event.slug) - self.assertEqual(len(response.json()[0]['related_issues']), 0) - - def test_members_with_two_file(self): - import base64 - item = Item.objects.create(container=self.box1, event=self.event, description='1') - file1 = File.objects.create(item=item, - data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) - file2 = File.objects.create(item=item, - data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8')) - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['id'], item.id) - self.assertEqual(response.json()[0]['description'], '1') - self.assertEqual(response.json()[0]['box'], 'BOX1') - self.assertEqual(response.json()[0]['cid'], self.box1.id) - self.assertEqual(response.json()[0]['file'], file2.hash) - self.assertEqual(response.json()[0]['returned'], False) - self.assertEqual(response.json()[0]['event'], self.event.slug) - self.assertEqual(len(response.json()[0]['related_issues']), 0) - - def test_multi_members(self): - Item.objects.create(container=self.box1, event=self.event, description='1') - Item.objects.create(container=self.box1, event=self.event, description='2') - Item.objects.create(container=self.box1, event=self.event, description='3') - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 3) - - def test_create_item(self): - response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box1.id, 'description': '1'}) - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['id'], 1) - self.assertEqual(response.json()['description'], '1') - self.assertEqual(response.json()['box'], 'BOX1') - self.assertEqual(response.json()['cid'], self.box1.id) - self.assertEqual(response.json()['file'], None) - self.assertEqual(response.json()['returned'], False) - self.assertEqual(response.json()['event'], self.event.slug) - self.assertEqual(len(response.json()['related_issues']), 0) - self.assertEqual(len(Item.objects.all()), 1) - self.assertEqual(Item.objects.all()[0].id, 1) - self.assertEqual(Item.objects.all()[0].description, '1') - self.assertEqual(Item.objects.all()[0].container.id, self.box1.id) - - def test_create_item_without_container(self): - response = self.client.post(f'/api/2/{self.event.slug}/item/', {'description': '1'}) - self.assertEqual(response.status_code, 400) - - def test_create_item_without_description(self): - response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box1.id}) - self.assertEqual(response.status_code, 400) - - def test_create_item_with_file(self): - import base64 - response = self.client.post(f'/api/2/{self.event.slug}/item/', - {'cid': self.box1.id, 'description': '1', - 'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode( - 'utf-8')}, content_type='application/json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['id'], 1) - self.assertEqual(response.json()['description'], '1') - self.assertEqual(response.json()['box'], 'BOX1') - self.assertEqual(response.json()['id'], self.box1.id) - self.assertEqual(len(response.json()['file']), 64) - self.assertEqual(len(Item.objects.all()), 1) - self.assertEqual(Item.objects.all()[0].id, 1) - self.assertEqual(Item.objects.all()[0].description, '1') - self.assertEqual(Item.objects.all()[0].container.id, self.box1.id) - self.assertEqual(len(File.objects.all()), 1) - - def test_update_item(self): - item = Item.objects.create(container=self.box1, event=self.event, description='1') - response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/', {'description': '2'}, - content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['id'], item.id) - self.assertEqual(response.json()['description'], '2') - self.assertEqual(response.json()['box'], 'BOX1') - self.assertEqual(response.json()['cid'], self.box1.id) - self.assertEqual(response.json()['file'], None) - self.assertEqual(response.json()['returned'], False) - self.assertEqual(response.json()['event'], self.event.slug) - self.assertEqual(len(response.json()['related_issues']), 0) - self.assertEqual(len(Item.objects.all()), 1) - self.assertEqual(Item.objects.all()[0].id, 1) - self.assertEqual(Item.objects.all()[0].description, '2') - self.assertEqual(Item.objects.all()[0].container.id, self.box1.id) - - def test_update_item_with_file(self): - import base64 - item = Item.objects.create(container=self.box1, event=self.event, description='1') - response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/', - {'description': '2', - 'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode( - 'utf-8')}, - content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['id'], 1) - self.assertEqual(response.json()['description'], '2') - self.assertEqual(response.json()['box'], 'BOX1') - self.assertEqual(response.json()['id'], self.box1.id) - self.assertEqual(len(response.json()['file']), 64) - self.assertEqual(len(Item.objects.all()), 1) - self.assertEqual(Item.objects.all()[0].id, 1) - self.assertEqual(Item.objects.all()[0].description, '2') - self.assertEqual(Item.objects.all()[0].container.id, self.box1.id) - self.assertEqual(len(File.objects.all()), 1) - - def test_delete_item(self): - item = Item.objects.create(container=self.box1, event=self.event, description='1') - Item.objects.create(container=self.box1, event=self.event, description='2') - self.assertEqual(len(Item.objects.all()), 2) - response = self.client.delete(f'/api/2/{self.event.slug}/item/{item.id}/') - self.assertEqual(response.status_code, 204) - self.assertEqual(len(Item.objects.all()), 1) - - def test_delete_item2(self): - Item.objects.create(container=self.box1, event=self.event, description='1') - item2 = Item.objects.create(container=self.box1, event=self.event, description='2') - self.assertEqual(len(Item.objects.all()), 2) - response = self.client.delete(f'/api/2/{self.event.slug}/item/{item2.id}/') - self.assertEqual(response.status_code, 204) - self.assertEqual(len(Item.objects.all()), 1) - item3 = Item.objects.create(container=self.box1, event=self.event, description='3') - self.assertEqual(item3.id, 3) - self.assertEqual(len(Item.objects.all()), 2) - - def test_item_count(self): - Item.objects.create(container=self.box1, event=self.event, description='1') - Item.objects.create(container=self.box1, event=self.event, description='2') - response = self.client.get('/api/2/boxes/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 2) - self.assertEqual(response.json()[0]['itemCount'], 2) - self.assertEqual(response.json()[1]['itemCount'], 0) - - def test_item_nonexistent(self): - response = self.client.get(f'/api/2/NOEVENT/item/') - self.assertEqual(response.status_code, 404) - - def test_item_return(self): - item = Item.objects.create(container=self.box1, event=self.event, description='1') - self.assertEqual(item.returned_at, None) - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/', {'returned': True}, - content_type='application/json') - self.assertEqual(response.status_code, 200) - item.refresh_from_db() - self.assertNotEqual(item.returned_at, None) - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 0) - - def test_item_show_not_returned(self): - item1 = Item.objects.create(container=self.box1, event=self.event, description='1') - item2 = Item.objects.create(container=self.box1, event=self.event, description='2') - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 2) - item2.returned_at = timezone.now() - item2.save() - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['id'], item1.id) - - -class ItemSearchTestCase(TestCase): - - def setUp(self): - super().setUp() - self.event = Event.objects.create(slug='EVENT', name='Event') - self.box1 = Container.objects.create(name='BOX1') - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - self.item1 = Item.objects.create(container=self.box1, event=self.event, description='abc def') - self.item2 = Item.objects.create(container=self.box1, event=self.event, description='def ghi') - self.item3 = Item.objects.create(container=self.box1, event=self.event, description='jkl mno pqr') - self.item4 = Item.objects.create(container=self.box1, event=self.event, description='stu vwx') - - def test_search(self): - search_query = b64encode(b'abc').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.json())) - self.assertEqual(self.item1.id, response.json()[0]['id']) - self.assertEqual('abc def', response.json()[0]['description']) - self.assertEqual('BOX1', response.json()[0]['box']) - self.assertEqual(self.box1.id, response.json()[0]['cid']) - self.assertEqual(1, response.json()[0]['search_score']) - - def test_search2(self): - search_query = b64encode(b'def').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(2, len(response.json())) - self.assertEqual(self.item1.id, response.json()[0]['id']) - self.assertEqual('abc def', response.json()[0]['description']) - self.assertEqual('BOX1', response.json()[0]['box']) - self.assertEqual(self.box1.id, response.json()[0]['cid']) - self.assertEqual(1, response.json()[0]['search_score']) - self.assertEqual(self.item2.id, response.json()[1]['id']) - self.assertEqual('def ghi', response.json()[1]['description']) - self.assertEqual('BOX1', response.json()[1]['box']) - self.assertEqual(self.box1.id, response.json()[1]['cid']) - self.assertEqual(1, response.json()[0]['search_score']) - - def test_search3(self): - search_query = b64encode(b'jkl').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.json())) - self.assertEqual(self.item3.id, response.json()[0]['id']) - self.assertEqual('jkl mno pqr', response.json()[0]['description']) - self.assertEqual('BOX1', response.json()[0]['box']) - self.assertEqual(self.box1.id, response.json()[0]['cid']) - self.assertEqual(1, response.json()[0]['search_score']) - - def test_search4(self): - search_query = b64encode(b'abc def').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(2, len(response.json())) - self.assertEqual(self.item1.id, response.json()[0]['id']) - self.assertEqual('abc def', response.json()[0]['description']) - self.assertEqual('BOX1', response.json()[0]['box']) - self.assertEqual(self.box1.id, response.json()[0]['cid']) - self.assertEqual(2, response.json()[0]['search_score']) - self.assertEqual(self.item2.id, response.json()[1]['id']) - self.assertEqual('def ghi', response.json()[1]['description']) - self.assertEqual('BOX1', response.json()[1]['box']) - self.assertEqual(self.box1.id, response.json()[1]['cid']) - self.assertEqual(1, response.json()[1]['search_score']) diff --git a/core/mail/__init__.py b/core/mail/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/mail/admin.py b/core/mail/admin.py deleted file mode 100644 index 55619c2..0000000 --- a/core/mail/admin.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.contrib import admin - -from mail.models import Email, EventAddress - - -class EmailAdmin(admin.ModelAdmin): - pass - - -admin.site.register(Email, EmailAdmin) - - -class EventAddressAdmin(admin.ModelAdmin): - pass - - -admin.site.register(EventAddress, EventAddressAdmin) diff --git a/core/mail/api_v2.py b/core/mail/api_v2.py deleted file mode 100644 index 0d96785..0000000 --- a/core/mail/api_v2.py +++ /dev/null @@ -1,26 +0,0 @@ -from rest_framework import routers, viewsets, serializers - -from mail.models import Email, EmailAttachment - - -class AttachmentSerializer(serializers.ModelSerializer): - class Meta: - model = EmailAttachment - fields = ['hash', 'mime_type', 'name'] - - -class EmailSerializer(serializers.ModelSerializer): - class Meta: - model = Email - fields = '__all__' - - -class EmailViewSet(viewsets.ModelViewSet): - serializer_class = EmailSerializer - queryset = Email.objects.all() - - -router = routers.SimpleRouter() -router.register(r'mails', EmailViewSet, basename='mails') - -urlpatterns = router.urls diff --git a/core/mail/migrations/0001_initial.py b/core/mail/migrations/0001_initial.py deleted file mode 100644 index 71c54e7..0000000 --- a/core/mail/migrations/0001_initial.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-09 02:13 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('inventory', '0001_initial'), - ('tickets', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='EventAddress', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('address', models.CharField(max_length=255)), - ('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.event')), - ], - ), - migrations.CreateModel( - name='Email', - fields=[ - ('is_deleted', models.BooleanField(default=False)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('body', models.TextField()), - ('subject', models.CharField(max_length=255)), - ('sender', models.CharField(max_length=255)), - ('recipient', models.CharField(max_length=255)), - ('reference', models.CharField(max_length=255, null=True, unique=True)), - ('in_reply_to', models.CharField(max_length=255, null=True)), - ('raw', models.TextField()), - ('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.event')), - ('issue_thread', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='emails', to='tickets.issuethread')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/core/mail/migrations/0002_printed_quotable.py b/core/mail/migrations/0002_printed_quotable.py deleted file mode 100644 index ce0d5da..0000000 --- a/core/mail/migrations/0002_printed_quotable.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-09 02:13 -import quopri - -from django.db import migrations - -from mail.protocol import unescape_and_decode_quoted_printable, unescape_and_decode_base64 - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ('mail', '0001_initial'), - ] - - def convert_printed_quotable(apps, schema_editor): - Email = apps.get_model('mail', 'Email') - for mail in Email.objects.all(): - mail.body = unescape_and_decode_quoted_printable(mail.body) - mail.body = unescape_and_decode_base64(mail.body) - mail.subject = unescape_and_decode_quoted_printable(mail.subject) - mail.subject = unescape_and_decode_base64(mail.subject) - mail.save() - IssueThread = apps.get_model('tickets', 'IssueThread') - for issue in IssueThread.objects.all(): - issue.name = unescape_and_decode_quoted_printable(issue.name) - issue.name = unescape_and_decode_base64(issue.name) - issue.save() - - operations = [ - migrations.RunPython(convert_printed_quotable), - ] diff --git a/core/mail/migrations/0003_emailattachment.py b/core/mail/migrations/0003_emailattachment.py deleted file mode 100644 index 7796876..0000000 --- a/core/mail/migrations/0003_emailattachment.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-09 20:56 - -from django.db import migrations, models -import django.db.models.deletion -import files.models -from mail.protocol import parse_email_body - - -class NullLogger: - def info(self, *args, **kwargs): - pass - - def warning(self, *args, **kwargs): - pass - - def debug(self, *args, **kwargs): - pass - - -class Migration(migrations.Migration): - dependencies = [ - ('mail', '0002_printed_quotable'), - ] - - def generate_email_attachments(apps, schema_editor): - Email = apps.get_model('mail', 'Email') - for email in Email.objects.all(): - raw = email.raw - if raw is None or raw == '': - continue - parsed, body, attachments = parse_email_body(raw.encode('utf-8'), NullLogger()) - email.attachments.clear() - for attachment in attachments: - email.attachments.add(attachment) - email.body = body - email.save() - - operations = [ - migrations.CreateModel( - name='EmailAttachment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(blank=True, null=True)), - ('updated_at', models.DateTimeField(blank=True, null=True)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('file', models.ImageField(upload_to=files.models.hash_upload)), - ('mime_type', models.CharField(max_length=255)), - ('hash', models.CharField(max_length=64, unique=True)), - ('name', models.CharField(max_length=255)), - ('email', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attachments', - to='mail.email')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(generate_email_attachments), - ] diff --git a/core/mail/migrations/0004_alter_emailattachment_file.py b/core/mail/migrations/0004_alter_emailattachment_file.py deleted file mode 100644 index 4342c8e..0000000 --- a/core/mail/migrations/0004_alter_emailattachment_file.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-10 19:04 - -from django.db import migrations, models -import files.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('mail', '0003_emailattachment'), - ] - - operations = [ - migrations.AlterField( - model_name='emailattachment', - name='file', - field=models.FileField(upload_to=files.models.hash_upload), - ), - ] diff --git a/core/mail/migrations/0005_alter_eventaddress_event.py b/core/mail/migrations/0005_alter_eventaddress_event.py deleted file mode 100644 index 30b79bf..0000000 --- a/core/mail/migrations/0005_alter_eventaddress_event.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.2.7 on 2024-11-03 18:30 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0004_alter_event_created_at_alter_item_created_at'), - ('mail', '0004_alter_emailattachment_file'), - ] - - operations = [ - migrations.AlterField( - model_name='eventaddress', - name='event', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='addresses', to='inventory.event'), - ), - ] diff --git a/core/mail/migrations/0006_email_raw_file.py b/core/mail/migrations/0006_email_raw_file.py deleted file mode 100644 index 4086af8..0000000 --- a/core/mail/migrations/0006_email_raw_file.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 4.2.7 on 2024-11-08 20:37 -from django.core.files.base import ContentFile -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('mail', '0005_alter_eventaddress_event'), - ] - - def move_raw_mails_to_file(apps, schema_editor): - Email = apps.get_model('mail', 'Email') - for email in Email.objects.all(): - raw_content = email.raw - path = "mail_{}".format(email.id) - if len(raw_content): - email.raw_file.save(path, ContentFile(raw_content)) - email.save() - - operations = [ - migrations.AddField( - model_name='email', - name='raw_file', - field=models.FileField(null=True, upload_to='raw_mail/'), - ), - migrations.RunPython(move_raw_mails_to_file), - migrations.RemoveField( - model_name='email', - name='raw', - ), - migrations.AlterField( - model_name='email', - name='raw_file', - field=models.FileField(upload_to='raw_mail/'), - ), - ] diff --git a/core/mail/migrations/__init__.py b/core/mail/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/mail/models.py b/core/mail/models.py deleted file mode 100644 index 36cd2b3..0000000 --- a/core/mail/models.py +++ /dev/null @@ -1,52 +0,0 @@ -import random - -from django.db import models -from django_softdelete.models import SoftDeleteModel - -from core.settings import MAIL_DOMAIN, ACTIVE_SPAM_TRAINING -from files.models import AbstractFile -from inventory.models import Event -from tickets.models import IssueThread - - -class Email(SoftDeleteModel): - id = models.AutoField(primary_key=True) - timestamp = models.DateTimeField(auto_now_add=True) - body = models.TextField() - subject = models.CharField(max_length=255) - sender = models.CharField(max_length=255) - recipient = models.CharField(max_length=255) - reference = models.CharField(max_length=255, null=True, unique=True) - in_reply_to = models.CharField(max_length=255, null=True) - raw_file = models.FileField(upload_to='raw_mail/') - issue_thread = models.ForeignKey(IssueThread, models.SET_NULL, null=True, related_name='emails') - event = models.ForeignKey(Event, models.SET_NULL, null=True) - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - if not self.reference: - self.reference = f'<{random.randint(0, 1000000000):09}@{MAIL_DOMAIN}>' - self.save() - - def train_spam(self): - if ACTIVE_SPAM_TRAINING and self.raw_file.path: - import subprocess - path = self.raw_file.path - subprocess.run(["rspamc", "learn_spam", path]) - - def train_ham(self): - if ACTIVE_SPAM_TRAINING and self.raw_file.path: - import subprocess - path = self.raw_file.path - subprocess.run(["rspamc", "learn_ham", path]) - - -class EventAddress(models.Model): - id = models.AutoField(primary_key=True) - event = models.ForeignKey(Event, models.SET_NULL, null=True, related_name='addresses') - address = models.CharField(max_length=255) - - -class EmailAttachment(AbstractFile): - email = models.ForeignKey(Email, models.CASCADE, related_name='attachments', null=True) - name = models.CharField(max_length=255) diff --git a/core/mail/protocol.py b/core/mail/protocol.py deleted file mode 100644 index 23ae696..0000000 --- a/core/mail/protocol.py +++ /dev/null @@ -1,330 +0,0 @@ -import logging -from re import match - -import aiosmtplib -from channels.layers import get_channel_layer -from channels.db import database_sync_to_async -from django.core.files.base import ContentFile - -from mail.models import Email, EventAddress, EmailAttachment -from notify_sessions.models import SystemEvent -from tickets.models import IssueThread - - -class SpecialMailException(Exception): - pass - - -def find_quoted_printable(s, marker): - positions = [i for i in range(len(s)) if s.lower().startswith('=?utf-8?' + marker + '?', i)] - for pos in positions: - end = s.find('?=', pos + 10) - if end == -1: - continue - yield pos, end + 3 - - -def unescape_and_decode_quoted_printable(s): - import quopri - decoded = '' - last_end = 0 - for start, end in find_quoted_printable(s, 'q'): - decoded += s[last_end:start] - decoded += quopri.decodestring(s[start + 10:end - 3]).decode('utf-8') - last_end = end - decoded += s[last_end:] - return decoded - - -def unescape_and_decode_base64(s): - import base64 - decoded = '' - last_end = 0 - for start, end in find_quoted_printable(s, 'b'): - decoded += s[last_end:start] - decoded += base64.b64decode(s[start + 10:end - 3]).decode('utf-8') - last_end = end - decoded += s[last_end:] - return decoded - - -def unescape_simplified_quoted_printable(s, encoding='utf-8'): - import quopri - return quopri.decodestring(s).decode(encoding) - - -def decode_inline_encodings(s): - s = unescape_and_decode_quoted_printable(s) - s = unescape_and_decode_base64(s) - return s - - -def ascii_strip(s): - if not s: - return None - return ''.join([c for c in str(s) if 128 > ord(c) > 31]) - - -def collect_references(issue_thread): - mails = issue_thread.emails.order_by('timestamp') - references = [] - for mail in mails: - if mail.reference: - references.append(mail.reference) - return references - - -def make_reply(reply_email, references=None, event=None): - from email.message import EmailMessage - from core.settings import MAIL_DOMAIN - event = event or "mail" - reply = EmailMessage() - reply["From"] = reply_email.sender - reply["To"] = reply_email.recipient - reply["Subject"] = reply_email.subject - reply["Reply-To"] = f"{event}@{MAIL_DOMAIN}" - if reply_email.in_reply_to: - reply["In-Reply-To"] = reply_email.in_reply_to - if reply_email.reference: - reply["Message-ID"] = reply_email.reference - else: - reply["Message-ID"] = reply_email.id + "@" + MAIL_DOMAIN - reply_email.reference = reply["Message-ID"] - reply_email.save() - if references: - reply["References"] = " ".join(references) - if reply_email.body != "": - reply.set_content(reply_email.body) - return reply - else: - raise SpecialMailException("mail content emty") - -async def send_smtp(message): - await aiosmtplib.send(message, hostname="127.0.0.1", port=25, use_tls=False, start_tls=False) - - -def find_active_issue_thread(in_reply_to, address, subject, event, spam=False): - from re import match - uuid_match = match(r'^ticket\+([a-f0-9-]{36})@', address) - if uuid_match: - issue = IssueThread.objects.filter(uuid=uuid_match.group(1)) - if issue.exists(): - return issue.first(), False - reply_to = Email.objects.filter(reference=in_reply_to) - if reply_to.exists(): - return reply_to.first().issue_thread, False - else: - issue = IssueThread.objects.create(name=subject, event=event, - initial_state='pending_suspected_spam' if spam else 'pending_new') - return issue, True - - -def find_target_event(address): - try: - address_map = EventAddress.objects.get(address=address) - if address_map.event: - return address_map.event - except EventAddress.DoesNotExist: - pass - return None - - -def decode_email_segment(segment, charset, transfer_encoding): - decode_as = 'utf-8' - if charset == 'windows-1251': - decode_as = 'cp1251' - elif charset == 'iso-8859-1': - decode_as = 'latin1' - if transfer_encoding == 'quoted-printable': - segment = unescape_simplified_quoted_printable(segment, decode_as) - elif transfer_encoding == 'base64': - import base64 - segment = base64.b64decode(segment).decode('utf-8') - else: - segment = decode_inline_encodings(segment.decode('utf-8')) - return segment - - -def parse_email_body(raw, log=None): - import email - from hashlib import sha256 - - attachments = [] - - parsed = email.message_from_bytes(raw) - body = "" - if parsed.is_multipart(): - for part in parsed.walk(): - ctype = part.get_content_type() - charset = part.get_content_charset() - cdispo = str(part.get('Content-Disposition')) - - if ctype == 'multipart/mixed': - log.debug("Ignoring Multipart %s %s", ctype, cdispo) - # skip any text/plain (txt) attachments - elif ctype == 'text/plain' and 'attachment' not in cdispo: - segment = part.get_payload() - if not segment: - continue - segment = decode_email_segment(segment.encode('utf-8'), charset, part.get('Content-Transfer-Encoding')) - log.debug(segment) - body = body + segment - elif 'attachment' in cdispo or 'inline' in cdispo: - content = part.get_payload(decode=True) - if content is None: - continue - file = ContentFile(content) - chash = sha256(file.read()).hexdigest() - name = part.get_filename() - if name is None: - name = "unnamed" - attachment, _ = EmailAttachment.objects.get_or_create( - name=name, mime_type=ctype, file=file, hash=chash) - attachment.save() - attachments.append(attachment) - if 'inline' in cdispo: - body = body + f'' - log.info("Image %s %s", ctype, attachment.id) - else: - log.info("Attachment %s %s", ctype, cdispo) - else: - if parsed.get_content_type() == 'text/plain': - body = parsed.get_payload() - elif parsed.get_content_type() == 'text/html': - from bs4 import BeautifulSoup - import re - body = parsed.get_payload() - soup = BeautifulSoup(body, 'html.parser') - body = re.sub(r'([\r\n]+.?)*[\r\n]', r'\n', soup.get_text()).strip('\n') - else: - log.warning("Unknown content type %s", parsed.get_content_type()) - body = "Unknown content type" - body = decode_email_segment(body.encode('utf-8'), parsed.get_content_charset(), - parsed.get('Content-Transfer-Encoding')) - log.debug(body) - - return parsed, body, attachments - - -@database_sync_to_async -def receive_email(envelope, log=None): - parsed, body, attachments = parse_email_body(envelope.content, log) - - header_from = parsed.get('From') - header_to = parsed.get('To') - header_in_reply_to = ascii_strip(parsed.get('In-Reply-To')) - header_message_id = ascii_strip(parsed.get('Message-ID')) - maybe_spam = parsed.get('X-Spam') - suspected_spam = (maybe_spam and maybe_spam.lower() == 'yes') - - if match(r'^([a-zA-Z ]*<)?MAILER-DAEMON@', header_from) and envelope.mail_from.strip("<>") == "": - log.warning("Ignoring mailer daemon") - raise SpecialMailException("Ignoring mailer daemon") - - if Email.objects.filter(reference=header_message_id).exists(): # break before issue thread is created - log.warning("Email already exists") - raise SpecialMailException("Email already exists") - - recipient = envelope.rcpt_tos[0].lower() if envelope.rcpt_tos else header_to.lower() - sender = envelope.mail_from if envelope.mail_from else header_from - subject = ascii_strip(parsed.get('Subject')) - if not subject: - subject = "No subject" - subject = decode_inline_encodings(subject) - recipient = decode_inline_encodings(recipient) - sender = decode_inline_encodings(sender) - target_event = find_target_event(recipient) - - active_issue_thread, new = find_active_issue_thread( - header_in_reply_to, recipient, subject, target_event, suspected_spam) - - from hashlib import sha256 - random_filename = 'mail-' + sha256(envelope.content).hexdigest() - - email = Email.objects.create( - sender=sender, recipient=recipient, body=body, subject=subject, reference=header_message_id, - in_reply_to=header_in_reply_to, raw_file=ContentFile(envelope.content, name=random_filename), - event=target_event, - issue_thread=active_issue_thread) - for attachment in attachments: - email.attachments.add(attachment) - email.save() - - reply = None - if new: - # auto reply if new issue - references = collect_references(active_issue_thread) - if not sender.startswith('noreply') and not sender.startswith('no-reply') and not suspected_spam: - subject = f"Re: {subject} [#{active_issue_thread.short_uuid()}]" - body = '''Your request (#{}) has been received and will be reviewed by our lost&found angels. - -We are reviewing incoming requests during the event and teardown. Immediately after the event, expect a delay as the \ -workload is high. We will not forget about your request and get back in touch once we have updated information on your \ -request. Requests for devices, wallets, credit cards or similar items will be handled with priority. - -If you happen to find your lost item or just want to add additional information, please reply to this email. Please \ -do not create a new request. - -Your c3lf (Cloakroom + Lost&Found) Team'''.format(active_issue_thread.short_uuid()) - reply_email = Email.objects.create( - sender=recipient, recipient=sender, body=body, subject=subject, - in_reply_to=header_message_id, event=target_event, issue_thread=active_issue_thread) - reply = make_reply(reply_email, references, event=target_event.slug if target_event else None) - else: - # change state if not new - if active_issue_thread.state != 'pending_new': - active_issue_thread.state = 'pending_open' - active_issue_thread.save() - - return email, new, reply, active_issue_thread - - -class LMTPHandler: - async def handle_RCPT(self, server, session, envelope, address, rcpt_options): - from core.settings import MAIL_DOMAIN - address = address.lower() - if not address.endswith('@' + MAIL_DOMAIN): - return '550 not relaying to that domain' - envelope.rcpt_tos.append(address) - return '250 OK' - - async def handle_DATA(self, server, session, envelope): - log = logging.getLogger('mail.log') - log.setLevel(logging.DEBUG) - log.info('Message from %s' % envelope.mail_from) - log.info('Message for %s' % envelope.rcpt_tos) - log.info('Message data:\n') - - content = None - try: - content = envelope.content - email, new, reply, thread = await receive_email(envelope, log) - log.info(f"Created email {email.id}") - systemevent = await database_sync_to_async(SystemEvent.objects.create)(type='email received', - reference=email.id) - log.info(f"Created system event {systemevent.id}") - #channel_layer = get_channel_layer() - #await channel_layer.group_send( - # 'general', {"type": "generic.event", "name": "send_message_to_frontend", "event_id": systemevent.id, - # "message": "email received"}) - log.info(f"Sent message to frontend") - if new and reply: - log.info('Sending message to %s' % reply['To']) - await send_smtp(reply) - log.info("Sent auto reply") - - return '250 Message accepted for delivery' - except SpecialMailException as e: - import uuid - random_filename = 'special-' + str(uuid.uuid4()) - with open(random_filename, 'wb') as f: - f.write(content) - log.warning(f"Special mail exception: {e} saved to {random_filename}") - return '250 Message accepted for delivery' - except Exception as e: - from hashlib import sha256 - random_filename = 'mail-' + sha256(content).hexdigest() - with open(random_filename, 'wb') as f: - f.write(content) - log.error(f"Saved email to {random_filename} because of error %s (%s)", e, type(e)) - return '451 Internal server error' diff --git a/core/mail/socket.py b/core/mail/socket.py deleted file mode 100644 index 312b916..0000000 --- a/core/mail/socket.py +++ /dev/null @@ -1,31 +0,0 @@ -from abc import ABCMeta - -from aiosmtpd.controller import BaseController, UnixSocketMixin -from aiosmtpd.lmtp import LMTP - - -class BaseAsyncController(BaseController, metaclass=ABCMeta): - def __init__( - self, - handler, - loop, - **SMTP_parameters, - ): - super().__init__( - handler, - loop, - **SMTP_parameters, - ) - - def serve(self): - return self._create_server() - - -class UnixSocketLMTPController(UnixSocketMixin, BaseAsyncController): - def factory(self): - return LMTP(self.handler) - - def _trigger_server(self): # pragma: no-unixsock - # Prevent confusion on which _trigger_server() to invoke. - # Or so LGTM.com claimed - UnixSocketMixin._trigger_server(self) diff --git a/core/mail/tests/__init__.py b/core/mail/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/mail/tests/v2/__init__.py b/core/mail/tests/v2/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/mail/tests/v2/test_mails.py b/core/mail/tests/v2/test_mails.py deleted file mode 100644 index 95d35cb..0000000 --- a/core/mail/tests/v2/test_mails.py +++ /dev/null @@ -1,1204 +0,0 @@ -import inspect -from unittest import mock - -from django.test import TestCase, Client -from django.contrib.auth.models import Permission -from knox.models import AuthToken - -from authentication.models import ExtendedUser -from core.settings import MAIL_DOMAIN -from inventory.models import Event -from mail.models import Email, EventAddress, EmailAttachment -from mail.protocol import LMTPHandler -from tickets.models import IssueThread, StateChange - -expected_auto_reply_subject = 'Re: {} [#{}]' - -expected_auto_reply = '''Your request (#{}) has been received and will be reviewed by our lost&found angels. - -We are reviewing incoming requests during the event and teardown. Immediately after the event, expect a delay as the \ -workload is high. We will not forget about your request and get back in touch once we have updated information on your \ -request. Requests for devices, wallets, credit cards or similar items will be handled with priority. - -If you happen to find your lost item or just want to add additional information, please reply to this email. Please \ -do not create a new request. - -Your c3lf (Cloakroom + Lost&Found) Team''' - - -def make_mocked_coro(return_value=mock.sentinel, raise_exception=mock.sentinel): - async def mock_coro(*args, **kwargs): - if raise_exception is not mock.sentinel: - raise raise_exception - if not inspect.isawaitable(return_value): - return return_value - await return_value - - return mock.Mock(wraps=mock_coro) - - -class EmailsApiTest(TestCase): - - def setUp(self): - super().setUp() - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.user.save() - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - def test_mails(self): - Event.objects.get_or_create( - name="Test event", - slug="test-event", - ) - Email.objects.create( - subject='test', - body='test', - sender='test', - recipient='test', - ) - response = self.client.get('/api/2/mails/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['subject'], 'test') - self.assertEqual(response.json()[0]['body'], 'test') - self.assertEqual(response.json()[0]['sender'], 'test') - self.assertEqual(response.json()[0]['recipient'], 'test') - - def test_mails_empty(self): - response = self.client.get('/api/2/mails/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - -class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test - - def setUp(self): - super().setUp() - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.user.save() - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - def test_handle_client(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@localhost'] - envelope.content = b'Subject: test\nFrom: test3@test\nTo: test4@localhost\nMessage-ID: <1@test>\n\ntest' - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@localhost', Email.objects.all()[0].recipient) - self.assertEqual('test', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@localhost', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - - def test_handle_quoted_printable(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'Subject: test =?utf-8?Q?=C3=A4?=\nFrom: test3@test\nTo: test4@test\nMessage-ID: <1@test>\n\nText mit Quoted-Printable-Kodierung: =?utf-8?Q?=C3=A4=C3=B6=C3=BC=C3=9F?=' - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual('test ä', Email.objects.all()[0].subject) - self.assertEqual('Text mit Quoted-Printable-Kodierung: äöüß', Email.objects.all()[0].body) - self.assertTrue(Email.objects.all()[0].raw_file.path) - - def test_handle_quoted_printable_2(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'Subject: =?UTF-8?Q?suche_M=C3=BCtze?=\nFrom: test3@test\nTo: test4@test\nMessage-ID: <1@test>\n\nText mit Quoted-Printable-Kodierung: =?utf-8?Q?=C3=A4=C3=B6=C3=BC=C3=9F?=' - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual('suche_Mütze', Email.objects.all()[0].subject) - self.assertEqual('Text mit Quoted-Printable-Kodierung: äöüß', Email.objects.all()[0].body) - self.assertTrue(Email.objects.all()[0].raw_file.path) - - def test_handle_base64_inline(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'Subject: =?utf-8?B?dGVzdA==?=\nFrom: test3@test\nTo: test4@test\nMessage-ID: <1@test>\n\nText mit Base64-Kodierung: =?utf-8?B?w6TDtsO8w58=?=' - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('Text mit Base64-Kodierung: äöüß', Email.objects.all()[0].body) - self.assertTrue(Email.objects.all()[0].raw_file.path) - - def test_handle_base64_transfer_encoding(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'''Subject: test -From: test3@test -To: test4@test -Message-ID: <1@test> -Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: base64 - -VGVzdCBtaXQgQmFzZTY0LUtvZGllcnVuZzogw6TDtsO8w58=''' - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('Test mit Base64-Kodierung: äöüß', Email.objects.all()[0].body) - self.assertTrue(Email.objects.all()[0].raw_file.path) - - def test_handle_client_reply(self): - issue_thread = IssueThread.objects.create( - name="test", - ) - mail1 = Email.objects.create( - subject='test subject', - body='test', - sender='test1@test', - recipient='test2@test', - issue_thread=issue_thread, - ) - mail1_reply = Email.objects.create( - subject='Message received', - body='Thank you for your message.', - sender='test2@test', - recipient='test1@test', - in_reply_to=mail1.reference, - issue_thread=issue_thread, - ) - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = (f'Subject: Re: test\nFrom: test3@test\nTo: test4@test\nMessage-ID: <3@test>\n' - f'In-Reply-To: {mail1_reply.reference}'.encode('utf-8') + b'\n\ntest') - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 3) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_not_called() - self.assertEqual(Email.objects.all()[2].subject, 'Re: test') - self.assertEqual(Email.objects.all()[2].sender, 'test1@test') - self.assertEqual(Email.objects.all()[2].recipient, 'test2@test') - self.assertEqual(Email.objects.all()[2].body, 'test') - self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread) - self.assertEqual(Email.objects.all()[2].reference, '<3@test>') - self.assertEqual(Email.objects.all()[2].in_reply_to, mail1_reply.reference) - self.assertEqual(IssueThread.objects.all()[0].name, 'test') - self.assertEqual(IssueThread.objects.all()[0].state, 'pending_new') - self.assertEqual(IssueThread.objects.all()[0].assigned_to, None) - self.assertTrue(Email.objects.all()[2].raw_file.path) - - def test_handle_client_reply_2(self): - issue_thread = IssueThread.objects.create( - name="test", - ) - mail1 = Email.objects.create( - subject='test subject', - body='test', - sender='test1@test', - recipient='test2@test', - issue_thread=issue_thread, - ) - mail1_reply = Email.objects.create( - subject='Message received', - body='Thank you for your message.', - sender='test2@test', - recipient='test1@test', - in_reply_to=mail1.reference, - issue_thread=issue_thread, - ) - StateChange.objects.create( - issue_thread=issue_thread, - state='waiting_details', - ) - self.assertEqual(IssueThread.objects.all()[0].state, 'waiting_details') - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = (f'Subject: Re: test\nFrom: test3@test\nTo: test4@test\nMessage-ID: <3@test>\n' - f'In-Reply-To: {mail1_reply.reference}'.encode('utf-8') + b'\n\ntest') - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 3) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_not_called() - self.assertEqual(Email.objects.all()[2].subject, 'Re: test') - self.assertEqual(Email.objects.all()[2].sender, 'test1@test') - self.assertEqual(Email.objects.all()[2].recipient, 'test2@test') - self.assertEqual(Email.objects.all()[2].body, 'test') - self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread) - self.assertEqual(Email.objects.all()[2].reference, '<3@test>') - self.assertEqual(Email.objects.all()[2].in_reply_to, mail1_reply.reference) - self.assertEqual(IssueThread.objects.all()[0].name, 'test') - self.assertEqual(IssueThread.objects.all()[0].state, 'pending_open') - self.assertEqual(IssueThread.objects.all()[0].assigned_to, None) - self.assertTrue(Email.objects.all()[2].raw_file.path) - - def test_mail_reply(self): - issue_thread = IssueThread.objects.create( - name="test subject", - ) - mail1 = Email.objects.create( - subject='test subject', - body='test', - sender='test1@test', - recipient='test2@localhost', - issue_thread=issue_thread, - ) - mail1_reply = Email.objects.create( - subject='Message received', - body='Thank you for your message.', - sender='test2@localhost', - recipient='test1@test', - in_reply_to=mail1.reference, - issue_thread=issue_thread, - ) - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - response = self.client.post(f'/api/2/tickets/{issue_thread.id}/reply/', { - 'message': 'test' - }) - self.assertEqual(response.status_code, 201) - self.assertEqual(len(Email.objects.all()), 3) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual(Email.objects.all()[0].subject, 'test subject') - self.assertEqual(Email.objects.all()[0].sender, 'test1@test') - self.assertEqual(Email.objects.all()[0].recipient, 'test2@localhost') - self.assertEqual(Email.objects.all()[0].body, 'test') - self.assertEqual(Email.objects.all()[0].issue_thread, issue_thread) - self.assertEqual(Email.objects.all()[0].reference, mail1.reference) - self.assertEqual(Email.objects.all()[1].subject, 'Message received') - self.assertEqual(Email.objects.all()[1].sender, 'test2@localhost') - self.assertEqual(Email.objects.all()[1].recipient, 'test1@test') - self.assertEqual(Email.objects.all()[1].body, 'Thank you for your message.') - self.assertEqual(Email.objects.all()[1].issue_thread, issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual(Email.objects.all()[1].in_reply_to, mail1.reference) - self.assertEqual(Email.objects.all()[2].subject, 'Re: test subject [#{0}]'.format(issue_thread.short_uuid())) - self.assertEqual(Email.objects.all()[2].sender, 'test2@localhost') - self.assertEqual(Email.objects.all()[2].recipient, 'test1@test') - self.assertEqual(Email.objects.all()[2].body, 'test') - self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread) - self.assertTrue(Email.objects.all()[2].reference.startswith("<")) - self.assertTrue(Email.objects.all()[2].reference.endswith("@localhost>")) - self.assertEqual(Email.objects.all()[2].in_reply_to, mail1.reference) - - def test_match_event(self): - event = Event.objects.create( - name="Test event", - slug="test-event", - ) - event_address = EventAddress.objects.create( - event=event, - address="test_event@localhost", - ) - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test_event@localhost'] - envelope.content = b'Subject: test\nFrom: test1@test\nTo: test_event@localhost\nMessage-ID: <1@test>\n\ntest' - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual(event, Email.objects.all()[0].event) - self.assertEqual(event, Email.objects.all()[1].event) - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test_event@localhost', Email.objects.all()[0].recipient) - self.assertEqual('test_event@localhost', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual('test', Email.objects.all()[0].body) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - self.assertEqual(1, len(IssueThread.objects.all())) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - self.assertEqual(event, IssueThread.objects.all()[0].event) - - def test_mail_html_body(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - - aiosmtplib.send = make_mocked_coro() - - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -Content-Type: text/html; charset=utf-8 - -
-
-

test

-
-
''' - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - self.assertEqual(len(EmailAttachment.objects.all()), 0) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('test', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - - def test_split_text_inline_image(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -Content-Type: multipart/alternative; boundary="abc" - ---abc -Content-Type: text/plain; charset=utf-8 - -test1 - ---abc -Content-Type: image/jpeg; name="test.jpg" -Content-Disposition: inline; filename="test.jpg" -Content-Transfer-Encoding: base64 -Content-ID: <1> -X-Attachment-Id: 1 - -dGVzdGltYWdl - ---abc -Content-Type: text/plain; charset=utf-8 - -test2 - ---abc--''' - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('test1\ntest2\n', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - self.assertEqual(1, len(EmailAttachment.objects.all())) - self.assertEqual(1, EmailAttachment.objects.all()[0].id) - self.assertEqual('image/jpeg', EmailAttachment.objects.all()[0].mime_type) - self.assertEqual('test.jpg', EmailAttachment.objects.all()[0].name) - file_content = EmailAttachment.objects.all()[0].file.read() - self.assertEqual(b'testimage', file_content) - - def test_text_with_attachment(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -Content-Type: multipart/mixed; boundary="abc" - ---abc -Content-Type: text/plain; charset=utf-8 - -test1 - ---abc -Content-Type: image/jpeg; name="test.jpg" -Content-Disposition: attachment; filename="test.jpg" -Content-Transfer-Encoding: base64 -Content-ID: <1> -X-Attachment-Id: 1 - -dGVzdGltYWdl - ---abc--''' - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('test1\n', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - self.assertEqual(1, len(EmailAttachment.objects.all())) - self.assertEqual(1, EmailAttachment.objects.all()[0].id) - self.assertEqual('image/jpeg', EmailAttachment.objects.all()[0].mime_type) - self.assertEqual('test.jpg', EmailAttachment.objects.all()[0].name) - file_content = EmailAttachment.objects.all()[0].file.read() - self.assertEqual(b'testimage', file_content) - - def test_mail_noreply(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'noreply@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'Subject: test\nFrom: noreply@test\nTo: test2@test\nMessage-ID: <1@test>\n\ntest' - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 1) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_not_called() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('noreply@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('test', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - - def test_mail_empty_subject(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - import logging - logging.disable(logging.CRITICAL) - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'From: noreply@test\nTo: test2@test\nMessage-ID: <1@test>\n\ntest' - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - logging.disable(logging.NOTSET) - self.assertEqual('250 Message accepted for delivery', result) - self.assertEqual(2, len(Email.objects.all())) - self.assertEqual(1, len(IssueThread.objects.all())) - aiosmtplib.send.assert_called_once() - self.assertEqual('No subject', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('test', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('No subject', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('No subject', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - - def test_mail_empty_body(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - import logging - logging.disable(logging.CRITICAL) - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = '' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'Subject: test\nFrom: \nTo: test2@test\nMessage-ID: <1@test>\n\n' - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - logging.disable(logging.NOTSET) - self.assertEqual('250 Message accepted for delivery', result) - self.assertEqual(2, len(Email.objects.all())) - self.assertEqual(1, len(IssueThread.objects.all())) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - - def test_mail_plus_issue_thread(self): - issue_thread = IssueThread.objects.create( - name="test subject", - ) - mail1 = Email.objects.create( - subject='test subject', - body='test', - sender='test1@test', - recipient='test2@localhost', - issue_thread=issue_thread, - ) - mail2 = Email.objects.create( - subject='Message received', - body='Thank you for your message.', - sender='test2@localhost', - recipient='test1@test', - in_reply_to=mail1.reference, - issue_thread=issue_thread, - ) - Email.objects.create( - subject='Re: Message received', - body='any updates?', - sender='test3@test', - recipient='test2@localhost', - in_reply_to=mail2.reference, - issue_thread=issue_thread, - ) - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - import logging - logging.disable(logging.CRITICAL) - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = '' - envelope.rcpt_tos = ['ticket+{}@test'.format(issue_thread.uuid)] - envelope.content = (f'Subject: foo\nFrom: \nTo: ticket+{issue_thread.uuid}@test\n' - f'Message-ID: <3@test>\n\nbar'.encode('utf-8')) - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - logging.disable(logging.NOTSET) - self.assertEqual('250 Message accepted for delivery', result) - self.assertEqual(4, len(Email.objects.all())) - self.assertEqual(4, len(Email.objects.filter(issue_thread=issue_thread))) - self.assertEqual(1, len(IssueThread.objects.all())) - aiosmtplib.send.assert_not_called() - self.assertEqual(Email.objects.all()[3].subject, 'foo') - self.assertEqual(Email.objects.all()[3].sender, '') - self.assertEqual(Email.objects.all()[3].recipient, 'ticket+{}@test'.format(issue_thread.uuid)) - self.assertEqual(Email.objects.all()[3].body, 'bar') - self.assertEqual(Email.objects.all()[3].issue_thread, issue_thread) - self.assertEqual(Email.objects.all()[3].reference, '<3@test>') - self.assertEqual('test subject', IssueThread.objects.all()[0].name) - response = self.client.post(f'/api/2/tickets/{issue_thread.id}/reply/', { - 'message': 'test' - }) - self.assertEqual(response.status_code, 201) - self.assertEqual(5, len(Email.objects.all())) - self.assertEqual(5, len(Email.objects.filter(issue_thread=issue_thread))) - self.assertEqual(1, len(IssueThread.objects.all())) - self.assertEqual(Email.objects.all()[4].subject, 'Re: test subject [#{0}]'.format(issue_thread.short_uuid())) - self.assertEqual(Email.objects.all()[4].sender, 'test2@localhost') - self.assertEqual(Email.objects.all()[4].recipient, 'test1@test') - self.assertEqual(Email.objects.all()[4].body, 'test') - self.assertEqual(Email.objects.all()[4].issue_thread, issue_thread) - self.assertTrue(Email.objects.all()[4].reference.startswith("<")) - self.assertTrue(Email.objects.all()[4].reference.endswith("@localhost>")) - self.assertEqual(Email.objects.all()[4].in_reply_to, mail1.reference) - self.assertEqual('test subject', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - aiosmtplib.send.assert_called_once() - - def test_mail_spam_header(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -X-Spam: Yes - -test''' - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 1) # do not send auto reply if spam is suspected - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_not_called() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('test', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_suspected_spam', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_suspected_spam', states[0].state) - - def test_mail_4byte_unicode_emoji(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - - aiosmtplib.send = make_mocked_coro() - - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -Content-Type: text/html; charset=utf-8 - -thank you =?utf-8?Q?=F0=9F=98=8A?=''' # thank you 😊 - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual('250 Message accepted for delivery', result) - self.assertEqual(2, len(Email.objects.all())) - self.assertEqual(1, len(IssueThread.objects.all())) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('thank you 😊', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - - def test_mail_non_utf8(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - - aiosmtplib.send = make_mocked_coro() - - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -Content-Type: text/html; charset=iso-8859-1 - -hello \xe4\xf6\xfc''' - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual('250 Message accepted for delivery', result) - self.assertEqual(2, len(Email.objects.all())) - self.assertEqual(1, len(IssueThread.objects.all())) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('hello äöü', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - - def test_mail_windows_1252(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - - aiosmtplib.send = make_mocked_coro() - - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -Content-Type: text/html; charset=windows-1252 -Content-Transfer-Encoding: quoted-printable - -=0D=0Ahello=''' - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual('250 Message accepted for delivery', result) - self.assertEqual(2, len(Email.objects.all())) - self.assertEqual(1, len(IssueThread.objects.all())) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('\r\nhello', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - - def test_mail_quoted_printable_transfer_encoding(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - - aiosmtplib.send = make_mocked_coro() - - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -Content-Type: text/html; charset=utf-8 -Content-Transfer-Encoding: quoted-printable - -hello =C3=A4=C3=B6=C3=BC''' - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual('250 Message accepted for delivery', result) - self.assertEqual(2, len(Email.objects.all())) - self.assertEqual(1, len(IssueThread.objects.all())) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('hello äöü', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - - def test_text_with_attachment2(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - aiosmtplib.send = make_mocked_coro() - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -Content-Type: multipart/mixed; boundary="abc" -Content-Disposition: inline -Content-Transfer-Encoding: 8bit - ---abc -Content-Type: text/plain; charset=utf-8 -Content-Disposition: inline -Content-Transfer-Encoding: 8bit - -test1 - ---abc -Content-Type: image/jpeg; name="test.jpg" -Content-Disposition: attachment; filename="test.jpg" -Content-Transfer-Encoding: base64 -Content-ID: <1> -X-Attachment-Id: 1 - -dGVzdGltYWdl - ---abc--''' - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('test1\n', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) - self.assertEqual(1, len(EmailAttachment.objects.all())) - self.assertEqual(1, EmailAttachment.objects.all()[0].id) - self.assertEqual('image/jpeg', EmailAttachment.objects.all()[0].mime_type) - self.assertEqual('test.jpg', EmailAttachment.objects.all()[0].name) - file_content = EmailAttachment.objects.all()[0].file.read() - self.assertEqual(b'testimage', file_content) - - - def test_text_non_utf8_in_multipart(self): - from aiosmtpd.smtp import Envelope - from asgiref.sync import async_to_sync - import aiosmtplib - - aiosmtplib.send = make_mocked_coro() - - handler = LMTPHandler() - server = mock.Mock() - session = mock.Mock() - envelope = Envelope() - - envelope.mail_from = 'test1@test' - envelope.rcpt_tos = ['test2@test'] - - envelope.content = b'''Subject: test -From: test1@test -To: test2@test -Message-ID: <1@test> -Content-Type: multipart/alternative; boundary="abc" - ---abc -Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: 8bit - -test1 - ---abc -Content-Type: text/plain; charset=iso-8859-1 -Content-Transfer-Encoding: quoted-printable - -hello =E4 - ---abc -Content-Type: text/plain; charset=windows-1252 -Content-Transfer-Encoding: quoted-printable - -=0D=0Ahello - ---abc--''' - - result = async_to_sync(handler.handle_DATA)(server, session, envelope) - self.assertEqual(result, '250 Message accepted for delivery') - self.assertEqual(len(Email.objects.all()), 2) - self.assertEqual(len(IssueThread.objects.all()), 1) - aiosmtplib.send.assert_called_once() - self.assertEqual('test', Email.objects.all()[0].subject) - self.assertEqual('test1@test', Email.objects.all()[0].sender) - self.assertEqual('test2@test', Email.objects.all()[0].recipient) - self.assertEqual('test1\nhello ä\n\r\nhello\n', Email.objects.all()[0].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) - self.assertEqual('<1@test>', Email.objects.all()[0].reference) - self.assertEqual(None, Email.objects.all()[0].in_reply_to) - self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].subject) - self.assertEqual('test2@test', Email.objects.all()[1].sender) - self.assertEqual('test1@test', Email.objects.all()[1].recipient) - self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), - Email.objects.all()[1].body) - self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) - self.assertTrue(Email.objects.all()[1].reference.startswith("<")) - self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) - self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) - self.assertEqual('test', IssueThread.objects.all()[0].name) - self.assertEqual('pending_new', IssueThread.objects.all()[0].state) - self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) - states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) - self.assertEqual(1, len(states)) - self.assertEqual('pending_new', states[0].state) diff --git a/core/manage.py b/core/manage.py deleted file mode 100755 index f2a662c..0000000 --- a/core/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/core/notify_sessions/__init__.py b/core/notify_sessions/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/notify_sessions/admin.py b/core/notify_sessions/admin.py deleted file mode 100644 index 5f518e7..0000000 --- a/core/notify_sessions/admin.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.contrib import admin - -from notify_sessions.models import SystemEvent - - -class SystemEventAdmin(admin.ModelAdmin): - pass - - -admin.site.register(SystemEvent, SystemEventAdmin) diff --git a/core/notify_sessions/api_v2.py b/core/notify_sessions/api_v2.py deleted file mode 100644 index 30ff737..0000000 --- a/core/notify_sessions/api_v2.py +++ /dev/null @@ -1,21 +0,0 @@ -from rest_framework import routers, viewsets, serializers - -from tickets.models import IssueThread -from notify_sessions.models import SystemEvent - - -class SystemEventSerializer(serializers.ModelSerializer): - class Meta: - model = SystemEvent - fields = '__all__' - - -class SystemEventViewSet(viewsets.ModelViewSet): - serializer_class = SystemEventSerializer - queryset = SystemEvent.objects.all() - - -router = routers.SimpleRouter() -router.register(r'systemevents', SystemEventViewSet, basename='systemevents') - -urlpatterns = router.urls diff --git a/core/notify_sessions/consumers.py b/core/notify_sessions/consumers.py deleted file mode 100644 index 06413b5..0000000 --- a/core/notify_sessions/consumers.py +++ /dev/null @@ -1,57 +0,0 @@ -import logging -from json.decoder import JSONDecodeError -from json import loads as json_loads -from json import dumps as json_dumps - -from channels.generic.websocket import AsyncWebsocketConsumer - - -class NotifyConsumer(AsyncWebsocketConsumer): - - def __init__(self, *args, **kwargs): - super().__init__(args, kwargs) - self.log = logging.getLogger("server.log") - self.room_group_name = "general" - - async def connect(self): - # Join room group - await self.channel_layer.group_add(self.room_group_name, self.channel_name) - self.log.info(f"Added {self.channel_name} channel to {self.room_group_name} group") - await self.accept() - - async def disconnect(self, close_code): - # Leave room group - await self.channel_layer.group_discard(self.room_group_name, self.channel_name) - - # Receive message from WebSocket - async def receive(self, text_data=None, bytes_data=None): - self.log.info(f"Received message: {text_data}") - try: - text_data_json = json_loads(text_data) - message = text_data_json["message"] - - # Send message to room group - await self.channel_layer.group_send( - self.room_group_name, - {"type": "generic.event", "message": message, "name": "send_message_to_frontend", "event_id": 1} - ) - except JSONDecodeError as e: - await self.send(text_data=json_dumps({"message": "error", "error": "malformed json"})) - self.log.error(e) - except KeyError as e: - await self.send(text_data=json_dumps({"message": "error", "error": f"missing key: {str(e)}"})) - self.log.error(e) - except Exception as e: - await self.send(text_data=json_dumps({"message": "error", "error": "unknown error"})) - self.log.error(e) - raise e - - # Receive message from room group - async def generic_event(self, event): - self.log.info(f"Received event: {event}") - message = event["message"] - name = event["name"] - event_id = event["event_id"] - - # Send message to WebSocket - await self.send(text_data=json_dumps({"message": message, "name": name, "event_id": event_id})) diff --git a/core/notify_sessions/migrations/0001_initial.py b/core/notify_sessions/migrations/0001_initial.py deleted file mode 100644 index c116acf..0000000 --- a/core/notify_sessions/migrations/0001_initial.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-09 02:13 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='SystemEvent', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('type', models.CharField(choices=[('ticket_created', 'ticket_created'), ('ticket_updated', 'ticket_updated'), ('ticket_deleted', 'ticket_deleted'), ('item_created', 'item_created'), ('item_updated', 'item_updated'), ('item_deleted', 'item_deleted'), ('user_created', 'user_created'), ('event_created', 'event_created'), ('event_updated', 'event_updated'), ('event_deleted', 'event_deleted')], max_length=255)), - ('reference', models.IntegerField(blank=True, null=True)), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/core/notify_sessions/migrations/__init__.py b/core/notify_sessions/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/notify_sessions/models.py b/core/notify_sessions/models.py deleted file mode 100644 index 095f1b5..0000000 --- a/core/notify_sessions/models.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging - -from django.db import models -from asgiref.sync import sync_to_async -from channels.layers import get_channel_layer - -from authentication.models import ExtendedUser - - -class SystemEvent(models.Model): - TYPE_CHOICES = [('ticket_created', 'ticket_created'), - ('ticket_updated', 'ticket_updated'), - ('ticket_deleted', 'ticket_deleted'), - ('item_created', 'item_created'), - ('item_updated', 'item_updated'), - ('item_deleted', 'item_deleted'), - ('user_created', 'user_created'), - ('event_created', 'event_created'), - ('event_updated', 'event_updated'), - ('event_deleted', 'event_deleted'), ] - id = models.AutoField(primary_key=True) - timestamp = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(ExtendedUser, models.SET_NULL, null=True) - type = models.CharField(max_length=255, choices=TYPE_CHOICES) - reference = models.IntegerField(blank=True, null=True) - - -async def trigger_event(user, type, reference=None): - log = logging.getLogger('server.log') - log.info(f"Triggering event {type} for user {user} with reference {reference}") - try: - event = await sync_to_async(SystemEvent.objects.create, thread_sensitive=True)(user=user, type=type, - reference=reference) - channel_layer = get_channel_layer() - await channel_layer.group_send( - 'general', - { - 'type': 'generic.event', - 'name': 'send_message_to_frontend', - 'message': "event_trigered_from_views", - 'event_id': event.id, - } - ) - log.info(f"SystemEvent {event.id} triggered") - return event - except Exception as e: - log.error(e) - raise e diff --git a/core/notify_sessions/routing.py b/core/notify_sessions/routing.py deleted file mode 100644 index 1aa0190..0000000 --- a/core/notify_sessions/routing.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path - -from .consumers import NotifyConsumer - -websocket_urlpatterns = [ - path('ws/2/notify/', NotifyConsumer.as_asgi()), -] diff --git a/core/notify_sessions/tests/__init__.py b/core/notify_sessions/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/notify_sessions/tests/test_notify_socket.py b/core/notify_sessions/tests/test_notify_socket.py deleted file mode 100644 index 4e971f1..0000000 --- a/core/notify_sessions/tests/test_notify_socket.py +++ /dev/null @@ -1,66 +0,0 @@ -from django.test import TestCase -from channels.testing import WebsocketCommunicator - -from notify_sessions.consumers import NotifyConsumer -from asgiref.sync import async_to_sync - - -class NotifyWebsocketTestCase(TestCase): - - async def test_connect(self): - communicator = WebsocketCommunicator(NotifyConsumer.as_asgi(), "/ws/2/notify/") - connected, subprotocol = await communicator.connect() - self.assertTrue(connected) - await communicator.disconnect() - - async def fut_send_message(self): - communicator = WebsocketCommunicator(NotifyConsumer.as_asgi(), "/ws/2/notify/") - connected, subprotocol = await communicator.connect() - self.assertTrue(connected) - await communicator.send_json_to({ - "name": "foo", - "message": "bar", - }) - response = await communicator.receive_json_from() - await communicator.disconnect() - return response - - def test_send_message(self): - response = async_to_sync(self.fut_send_message)() - self.assertEqual(response["message"], "bar") - self.assertEqual(response["event_id"], 1) - self.assertEqual(response["name"], "send_message_to_frontend") - # events = SystemEvent.objects.all() - # self.assertEqual(len(events), 1) - # event = events[0] - # self.assertEqual(event.event_id, 1) - # self.assertEqual(event.name, "send_message_to_frontend") - # self.assertEqual(event.message, "bar") - - async def fut_send_and_receive_message(self): - communicator1 = WebsocketCommunicator(NotifyConsumer.as_asgi(), "/ws/2/notify/") - communicator2 = WebsocketCommunicator(NotifyConsumer.as_asgi(), "/ws/2/notify/") - connected1, subprotocol1 = await communicator1.connect() - connected2, subprotocol2 = await communicator2.connect() - self.assertTrue(connected1) - self.assertTrue(connected2) - await communicator1.send_json_to({ - "name": "foo", - "message": "bar", - }) - response = await communicator2.receive_json_from() - await communicator1.disconnect() - await communicator2.disconnect() - return response - - def test_send_and_receive_message(self): - response = async_to_sync(self.fut_send_and_receive_message)() - self.assertEqual(response["message"], "bar") - self.assertEqual(response["event_id"], 1) - self.assertEqual(response["name"], "send_message_to_frontend") - # events = SystemEvent.objects.all() - # self.assertEqual(len(events), 1) - # event = events[0] - # self.assertEqual(event.event_id, 1) - # self.assertEqual(event.name, "send_message_to_frontend") - # self.assertEqual(event.message, "bar") diff --git a/core/requirements.dev.txt b/core/requirements.dev.txt deleted file mode 100644 index ed037b6..0000000 --- a/core/requirements.dev.txt +++ /dev/null @@ -1,76 +0,0 @@ -aiodns==3.2.0 -aiohttp==3.9.5 -aiosignal==1.3.1 -aiosmtpd==1.4.4.post2 -aiosmtplib==3.0.1 -anyio==4.1.0 -asgiref==3.7.2 -asynctest==0.7.1 -atpublic==4.0 -attrs==23.1.0 -autobahn==23.6.2 -Automat==22.10.0 -beautifulsoup4==4.12.2 -bs4==0.0.1 -certifi==2023.11.17 -#cffi==1.16.0 -channels==4.0.0 -channels-redis==4.1.0 -charset-normalizer==3.3.2 -click==8.1.7 -constantly==23.10.4 -coreapi==2.3.3 -coreschema==0.0.4 -coverage==7.3.2 -cryptography==41.0.5 -daphne==4.0.0 -Django==4.2.7 -django-async-test==0.2.2 -django-extensions==3.2.3 -django-rest-knox==4.2.0 -django-soft-delete==0.9.21 -djangorestframework==3.14.0 -drf-yasg==1.21.7 -frozenlist==1.4.1 -h11==0.14.0 -hyperlink==21.0.0 -idna==3.4 -incremental==22.10.0 -inflection==0.5.1 -itypes==1.2.0 -Jinja2==3.1.2 -MarkupSafe==2.1.3 -#msgpack==1.0.7 -#msgpack-python==0.5.6 -multidict==6.0.5 -openapi-codec==1.3.2 -packaging==23.2 -Pillow==11.1.0 -pyasn1==0.5.1 -pyasn1-modules==0.3.0 -pycares==4.4.0 -pycparser==2.21 -pyOpenSSL==23.3.0 -python-dotenv==1.0.0 -pytz==2023.3.post1 -PyYAML==6.0.1 -redis==5.0.1 -requests==2.31.0 -sdnotify==0.3.2 -service-identity==23.1.0 -setproctitle==1.3.3 -six==1.16.0 -sniffio==1.3.0 -soupsieve==2.5 -sqlparse==0.4.4 -Twisted==23.10.0 -txaio==23.1.1 -typing_extensions==4.8.0 -uritemplate==4.1.1 -urllib3==2.1.0 -uvicorn==0.24.0.post1 -websockets==12.0 -yarl==1.9.4 -zope.interface==6.1 -django-prometheus==2.3.1 -prometheus_client==0.21.0 diff --git a/core/requirements.prod.txt b/core/requirements.prod.txt deleted file mode 100644 index ee69fe7..0000000 --- a/core/requirements.prod.txt +++ /dev/null @@ -1,45 +0,0 @@ -aiodns==3.2.0 -aiohttp==3.9.5 -aiosignal==1.3.1 -aiosmtpd==1.4.4.post2 -aiosmtplib==3.0.1 -asgiref==3.7.2 -attrs==23.1.0 -beautifulsoup4==4.12.2 -bs4==0.0.1 -certifi==2023.11.17 -channels==4.0.0 -channels-redis==4.1.0 -coreapi==2.3.3 -coreschema==0.0.4 -Django==4.2.7 -django-extensions==3.2.3 -django-mysql==4.12.0 -django-rest-knox==4.2.0 -django-soft-delete==0.9.21 -djangorestframework==3.14.0 -drf-yasg==1.21.7 -MarkupSafe==2.1.3 -msgpack==1.0.7 -msgpack-python==0.5.6 -mysqlclient==2.2.0 -openapi-codec==1.3.2 -packaging==23.2 -Pillow==10.1.0 -python-dotenv==1.0.0 -pytz==2023.3.post1 -PyYAML==6.0.1 -requests==2.31.0 -sdnotify==0.3.2 -setproctitle==1.3.3 -sniffio==1.3.0 -soupsieve==2.5 -sqlparse==0.4.4 -typing_extensions==4.8.0 -uritemplate==4.1.1 -urllib3==2.1.0 -uvicorn==0.24.0.post1 -watchfiles==0.21.0 -websockets==12.0 -django-prometheus==2.3.1 -prometheus_client==0.21.0 diff --git a/core/server.py b/core/server.py deleted file mode 100644 index d08b595..0000000 --- a/core/server.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -import logging -import os - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') - -import django -import uvicorn - -django.setup() - -from helper import init_loop -from mail.protocol import LMTPHandler -from mail.socket import UnixSocketLMTPController - - -class UvicornServer(uvicorn.Server): - def install_signal_handlers(self): - pass - - -async def web(loop): - log_config = uvicorn.config.LOGGING_CONFIG - log_config["handlers"]["default"] = {"class": "logging.FileHandler", "filename": "web.log", "formatter": "default"} - log_config["handlers"]["access"] = {"class": "logging.FileHandler", "filename": "web-access.log", - "formatter": "access"} - config = uvicorn.Config("core.asgi:application", uds="web.sock", log_config=log_config) - server = UvicornServer(config=config) - await server.serve() - - -async def lmtp(loop): - import grp - log = logging.getLogger('mail.log') - log.addHandler(logging.FileHandler('mail.log')) - # log.setLevel(logging.WARNING) - log.setLevel(logging.INFO) - log.info("Starting LMTP server") - server = await UnixSocketLMTPController(LMTPHandler(), unix_socket='lmtp.sock', loop=loop).serve() - - addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets) - log.info(f'Serving on {addrs}') - - try: - os.chmod('lmtp.sock', 0o775) - current_uid = os.getuid() - posix_gid = grp.getgrnam('postfix').gr_gid - os.chown('lmtp.sock', current_uid, posix_gid) - except Exception as e: - log.error(e) - - async with server: - await server.serve_forever() - log.info("LMTP done") - - -def main(): - import sdnotify - import setproctitle - import os - setproctitle.setproctitle("c3lf-sys3") - log = logging.getLogger('server.log') - log.addHandler(logging.FileHandler('server.log')) - log.setLevel(logging.DEBUG) - log.info("Starting server") - loop = init_loop() - loop.create_task(web(loop)) - # loop.create_task(tcp(loop)) - loop.create_task(lmtp(loop)) - n = sdnotify.SystemdNotifier() - n.notify("READY=1") - log.info("Server ready") - try: - loop.run_forever() - finally: - loop.close() - try: - os.remove("lmtp.sock") - except Exception as e: - log.error(e) - try: - os.remove("web.sock") - except Exception as e: - log.error(e) - log.error(e) - logging.info("Server stopped") - - logging.shutdown() - - -if __name__ == '__main__': - main() diff --git a/core/tickets/__init__.py b/core/tickets/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/tickets/admin.py b/core/tickets/admin.py deleted file mode 100644 index d842482..0000000 --- a/core/tickets/admin.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.contrib import admin - -from tickets.models import IssueThread, Comment, StateChange, Assignment, ItemRelation, ShippingVoucher - - -class IssueThreadAdmin(admin.ModelAdmin): - pass - - -class CommentAdmin(admin.ModelAdmin): - pass - - -class StateChangeAdmin(admin.ModelAdmin): - pass - - -class AssignmentAdmin(admin.ModelAdmin): - pass - - -class ItemRelationAdmin(admin.ModelAdmin): - pass - - -class ShippingVouchersAdmin(admin.ModelAdmin): - pass - - -admin.site.register(IssueThread, IssueThreadAdmin) -admin.site.register(Comment, CommentAdmin) -admin.site.register(StateChange, StateChangeAdmin) -admin.site.register(Assignment, AssignmentAdmin) -admin.site.register(ItemRelation, ItemRelationAdmin) -admin.site.register(ShippingVoucher, ShippingVouchersAdmin) diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py deleted file mode 100644 index 6e34465..0000000 --- a/core/tickets/api_v2.py +++ /dev/null @@ -1,229 +0,0 @@ -from base64 import b64decode - -from django.urls import re_path -from django.contrib.auth.decorators import permission_required -from rest_framework import routers, viewsets, status -from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from asgiref.sync import async_to_sync -from channels.layers import get_channel_layer - -from core.settings import MAIL_DOMAIN -from inventory.models import Event -from mail.models import Email -from mail.protocol import send_smtp, make_reply, collect_references -from notify_sessions.models import SystemEvent -from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher, ItemRelation -from tickets.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer, SearchResultSerializer -from tickets.shared_serializers import RelationSerializer - - -class IssueViewSet(viewsets.ModelViewSet): - serializer_class = IssueSerializer - - def get_queryset(self): - queryset = IssueThread.objects.all() - serializer = self.get_serializer_class() - if hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'prefetch_related_fields'): - queryset = queryset.prefetch_related(*serializer.Meta.prefetch_related_fields) - return queryset - - -class RelationViewSet(viewsets.ModelViewSet): - serializer_class = RelationSerializer - queryset = ItemRelation.objects.all() - - -class CommentViewSet(viewsets.ModelViewSet): - serializer_class = CommentSerializer - queryset = Comment.objects.all() - - -class ShippingVoucherViewSet(viewsets.ModelViewSet): - serializer_class = ShippingVoucherSerializer - queryset = ShippingVoucher.objects.all() - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@permission_required('tickets.add_issuethread', raise_exception=True) -def reply(request, pk): - issue = IssueThread.objects.get(pk=pk) - # email = issue.reply(request.data['body']) # TODO evaluate if this is a useful abstraction - references = collect_references(issue) - first_mail = Email.objects.filter(issue_thread=issue, recipient__endswith='@' + MAIL_DOMAIN).order_by( - 'timestamp').first() - if not first_mail: - return Response({'status': 'error', 'message': 'no previous mail found, reply would not make sense.'}, - status=status.HTTP_400_BAD_REQUEST) - mail = Email.objects.create( - issue_thread=issue, - sender=first_mail.recipient, - recipient=first_mail.sender, - subject=f'Re: {issue.name} [#{issue.short_uuid()}]', - body=request.data['message'], - in_reply_to=first_mail.reference, - ) - async_to_sync(send_smtp)(make_reply(mail, references)) - - return Response({'status': 'ok'}, status=status.HTTP_201_CREATED) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@permission_required('tickets.add_issuethread_manual', raise_exception=True) -def manual_ticket(request, event_slug): - if 'name' not in request.data: - return Response({'status': 'error', 'message': 'missing name'}, status=status.HTTP_400_BAD_REQUEST) - if 'sender' not in request.data: - return Response({'status': 'error', 'message': 'missing sender'}, status=status.HTTP_400_BAD_REQUEST) - if 'recipient' not in request.data: - return Response({'status': 'error', 'message': 'missing recipient'}, status=status.HTTP_400_BAD_REQUEST) - if 'body' not in request.data: - return Response({'status': 'error', 'message': 'missing body'}, status=status.HTTP_400_BAD_REQUEST) - - event = None - if event_slug != 'none': - try: - event = Event.objects.get(slug=event_slug) - except: - return Response({'status': 'error', 'message': 'invalid event'}, status=status.HTTP_400_BAD_REQUEST) - - issue = IssueThread.objects.create( - name=request.data['name'], - event=event, - manually_created=True, - ) - email = Email.objects.create( - issue_thread=issue, - sender=request.data['sender'], - recipient=request.data['recipient'], - subject=request.data['name'], - body=request.data['body'], - ) - - return Response(IssueSerializer(issue).data, status=status.HTTP_201_CREATED) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def get_available_states(request): - def get_state_choices(): - for state in STATE_CHOICES: - yield {'value': list(state)[0], 'text': list(state)[1]} - - return Response(get_state_choices()) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@permission_required('tickets.add_comment', raise_exception=True) -def add_comment(request, pk): - issue = IssueThread.objects.get(pk=pk) - if 'comment' not in request.data or request.data['comment'] == '': - return Response({'status': 'error', 'message': 'missing comment'}, status=status.HTTP_400_BAD_REQUEST) - comment = Comment.objects.create( - issue_thread=issue, - comment=request.data['comment'], - ) - return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED) - - -def filter_issues(issues, query): - query_tokens = query.lower().split(' ') - matches = [] - for issue in issues: - value = 0 - if "T#" + issue.short_uuid() in query: - value += 12 - matches.append( - {'type': 'ticket_uuid', 'text': f'is exactly {issue.short_uuid()} and matched "T#{issue.short_uuid()}"'}) - elif "#" + issue.short_uuid() in query: - value += 11 - matches.append( - {'type': 'ticket_uuid', 'text': f'is exactly {issue.short_uuid()} and matched "#{issue.short_uuid()}"'}) - elif issue.short_uuid() in query: - value += 10 - matches.append({'type': 'ticket_uuid', 'text': f'is exactly {issue.short_uuid()}'}) - if "T#" + str(issue.id) in query: - value += 10 - matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id} and matched "T#{issue.id}"'}) - elif "#" + str(issue.id) in query: - value += 7 - matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id} and matched "#{issue.id}"'}) - elif str(issue.id) in query: - value += 4 - matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id}'}) - for item in issue.related_items: - if "I#" + str(item.id) in query: - value += 8 - matches.append({'type': 'item_id', 'text': f'is exactly {item.id} and matched "I#{item.id}"'}) - elif "#" + str(item.id) in query: - value += 5 - matches.append({'type': 'item_id', 'text': f'is exactly {item.id} and matched "#{item.id}"'}) - elif str(item.id) in query: - value += 3 - matches.append({'type': 'item_id', 'text': f'is exactly {item.id}'}) - for token in query_tokens: - if token in item.description.lower(): - value += 1 - matches.append({'type': 'item_description', 'text': f'contains {token}'}) - for comment in item.comments.all(): - for token in query_tokens: - if token in comment.comment.lower(): - value += 1 - matches.append({'type': 'item_comment', 'text': f'contains {token}'}) - for token in query_tokens: - if token in issue.name.lower(): - value += 1 - matches.append({'type': 'ticket_name', 'text': f'contains {token}'}) - for comment in issue.comments.all(): - for token in query_tokens: - if token in comment.comment.lower(): - value += 1 - matches.append({'type': 'ticket_comment', 'text': f'contains {token}'}) - for email in issue.emails.all(): - for token in query_tokens: - if token in email.subject.lower(): - value += 1 - matches.append({'type': 'email_subject', 'text': f'contains {token}'}) - if token in email.body.lower(): - value += 1 - matches.append({'type': 'email_body', 'text': f'contains {token}'}) - if token in email.sender.lower(): - value += 1 - matches.append({'type': 'email_sender', 'text': f'contains {token}'}) - if value > 0: - yield {'search_score': value, 'issue': issue, 'search_matches': matches} - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def search_issues(request, event_slug, query): - try: - event = Event.objects.get(slug=event_slug) - if not request.user.has_event_perm(event, 'view_issuethread'): - return Response(status=403) - serializer = IssueSerializer() - queryset = IssueThread.objects.filter(event=event) - items = filter_issues(queryset.prefetch_related(*serializer.Meta.prefetch_related_fields), - b64decode(query).decode('utf-8')) - return Response(SearchResultSerializer(items, many=True).data) - except Event.DoesNotExist: - return Response(status=404) - - -router = routers.SimpleRouter() -router.register(r'tickets', IssueViewSet, basename='issues') -router.register(r'matches', RelationViewSet, basename='matches') -router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers') - -urlpatterns = ([ - re_path(r'tickets/states/$', get_available_states, name='get_available_states'), - re_path(r'^tickets/(?P\d+)/reply/$', reply, name='reply'), - re_path(r'^tickets/(?P\d+)/comment/$', add_comment, name='add_comment'), - re_path(r'^(?P[\w-]+)/tickets/manual/$', manual_ticket, name='manual_ticket'), - re_path(r'^(?P[\w-]+)/tickets/(?P[-A-Za-z0-9+/]*={0,3})/$', search_issues, - name='search_issues'), - ] + router.urls) diff --git a/core/tickets/migrations/0001_initial.py b/core/tickets/migrations/0001_initial.py deleted file mode 100644 index 475d70c..0000000 --- a/core/tickets/migrations/0001_initial.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-09 02:13 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='IssueThread', - fields=[ - ('is_deleted', models.BooleanField(default=False)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=255)), - ('state', models.CharField(default='new', max_length=255)), - ('assigned_to', models.CharField(max_length=255, null=True)), - ('last_activity', models.DateTimeField(auto_now=True)), - ], - options={ - 'permissions': [('send_mail', 'Can send mail')], - }, - ), - migrations.CreateModel( - name='StateChange', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('state', models.CharField(max_length=255)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('issue_thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='state_changes', to='tickets.issuethread')), - ], - ), - migrations.CreateModel( - name='Comment', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('comment', models.TextField()), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('issue_thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tickets.issuethread')), - ], - ), - ] diff --git a/core/tickets/migrations/0002_alter_issuethread_options_and_more.py b/core/tickets/migrations/0002_alter_issuethread_options_and_more.py deleted file mode 100644 index 3c1e4a7..0000000 --- a/core/tickets/migrations/0002_alter_issuethread_options_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-22 20:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tickets', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='issuethread', - options={'permissions': [('send_mail', 'Can send mail'), ('add_issuethread_manual', 'Can add issue thread manually')]}, - ), - migrations.AddField( - model_name='issuethread', - name='manually_created', - field=models.BooleanField(default=False), - ), - ] diff --git a/core/tickets/migrations/0003_alter_issuethread_state.py b/core/tickets/migrations/0003_alter_issuethread_state.py deleted file mode 100644 index dd2c9bc..0000000 --- a/core/tickets/migrations/0003_alter_issuethread_state.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-28 20:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('tickets', '0002_alter_issuethread_options_and_more'), - ] - - def convert_state(apps, schema_editor): - IssueThread = apps.get_model('tickets', 'IssueThread') - for issue in IssueThread.objects.all(): - if issue.state == 'new': - issue.state = 'pending_new' - issue.save() - StateChange = apps.get_model('tickets', 'StateChange') - for change in StateChange.objects.all(): - if change.state == 'new': - change.state = 'pending_new' - change.save() - - operations = [ - migrations.AlterField( - model_name='issuethread', - name='state', - field=models.CharField( - choices=[('pending_new', 'New'), ('pending_open', 'Open'), ('pending_shipping', 'Needs to be shipped'), - ('pending_physical_confirmation', 'Needs to be confirmed physically'), - ('pending_return', 'Needs to be returned'), ('waiting_details', 'Waiting for details'), - ('waiting_pre_shipping', 'Waiting for Address/Shipping Info'), - ('closed_returned', 'Closed: Returned'), ('closed_shipped', 'Closed: Shipped'), - ('closed_not_found', 'Closed: Not found'), - ('closed_not_our_problem', 'Closed: Not our problem'), - ('closed_duplicate', 'Closed: Duplicate'), ('closed_timeout', 'Closed: Timeout'), - ('closed_spam', 'Closed: Spam')], default='pending_new', max_length=32, verbose_name='state'), - ), - migrations.RunPython(convert_state), - ] diff --git a/core/tickets/migrations/0004_remove_issuethread_state_alter_statechange_state.py b/core/tickets/migrations/0004_remove_issuethread_state_alter_statechange_state.py deleted file mode 100644 index 7fda407..0000000 --- a/core/tickets/migrations/0004_remove_issuethread_state_alter_statechange_state.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-29 22:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tickets', '0003_alter_issuethread_state'), - ] - - operations = [ - migrations.RemoveField( - model_name='issuethread', - name='state', - ), - migrations.AlterField( - model_name='statechange', - name='state', - field=models.CharField(choices=[('pending_new', 'New'), ('pending_open', 'Open'), ('pending_shipping', 'Needs to be shipped'), ('pending_physical_confirmation', 'Needs to be confirmed physically'), ('pending_return', 'Needs to be returned'), ('waiting_details', 'Waiting for details'), ('waiting_pre_shipping', 'Waiting for Address/Shipping Info'), ('closed_returned', 'Closed: Returned'), ('closed_shipped', 'Closed: Shipped'), ('closed_not_found', 'Closed: Not found'), ('closed_not_our_problem', 'Closed: Not our problem'), ('closed_duplicate', 'Closed: Duplicate'), ('closed_timeout', 'Closed: Timeout'), ('closed_spam', 'Closed: Spam')], default='pending_new', max_length=255), - ), - ] diff --git a/core/tickets/migrations/0005_remove_issuethread_last_activity.py b/core/tickets/migrations/0005_remove_issuethread_last_activity.py deleted file mode 100644 index 37a03ef..0000000 --- a/core/tickets/migrations/0005_remove_issuethread_last_activity.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-10 19:04 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('tickets', '0004_remove_issuethread_state_alter_statechange_state'), - ] - - operations = [ - migrations.RemoveField( - model_name='issuethread', - name='last_activity', - ), - ] diff --git a/core/tickets/migrations/0006_issuethread_uuid.py b/core/tickets/migrations/0006_issuethread_uuid.py deleted file mode 100644 index 6618bbe..0000000 --- a/core/tickets/migrations/0006_issuethread_uuid.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-12 21:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('tickets', '0005_remove_issuethread_last_activity'), - ] - - def set_uuid(apps, schema_editor): - import uuid - IssueThread = apps.get_model('tickets', 'IssueThread') - for issue_thread in IssueThread.objects.all(): - issue_thread.uuid = str(uuid.uuid4()) - issue_thread.save() - - operations = [ - migrations.AddField( - model_name='issuethread', - name='uuid', - field=models.CharField(max_length=255, null=True), - ), - migrations.RunPython(set_uuid), - migrations.AlterField( - model_name='issuethread', - name='uuid', - field=models.CharField(max_length=255, unique=True, null=False, blank=False), - ), - ] diff --git a/core/tickets/migrations/0007_alter_statechange_state.py b/core/tickets/migrations/0007_alter_statechange_state.py deleted file mode 100644 index 6a105a7..0000000 --- a/core/tickets/migrations/0007_alter_statechange_state.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-19 20:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tickets', '0006_issuethread_uuid'), - ] - - operations = [ - migrations.AlterField( - model_name='statechange', - name='state', - field=models.CharField(choices=[('pending_new', 'New'), ('pending_open', 'Open'), ('pending_shipping', 'Needs to be shipped'), ('pending_physical_confirmation', 'Needs to be confirmed physically'), ('pending_return', 'Needs to be returned'), ('pending_postponed', 'Postponed'), ('waiting_details', 'Waiting for details'), ('waiting_pre_shipping', 'Waiting for Address/Shipping Info'), ('closed_returned', 'Closed: Returned'), ('closed_shipped', 'Closed: Shipped'), ('closed_not_found', 'Closed: Not found'), ('closed_not_our_problem', 'Closed: Not our problem'), ('closed_duplicate', 'Closed: Duplicate'), ('closed_timeout', 'Closed: Timeout'), ('closed_spam', 'Closed: Spam'), ('closed_nothing_missing', 'Closed: Nothing missing'), ('closed_wtf', 'Closed: WTF'), ('found_open', 'Item Found and stored externally'), ('found_closed', 'Item Found and stored externally and closed')], default='pending_new', max_length=255), - ), - ] diff --git a/core/tickets/migrations/0008_alter_issuethread_options_and_more.py b/core/tickets/migrations/0008_alter_issuethread_options_and_more.py deleted file mode 100644 index 788f9f6..0000000 --- a/core/tickets/migrations/0008_alter_issuethread_options_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-22 16:15 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('tickets', '0007_alter_statechange_state'), - ] - - operations = [ - migrations.AlterModelOptions( - name='issuethread', - options={'permissions': [('send_mail', 'Can send mail'), ('add_issuethread_manual', 'Can add issue thread manually'), ('assign_issuethread', 'Can assign issue thread')]}, - ), - migrations.RemoveField( - model_name='issuethread', - name='assigned_to', - ), - migrations.CreateModel( - name='Assignment', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('assigned_to', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assigned_tickets', to=settings.AUTH_USER_MODEL)), - ('issue_thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='tickets.issuethread')), - ], - ), - ] diff --git a/core/tickets/migrations/0009_shippingvoucher.py b/core/tickets/migrations/0009_shippingvoucher.py deleted file mode 100644 index a857457..0000000 --- a/core/tickets/migrations/0009_shippingvoucher.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.7 on 2024-06-23 00:47 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('tickets', '0008_alter_issuethread_options_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='ShippingVoucher', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('voucher', models.CharField(max_length=255)), - ('type', models.CharField(max_length=255)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('used_at', models.DateTimeField(null=True)), - ('issue_thread', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shipping_vouchers', to='tickets.issuethread')), - ], - ), - ] diff --git a/core/tickets/migrations/0010_issuethread_event_itemrelation_and_more.py b/core/tickets/migrations/0010_issuethread_event_itemrelation_and_more.py deleted file mode 100644 index 06ec4fd..0000000 --- a/core/tickets/migrations/0010_issuethread_event_itemrelation_and_more.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 4.2.7 on 2024-06-23 02:17 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0004_alter_event_created_at_alter_item_created_at'), - ('tickets', '0009_shippingvoucher'), - ] - - operations = [ - migrations.AddField( - model_name='issuethread', - name='event', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_threads', to='inventory.event'), - ), - migrations.CreateModel( - name='ItemRelation', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('status', models.CharField(choices=[('possible', 'Possible'), ('confirmed', 'Confirmed'), ('discarded', 'Discarded'), ('provided', 'Provided')], default='possible', max_length=255)), - ('issue_thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_relations', to='tickets.issuethread')), - ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='inventory.item')), - ], - ), - migrations.AddField( - model_name='issuethread', - name='related_items', - field=models.ManyToManyField(through='tickets.ItemRelation', to='inventory.item'), - ), - ] diff --git a/core/tickets/migrations/0011_train_old_spam.py b/core/tickets/migrations/0011_train_old_spam.py deleted file mode 100644 index 206cbb4..0000000 --- a/core/tickets/migrations/0011_train_old_spam.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 4.2.7 on 2024-06-23 02:17 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - dependencies = [ - ('mail', '0006_email_raw_file'), - ('tickets', '0010_issuethread_event_itemrelation_and_more'), - ] - - def train_old_mails(apps, schema_editor): - from tickets.models import IssueThread - for t in IssueThread.objects.all(): - try: - state = t.state - i = 0 - for e in t.emails.all(): - if e.raw_file: - if state == 'closed_spam' and i == 0: - e.train_spam() - else: - e.train_ham() - i += 1 - except: - pass - - operations = [ - migrations.RunPython(train_old_mails), - ] diff --git a/core/tickets/migrations/0012_remove_issuethread_related_items_and_more.py b/core/tickets/migrations/0012_remove_issuethread_related_items_and_more.py deleted file mode 100644 index d8a24c7..0000000 --- a/core/tickets/migrations/0012_remove_issuethread_related_items_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 4.2.7 on 2024-11-20 23:58 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0006_alter_event_table'), - ('tickets', '0011_train_old_spam'), - ] - - operations = [ - migrations.RemoveField( - model_name='issuethread', - name='related_items', - ), - migrations.AlterField( - model_name='itemrelation', - name='issue_thread', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_relation_changes', to='tickets.issuethread'), - ), - migrations.AlterField( - model_name='itemrelation', - name='item', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relation_changes', to='inventory.item'), - ), - ] diff --git a/core/tickets/migrations/0013_alter_statechange_state.py b/core/tickets/migrations/0013_alter_statechange_state.py deleted file mode 100644 index 6a99ce5..0000000 --- a/core/tickets/migrations/0013_alter_statechange_state.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.7 on 2025-03-15 21:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tickets', '0012_remove_issuethread_related_items_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='statechange', - name='state', - field=models.CharField(choices=[('pending_new', 'New'), ('pending_open', 'Open'), ('pending_shipping', 'Needs to be shipped'), ('pending_physical_confirmation', 'Needs to be confirmed physically'), ('pending_return', 'Needs to be returned'), ('pending_postponed', 'Postponed'), ('pending_suspected_spam', 'Suspected Spam'), ('waiting_details', 'Waiting for details'), ('waiting_pre_shipping', 'Waiting for Address/Shipping Info'), ('closed_returned', 'Closed: Returned'), ('closed_shipped', 'Closed: Shipped'), ('closed_not_found', 'Closed: Not found'), ('closed_not_our_problem', 'Closed: Not our problem'), ('closed_duplicate', 'Closed: Duplicate'), ('closed_timeout', 'Closed: Timeout'), ('closed_spam', 'Closed: Spam'), ('closed_nothing_missing', 'Closed: Nothing missing'), ('closed_wtf', 'Closed: WTF'), ('found_open', 'Item Found and stored externally'), ('found_closed', 'Item Found and stored externally and closed')], default='pending_new', max_length=255), - ), - ] diff --git a/core/tickets/migrations/__init__.py b/core/tickets/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/tickets/models.py b/core/tickets/models.py deleted file mode 100644 index 794c8e4..0000000 --- a/core/tickets/models.py +++ /dev/null @@ -1,179 +0,0 @@ -from itertools import groupby - -from django.utils import timezone -from django.db import models -from django_softdelete.models import SoftDeleteModel - -from authentication.models import ExtendedUser -from inventory.models import Event, Item -from django.db.models.signals import post_save, pre_save -from django.dispatch import receiver - -STATE_CHOICES = ( - ('pending_new', 'New'), - ('pending_open', 'Open'), - ('pending_shipping', 'Needs to be shipped'), - ('pending_physical_confirmation', 'Needs to be confirmed physically'), - ('pending_return', 'Needs to be returned'), - ('pending_postponed', 'Postponed'), - ('pending_suspected_spam', 'Suspected Spam'), - ('waiting_details', 'Waiting for details'), - ('waiting_pre_shipping', 'Waiting for Address/Shipping Info'), - ('closed_returned', 'Closed: Returned'), - ('closed_shipped', 'Closed: Shipped'), - ('closed_not_found', 'Closed: Not found'), - ('closed_not_our_problem', 'Closed: Not our problem'), - ('closed_duplicate', 'Closed: Duplicate'), - ('closed_timeout', 'Closed: Timeout'), - ('closed_spam', 'Closed: Spam'), - ('closed_nothing_missing', 'Closed: Nothing missing'), - ('closed_wtf', 'Closed: WTF'), - ('found_open', 'Item Found and stored externally'), - ('found_closed', 'Item Found and stored externally and closed'), -) - -RELATION_STATUS_CHOICES = ( - ('possible', 'Possible'), - ('confirmed', 'Confirmed'), - ('discarded', 'Discarded'), - ('provided', 'Provided'), -) - - -class IssueThread(SoftDeleteModel): - id = models.AutoField(primary_key=True) - uuid = models.CharField(max_length=255, unique=True, null=False, blank=False) - name = models.CharField(max_length=255) - event = models.ForeignKey(Event, null=True, on_delete=models.SET_NULL, related_name='issue_threads') - manually_created = models.BooleanField(default=False) - - def __init__(self, *args, **kwargs): - if 'initial_state' in kwargs: - self._initial_state = kwargs.pop('initial_state') - super().__init__(*args, **kwargs) - - def short_uuid(self): - return self.uuid[:8] - - @property - def state(self): - try: - state_changes = sorted(self.state_changes.all(), key=lambda x: x.timestamp, reverse=True) - if state_changes: - return state_changes[0].state - else: - return None - except AttributeError: - return 'none' - - @state.setter - def state(self, value): - if self.state == value: - return - self.state_changes.create(state=value) - if value == 'closed_spam' and self.emails.exists(): - self.emails.first().train_spam() - - @property - def assigned_to(self): - try: - assignments = sorted(self.assignments.all(), key=lambda x: x.timestamp, reverse=True) - if assignments: - return assignments[0].assigned_to - else: - return None - except AttributeError: - return None - - @assigned_to.setter - def assigned_to(self, value): - if self.assigned_to == value: - return - self.assignments.create(assigned_to=value) - - @property - def related_items(self): - groups = groupby(self.item_relation_changes.all(), lambda rel: rel.item.id) - return [sorted(v, key=lambda r: r.timestamp)[0].item for k, v in groups] - - def __str__(self): - return '[' + str(self.id) + '][' + self.short_uuid() + '] ' + self.name - - class Meta: - permissions = [ - ('send_mail', 'Can send mail'), - ('add_issuethread_manual', 'Can add issue thread manually'), - ('assign_issuethread', 'Can assign issue thread'), - ] - - -@receiver(pre_save, sender=IssueThread) -def set_uuid(sender, instance, **kwargs): - import uuid - if instance.uuid is None or instance.uuid == '': - instance.uuid = str(uuid.uuid4()) - - -@receiver(post_save, sender=IssueThread) -def create_issue_thread(sender, instance, created, **kwargs): - if created and instance.state_changes.count() == 0: - initial_state = getattr(instance, '_initial_state', None) - StateChange.objects.create(issue_thread=instance, state=initial_state if initial_state else 'pending_new') - - -class Comment(models.Model): - id = models.AutoField(primary_key=True) - issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='comments') - comment = models.TextField() - timestamp = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return str(self.issue_thread) + ' comment #' + str(self.id) - - -class StateChange(models.Model): - id = models.AutoField(primary_key=True) - issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='state_changes') - state = models.CharField(max_length=255, choices=STATE_CHOICES, default='pending_new') - timestamp = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return str(self.issue_thread) + ' state change to ' + self.state - - -class Assignment(models.Model): - id = models.AutoField(primary_key=True) - issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='assignments') - assigned_to = models.ForeignKey(ExtendedUser, on_delete=models.CASCADE, related_name='assigned_tickets') - timestamp = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return str(self.issue_thread) + ' assigned to ' + self.assigned_to.username - - -class ItemRelation(models.Model): - id = models.AutoField(primary_key=True) - issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='item_relation_changes') - item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='issue_relation_changes') - timestamp = models.DateTimeField(auto_now_add=True) - status = models.CharField(max_length=255, choices=RELATION_STATUS_CHOICES, default='possible') - - def __str__(self): - return str(self.issue_thread) + ' related to ' + str(self.item) - - -class ShippingVoucher(models.Model): - id = models.AutoField(primary_key=True) - issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='shipping_vouchers', null=True) - voucher = models.CharField(max_length=255) - type = models.CharField(max_length=255) - timestamp = models.DateTimeField(auto_now_add=True) - used_at = models.DateTimeField(null=True) - - def __str__(self): - return self.voucher + ' (' + self.type + ')' - - def save(self, *args, **kwargs): - if self.used_at is None and self.issue_thread is not None: - self.used_at = timezone.now() - super().save(*args, **kwargs) diff --git a/core/tickets/serializers.py b/core/tickets/serializers.py deleted file mode 100644 index ff695b1..0000000 --- a/core/tickets/serializers.py +++ /dev/null @@ -1,150 +0,0 @@ -from rest_framework import serializers - -from authentication.models import ExtendedUser -from inventory.models import Event -from inventory.shared_serializers import BasicItemSerializer -from mail.api_v2 import AttachmentSerializer -from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher -from tickets.shared_serializers import BasicIssueSerializer - - -class CommentSerializer(serializers.ModelSerializer): - - def validate(self, attrs): - if 'comment' not in attrs or attrs['comment'] == '': - raise serializers.ValidationError('comment cannot be empty') - return attrs - - class Meta: - model = Comment - fields = ('id', 'comment', 'timestamp', 'issue_thread') - - -class StateSerializer(serializers.Serializer): - text = serializers.SerializerMethodField() - value = serializers.SerializerMethodField() - - def get_text(self, obj): - return obj['text'] - - def get_value(self, obj): - return obj['value'] - - -class ShippingVoucherSerializer(serializers.ModelSerializer): - class Meta: - model = ShippingVoucher - fields = ('id', 'voucher', 'type', 'timestamp', 'issue_thread', 'used_at') - read_only_fields = ('id', 'timestamp', 'used_at') - - -class IssueSerializer(BasicIssueSerializer): - timeline = serializers.SerializerMethodField() - last_activity = serializers.SerializerMethodField() - related_items = BasicItemSerializer(many=True, read_only=True) - - class Meta: - model = IssueThread - fields = ('id', 'timeline', 'name', 'state', 'assigned_to', 'last_activity', 'uuid', 'related_items', 'event') - read_only_fields = ('id', 'timeline', 'last_activity', 'uuid', 'related_items') - prefetch_related_fields = ['state_changes', 'comments', 'emails', 'emails__attachments', 'assignments', - 'item_relation_changes', 'shipping_vouchers', 'item_relation_changes__item', - 'item_relation_changes__item__container_history', 'event', - 'item_relation_changes__item__container_history__container', - 'item_relation_changes__item__files', 'item_relation_changes__item__event', - 'item_relation_changes__item__event'] - - def to_internal_value(self, data): - ret = super().to_internal_value(data) - if 'state' in data: - ret['state'] = data['state'] - return ret - - def validate(self, attrs): - if 'state' in attrs: - if attrs['state'] not in [x[0] for x in STATE_CHOICES]: - raise serializers.ValidationError('invalid state') - return attrs - - @staticmethod - def get_last_activity(self): - try: - last_state_change = max( - [t.timestamp for t in self.state_changes.all()]) if self.state_changes.exists() else None - last_comment = max([t.timestamp for t in self.comments.all()]) if self.comments.exists() else None - last_mail = max([t.timestamp for t in self.emails.all()]) if self.emails.exists() else None - last_assignment = max([t.timestamp for t in self.assignments.all()]) if self.assignments.exists() else None - - last_relation = max([t.timestamp for t in - self.item_relation_changes.all()]) if self.item_relation_changes.exists() else None - args = [x for x in [last_state_change, last_comment, last_mail, last_assignment, last_relation] if - x is not None] - return max(args) - except AttributeError: - return None - - @staticmethod - def get_timeline(obj): - timeline = [] - for comment in obj.comments.all(): - timeline.append({ - 'type': 'comment', - 'id': comment.id, - 'timestamp': comment.timestamp, - 'comment': comment.comment, - }) - for state_change in obj.state_changes.all(): - timeline.append({ - 'type': 'state', - 'id': state_change.id, - 'timestamp': state_change.timestamp, - 'state': state_change.state, - }) - for email in obj.emails.all(): - timeline.append({ - 'type': 'mail', - 'id': email.id, - 'timestamp': email.timestamp, - 'sender': email.sender, - 'recipient': email.recipient, - 'subject': email.subject, - 'body': email.body, - 'attachments': AttachmentSerializer(email.attachments.all(), many=True).data, - }) - for assignment in obj.assignments.all(): - timeline.append({ - 'type': 'assignment', - 'id': assignment.id, - 'timestamp': assignment.timestamp, - 'assigned_to': assignment.assigned_to.username, - }) - for relation in (obj.item_relation_changes.all()): - timeline.append({ - 'type': 'item_relation', - 'id': relation.id, - 'status': relation.status, - 'timestamp': relation.timestamp, - 'item': BasicItemSerializer(relation.item).data, - }) - for shipping_voucher in obj.shipping_vouchers.all(): - timeline.append({ - 'type': 'shipping_voucher', - 'id': shipping_voucher.id, - 'timestamp': shipping_voucher.used_at, - 'voucher': shipping_voucher.voucher, - 'voucher_type': shipping_voucher.type, - }) - return sorted(timeline, key=lambda x: x['timestamp']) - - -class SearchResultSerializer(serializers.Serializer): - search_score = serializers.IntegerField() - search_matches = serializers.ListField(child=serializers.DictField()) - issue = IssueSerializer() - - def to_representation(self, instance): - return {**IssueSerializer(instance['issue']).data, 'search_score': instance['search_score'], - 'search_matches': instance['search_matches']} - - class Meta: - model = IssueThread diff --git a/core/tickets/shared_serializers.py b/core/tickets/shared_serializers.py deleted file mode 100644 index 3d46013..0000000 --- a/core/tickets/shared_serializers.py +++ /dev/null @@ -1,24 +0,0 @@ -from rest_framework import serializers - -from authentication.models import ExtendedUser -from inventory.models import Event -from tickets.models import IssueThread, ItemRelation - - -class RelationSerializer(serializers.ModelSerializer): - class Meta: - model = ItemRelation - fields = ('id', 'status', 'timestamp', 'item', 'issue_thread') - read_only_fields = ('id', 'timestamp') - - -class BasicIssueSerializer(serializers.ModelSerializer): - assigned_to = serializers.SlugRelatedField(slug_field='username', queryset=ExtendedUser.objects.all(), - allow_null=True, required=False) - event = serializers.SlugRelatedField(slug_field='slug', queryset=Event.objects.all(), - allow_null=True, required=False) - - class Meta: - model = IssueThread - fields = ('id', 'name', 'state', 'assigned_to', 'uuid', 'event') - read_only_fields = ('id', 'timeline', 'last_activity', 'uuid', 'related_items') diff --git a/core/tickets/tests/__init__.py b/core/tickets/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/tickets/tests/v2/__init__.py b/core/tickets/tests/v2/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/tickets/tests/v2/test_matches.py b/core/tickets/tests/v2/test_matches.py deleted file mode 100644 index 7bd7a52..0000000 --- a/core/tickets/tests/v2/test_matches.py +++ /dev/null @@ -1,149 +0,0 @@ -from datetime import datetime, timedelta - -from django.test import TestCase, Client - -from authentication.models import ExtendedUser -from inventory.models import Event, Container, Item -from mail.models import Email, EmailAttachment -from tickets.models import IssueThread, StateChange, Comment, ItemRelation -from django.contrib.auth.models import Permission -from knox.models import AuthToken - -from base64 import b64encode - - -class IssueItemMatchApiTest(TestCase): - - def setUp(self): - super().setUp() - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.user.save() - self.event = Event.objects.create(slug='evt') - self.box = Container.objects.create(name='box1') - self.item = Item.objects.create(container=self.box, description="foo", event=self.event) - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - now = datetime.now() - self.issue = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - self.mail1 = Email.objects.create( - subject='test', - body='test', - sender='test', - recipient='test', - issue_thread=self.issue, - timestamp=now, - ) - self.comment = Comment.objects.create( - issue_thread=self.issue, - comment="test", - timestamp=now + timedelta(seconds=3), - ) - self.match = ItemRelation.objects.create( - issue_thread=self.issue, - item=self.item, - timestamp=now + timedelta(seconds=5), - ) - - def test_issues(self): - response = self.client.get('/api/2/tickets/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['id'], self.issue.id) - self.assertEqual(response.json()[0]['name'], "test issue") - self.assertEqual(response.json()[0]['state'], "pending_new") - self.assertEqual(response.json()[0]['event'], "evt") - self.assertEqual(response.json()[0]['assigned_to'], None) - self.assertEqual(response.json()[0]['uuid'], self.issue.uuid) - self.assertEqual(response.json()[0]['last_activity'], self.match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(len(response.json()[0]['timeline']), 4) - self.assertEqual(response.json()[0]['timeline'][0]['type'], 'state') - self.assertEqual(response.json()[0]['timeline'][1]['type'], 'mail') - self.assertEqual(response.json()[0]['timeline'][2]['type'], 'comment') - self.assertEqual(response.json()[0]['timeline'][1]['id'], self.mail1.id) - self.assertEqual(response.json()[0]['timeline'][2]['id'], self.comment.id) - self.assertEqual(response.json()[0]['timeline'][0]['state'], 'pending_new') - self.assertEqual(response.json()[0]['timeline'][1]['sender'], 'test') - self.assertEqual(response.json()[0]['timeline'][1]['recipient'], 'test') - self.assertEqual(response.json()[0]['timeline'][1]['subject'], 'test') - self.assertEqual(response.json()[0]['timeline'][1]['body'], 'test') - self.assertEqual(response.json()[0]['timeline'][1]['timestamp'], - self.mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][2]['comment'], 'test') - self.assertEqual(response.json()[0]['timeline'][2]['timestamp'], - self.comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][3]['status'], 'possible') - self.assertEqual(response.json()[0]['timeline'][3]['timestamp'], - self.match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][3]['item']['description'], "foo") - self.assertEqual(response.json()[0]['timeline'][3]['item']['event'], "evt") - self.assertEqual(response.json()[0]['timeline'][3]['item']['box'], "box1") - self.assertEqual(response.json()[0]['related_items'][0]['description'], "foo") - self.assertEqual(response.json()[0]['related_items'][0]['event'], "evt") - self.assertEqual(response.json()[0]['related_items'][0]['box'], "box1") - - def test_members(self): - response = self.client.get(f'/api/2/{self.event.slug}/item/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['id'], self.item.id) - self.assertEqual(response.json()[0]['description'], 'foo') - self.assertEqual(response.json()[0]['box'], 'box1') - self.assertEqual(response.json()[0]['cid'], self.box.id) - self.assertEqual(response.json()[0]['file'], None) - self.assertEqual(response.json()[0]['returned'], False) - self.assertEqual(response.json()[0]['event'], self.event.slug) - self.assertEqual(len(response.json()[0]['related_issues']), 1) - self.assertEqual(response.json()[0]['related_issues'][0]['id'], self.issue.id) - self.assertEqual(response.json()[0]['related_issues'][0]['name'], "test issue") - - def test_add_match(self): - response = self.client.get('/api/2/matches/') - self.assertEqual(1, len(response.json())) - item = Item.objects.create(container=self.box, event=self.event, description='1') - issue = IssueThread.objects.create(name="test issue", event=self.event) - - response = self.client.post(f'/api/2/matches/', - {'item': item.id, 'issue_thread': issue.id}, - content_type='application/json') - self.assertEqual(response.status_code, 201) - - response = self.client.get('/api/2/matches/') - self.assertEqual(2, len(response.json())) - - response = self.client.get('/api/2/tickets/') - self.assertEqual(4, len(response.json()[0]['timeline'])) - self.assertEqual('item_relation', response.json()[0]['timeline'][3]['type']) - self.assertEqual('possible', response.json()[0]['timeline'][3]['status']) - self.assertEqual(1, len(response.json()[0]['related_items'])) - - def test_change_match_state(self): - response = self.client.get('/api/2/matches/') - self.assertEqual(1, len(response.json())) - - response = self.client.get('/api/2/tickets/') - self.assertEqual(4, len(response.json()[0]['timeline'])) - self.assertEqual('item_relation', response.json()[0]['timeline'][3]['type']) - self.assertEqual('possible', response.json()[0]['timeline'][3]['status']) - self.assertEqual(1, len(response.json()[0]['related_items'])) - - response = self.client.post(f'/api/2/matches/', - {'item': self.item.id, 'issue_thread': self.issue.id, 'status': 'confirmed'}, - content_type='application/json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['status'], 'confirmed') - self.assertEqual(response.json()['id'], 2) - - response = self.client.get('/api/2/matches/') - self.assertEqual(2, len(response.json())) - - response = self.client.get('/api/2/tickets/') - self.assertEqual(5, len(response.json()[0]['timeline'])) - self.assertEqual('item_relation', response.json()[0]['timeline'][3]['type']) - self.assertEqual('possible', response.json()[0]['timeline'][3]['status']) - self.assertEqual('item_relation', response.json()[0]['timeline'][4]['type']) - self.assertEqual('confirmed', response.json()[0]['timeline'][4]['status']) - self.assertEqual(1, len(response.json()[0]['related_items'])) diff --git a/core/tickets/tests/v2/test_shipping_vouchers.py b/core/tickets/tests/v2/test_shipping_vouchers.py deleted file mode 100644 index 45fa245..0000000 --- a/core/tickets/tests/v2/test_shipping_vouchers.py +++ /dev/null @@ -1,41 +0,0 @@ -from datetime import datetime, timedelta - -from django.test import TestCase, Client - -from authentication.models import ExtendedUser -from mail.models import Email, EmailAttachment -from tickets.models import IssueThread, StateChange, Comment, ShippingVoucher -from django.contrib.auth.models import Permission -from knox.models import AuthToken - - -class ShippingVoucherApiTest(TestCase): - - def setUp(self): - super().setUp() - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.user.save() - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - def test_issues_empty(self): - response = self.client.get('/api/2/shipping_vouchers/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_issues_list(self): - ShippingVoucher.objects.create(voucher='1234', type='2kg-eu') - response = self.client.get('/api/2/shipping_vouchers/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()[0]['voucher'], '1234') - self.assertEqual(response.json()[0]['used_at'], None) - self.assertEqual(response.json()[0]['issue_thread'], None) - self.assertEqual(response.json()[0]['type'], '2kg-eu') - - def test_issues_create(self): - response = self.client.post('/api/2/shipping_vouchers/', {'voucher': '1234', 'type': '2kg-eu'}) - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['voucher'], '1234') - self.assertEqual(response.json()['used_at'], None) - self.assertEqual(response.json()['issue_thread'], None) diff --git a/core/tickets/tests/v2/test_tickets.py b/core/tickets/tests/v2/test_tickets.py deleted file mode 100644 index d7bb346..0000000 --- a/core/tickets/tests/v2/test_tickets.py +++ /dev/null @@ -1,491 +0,0 @@ -from datetime import datetime, timedelta - -from django.test import TestCase, Client - -from authentication.models import ExtendedUser -from inventory.models import Event, Container, Item -from inventory.models import Comment as ItemComment -from mail.models import Email, EmailAttachment -from tickets.models import IssueThread, StateChange, Comment, ItemRelation, Assignment -from django.contrib.auth.models import Permission -from knox.models import AuthToken - -from base64 import b64encode - - -class IssueApiTest(TestCase): - - def setUp(self): - super().setUp() - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.user.save() - self.event = Event.objects.create(slug='evt') - self.box = Container.objects.create(name='box1') - self.item = Item.objects.create(container=self.box, description="foo", event=self.event) - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - def test_issues_empty(self): - response = self.client.get('/api/2/tickets/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_issues(self): - now = datetime.now() - issue = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - mail1 = Email.objects.create( - subject='test', - body='test', - sender='test', - recipient='test', - issue_thread=issue, - timestamp=now, - ) - mail2 = Email.objects.create( - subject='test', - body='test', - sender='test', - recipient='test', - issue_thread=issue, - in_reply_to=mail1.reference, - timestamp=now + timedelta(seconds=2), - ) - assignment = Assignment.objects.create( - issue_thread=issue, - assigned_to=self.user, - timestamp=now + timedelta(seconds=3), - ) - comment = Comment.objects.create( - issue_thread=issue, - comment="test", - timestamp=now + timedelta(seconds=4), - ) - match = ItemRelation.objects.create( - issue_thread=issue, - item=self.item, - timestamp=now + timedelta(seconds=5), - ) - self.assertEqual('pending_new', issue.state) - self.assertEqual('test issue', issue.name) - self.assertEqual(self.user, issue.assigned_to) - self.assertEqual(36, len(issue.uuid)) - response = self.client.get('/api/2/tickets/') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]['id'], issue.id) - self.assertEqual(response.json()[0]['name'], "test issue") - self.assertEqual(response.json()[0]['state'], "pending_new") - self.assertEqual(response.json()[0]['event'], "evt") - self.assertEqual(response.json()[0]['assigned_to'], self.user.username) - self.assertEqual(response.json()[0]['uuid'], issue.uuid) - self.assertEqual(response.json()[0]['last_activity'], match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(len(response.json()[0]['timeline']), 6) - self.assertEqual(response.json()[0]['timeline'][0]['type'], 'state') - self.assertEqual(response.json()[0]['timeline'][1]['type'], 'mail') - self.assertEqual(response.json()[0]['timeline'][2]['type'], 'mail') - self.assertEqual(response.json()[0]['timeline'][3]['type'], 'assignment') - self.assertEqual(response.json()[0]['timeline'][4]['type'], 'comment') - self.assertEqual(response.json()[0]['timeline'][5]['type'], 'item_relation') - self.assertEqual(response.json()[0]['timeline'][1]['id'], mail1.id) - self.assertEqual(response.json()[0]['timeline'][2]['id'], mail2.id) - self.assertEqual(response.json()[0]['timeline'][3]['id'], comment.id) - self.assertEqual(response.json()[0]['timeline'][0]['state'], 'pending_new') - self.assertEqual(response.json()[0]['timeline'][1]['sender'], 'test') - self.assertEqual(response.json()[0]['timeline'][1]['recipient'], 'test') - self.assertEqual(response.json()[0]['timeline'][1]['subject'], 'test') - self.assertEqual(response.json()[0]['timeline'][1]['body'], 'test') - self.assertEqual(response.json()[0]['timeline'][1]['timestamp'], - mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][2]['sender'], 'test') - self.assertEqual(response.json()[0]['timeline'][2]['recipient'], 'test') - self.assertEqual(response.json()[0]['timeline'][2]['subject'], 'test') - self.assertEqual(response.json()[0]['timeline'][2]['body'], 'test') - self.assertEqual(response.json()[0]['timeline'][2]['timestamp'], - mail2.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][3]['assigned_to'], self.user.username) - self.assertEqual(response.json()[0]['timeline'][3]['timestamp'], - assignment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][4]['comment'], 'test') - self.assertEqual(response.json()[0]['timeline'][4]['timestamp'], - comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][5]['status'], 'possible') - self.assertEqual(response.json()[0]['timeline'][5]['timestamp'], - match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(response.json()[0]['timeline'][5]['item']['description'], "foo") - self.assertEqual(response.json()[0]['timeline'][5]['item']['event'], "evt") - self.assertEqual(response.json()[0]['timeline'][5]['item']['box'], "box1") - self.assertEqual(response.json()[0]['related_items'][0]['description'], "foo") - self.assertEqual(response.json()[0]['related_items'][0]['event'], "evt") - self.assertEqual(response.json()[0]['related_items'][0]['box'], "box1") - - def test_issues_incomplete_timeline(self): - now = datetime.now() - issue1 = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - issue2 = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - issue3 = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - mail1 = Email.objects.create( - subject='test', - body='test', - sender='test', - recipient='test', - issue_thread=issue2, - timestamp=now + timedelta(seconds=2), - ) - comment = Comment.objects.create( - issue_thread=issue3, - comment="test", - timestamp=now + timedelta(seconds=3), - ) - - response = self.client.get('/api/2/tickets/') - self.assertEqual(200, response.status_code) - self.assertEqual(3, len(response.json())) - self.assertEqual(issue1.id, response.json()[0]['id']) - self.assertEqual("evt", response.json()[0]['event']) - self.assertEqual(issue2.id, response.json()[1]['id']) - self.assertEqual("evt", response.json()[1]['event']) - self.assertEqual(issue3.id, response.json()[2]['id']) - self.assertEqual("evt", response.json()[2]['event']) - self.assertEqual(issue1.state_changes.first().timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - response.json()[0]['last_activity']) - self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - response.json()[1]['last_activity']) - self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - response.json()[2]['last_activity']) - self.assertEqual(1, len(response.json()[0]['timeline'])) - self.assertEqual(2, len(response.json()[1]['timeline'])) - self.assertEqual(2, len(response.json()[2]['timeline'])) - self.assertEqual('state', response.json()[0]['timeline'][0]['type']) - self.assertEqual('state', response.json()[1]['timeline'][0]['type']) - self.assertEqual('mail', response.json()[1]['timeline'][1]['type']) - self.assertEqual('state', response.json()[2]['timeline'][0]['type']) - self.assertEqual('comment', response.json()[2]['timeline'][1]['type']) - self.assertEqual('pending_new', response.json()[0]['timeline'][0]['state']) - self.assertEqual('pending_new', response.json()[1]['timeline'][0]['state']) - self.assertEqual('test', response.json()[1]['timeline'][1]['sender']) - self.assertEqual('test', response.json()[1]['timeline'][1]['recipient']) - self.assertEqual('test', response.json()[1]['timeline'][1]['subject']) - self.assertEqual('test', response.json()[1]['timeline'][1]['body']) - self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - response.json()[1]['timeline'][1]['timestamp']) - self.assertEqual('pending_new', response.json()[2]['timeline'][0]['state']) - self.assertEqual('test', response.json()[2]['timeline'][1]['comment']) - self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - response.json()[2]['timeline'][1]['timestamp']) - - def test_issues_with_files(self): - from django.core.files.base import ContentFile - from hashlib import sha256 - now = datetime.now() - issue = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - mail1 = Email.objects.create( - subject='test', - body='test', - sender='test', - recipient='test', - issue_thread=issue, - timestamp=now, - ) - mail2 = Email.objects.create( - subject='test', - body='test', - sender='test', - recipient='test', - issue_thread=issue, - in_reply_to=mail1.reference, - timestamp=now + timedelta(seconds=2), - ) - comment = Comment.objects.create( - issue_thread=issue, - comment="test", - timestamp=now + timedelta(seconds=3), - ) - file1 = EmailAttachment.objects.create( - name='file1', mime_type='text/plain', file=ContentFile(b"foo1", "f1"), - hash=sha256(b"foo1").hexdigest(), email=mail1 - ) - file2 = EmailAttachment.objects.create( - name='file2', mime_type='text/plain', file=ContentFile(b"foo2", "f2"), - hash=sha256(b"foo2").hexdigest(), email=mail1 - ) - - response = self.client.get('/api/2/tickets/') - self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.json())) - self.assertEqual(issue.id, response.json()[0]['id']) - self.assertEqual("evt", response.json()[0]['event']) - self.assertEqual('pending_new', response.json()[0]['state']) - self.assertEqual('test issue', response.json()[0]['name']) - self.assertEqual(None, response.json()[0]['assigned_to']) - self.assertEqual(36, len(response.json()[0]['uuid'])) - self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - response.json()[0]['last_activity']) - self.assertEqual(4, len(response.json()[0]['timeline'])) - self.assertEqual('state', response.json()[0]['timeline'][0]['type']) - self.assertEqual('mail', response.json()[0]['timeline'][1]['type']) - self.assertEqual('mail', response.json()[0]['timeline'][2]['type']) - self.assertEqual('comment', response.json()[0]['timeline'][3]['type']) - self.assertEqual(mail1.id, response.json()[0]['timeline'][1]['id']) - self.assertEqual(mail2.id, response.json()[0]['timeline'][2]['id']) - self.assertEqual(comment.id, response.json()[0]['timeline'][3]['id']) - self.assertEqual('pending_new', response.json()[0]['timeline'][0]['state']) - self.assertEqual('test', response.json()[0]['timeline'][1]['sender']) - self.assertEqual('test', response.json()[0]['timeline'][1]['recipient']) - self.assertEqual('test', response.json()[0]['timeline'][1]['subject']) - self.assertEqual('test', response.json()[0]['timeline'][1]['body']) - self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - response.json()[0]['timeline'][1]['timestamp']) - self.assertEqual('test', response.json()[0]['timeline'][2]['sender']) - self.assertEqual('test', response.json()[0]['timeline'][2]['recipient']) - - self.assertEqual('test', response.json()[0]['timeline'][2]['subject']) - self.assertEqual('test', response.json()[0]['timeline'][2]['body']) - self.assertEqual(mail2.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - response.json()[0]['timeline'][2]['timestamp']) - self.assertEqual('test', response.json()[0]['timeline'][3]['comment']) - self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - response.json()[0]['timeline'][3]['timestamp']) - self.assertEqual(2, len(response.json()[0]['timeline'][1]['attachments'])) - self.assertEqual(0, len(response.json()[0]['timeline'][2]['attachments'])) - self.assertEqual('file1', response.json()[0]['timeline'][1]['attachments'][0]['name']) - self.assertEqual('file2', response.json()[0]['timeline'][1]['attachments'][1]['name']) - self.assertEqual('text/plain', response.json()[0]['timeline'][1]['attachments'][0]['mime_type']) - self.assertEqual('text/plain', response.json()[0]['timeline'][1]['attachments'][1]['mime_type']) - self.assertEqual(file1.hash, response.json()[0]['timeline'][1]['attachments'][0]['hash']) - self.assertEqual(file2.hash, response.json()[0]['timeline'][1]['attachments'][1]['hash']) - - def test_manual_creation(self): - response = self.client.post('/api/2/evt/tickets/manual/', - {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'}, - content_type='application/json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['state'], 'pending_new') - self.assertEqual(response.json()['name'], 'test issue') - self.assertEqual(response.json()['assigned_to'], None) - self.assertEqual("evt", response.json()['event']) - timeline = response.json()['timeline'] - self.assertEqual(len(timeline), 2) - self.assertEqual(timeline[0]['type'], 'state') - self.assertEqual(timeline[0]['state'], 'pending_new') - self.assertEqual(timeline[1]['type'], 'mail') - self.assertEqual(timeline[1]['sender'], 'test') - self.assertEqual(timeline[1]['recipient'], 'test') - self.assertEqual(timeline[1]['subject'], 'test issue') - self.assertEqual(timeline[1]['body'], 'test') - - def test_manual_creation_none(self): - response = self.client.post('/api/2/none/tickets/manual/', - {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'}, - content_type='application/json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['state'], 'pending_new') - self.assertEqual(response.json()['name'], 'test issue') - self.assertEqual(response.json()['assigned_to'], None) - self.assertEqual(None, response.json()['event']) - timeline = response.json()['timeline'] - self.assertEqual(len(timeline), 2) - self.assertEqual(timeline[0]['type'], 'state') - self.assertEqual(timeline[0]['state'], 'pending_new') - self.assertEqual(timeline[1]['type'], 'mail') - self.assertEqual(timeline[1]['sender'], 'test') - self.assertEqual(timeline[1]['recipient'], 'test') - self.assertEqual(timeline[1]['subject'], 'test issue') - self.assertEqual(timeline[1]['body'], 'test') - - def test_manual_creation_invalid(self): - response = self.client.post('/api/2/foobar/tickets/manual/', - {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'}, - content_type='application/json') - self.assertEqual(response.status_code, 400) - - def test_post_comment_altenative(self): - issue = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': 'test'}) - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['comment'], 'test') - self.assertEqual(response.json()['issue_thread'], issue.id) - self.assertEqual(response.json()['timestamp'], response.json()['timestamp']) - - def test_post_alt_comment_empty(self): - issue = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': ''}) - self.assertEqual(response.status_code, 400) - - def test_state_change(self): - issue = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'pending_open'}, - content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['state'], 'pending_open') - self.assertEqual(response.json()['name'], 'test issue') - self.assertEqual(response.json()['assigned_to'], None) - timeline = response.json()['timeline'] - self.assertEqual(len(timeline), 2) - self.assertEqual(timeline[0]['type'], 'state') - self.assertEqual(timeline[0]['state'], 'pending_new') - self.assertEqual(timeline[1]['type'], 'state') - self.assertEqual(timeline[1]['state'], 'pending_open') - - def test_state_change_invalid_state(self): - issue = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'invalid'}, - content_type='application/json') - self.assertEqual(400, response.status_code) - - def test_assign_user(self): - issue = IssueThread.objects.create( - name="test issue", - event=self.event, - ) - response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'assigned_to': self.user.username}, - content_type='application/json') - self.assertEqual(200, response.status_code) - self.assertEqual('pending_new', response.json()['state']) - self.assertEqual('test issue', response.json()['name']) - self.assertEqual("evt", response.json()['event']) - self.assertEqual(self.user.username, response.json()['assigned_to']) - timeline = response.json()['timeline'] - self.assertEqual(2, len(timeline)) - self.assertEqual('state', timeline[0]['type']) - self.assertEqual('pending_new', timeline[0]['state']) - self.assertEqual('assignment', timeline[1]['type']) - self.assertEqual(self.user.username, timeline[1]['assigned_to']) - - -class IssueSearchTest(TestCase): - - def setUp(self): - super().setUp() - self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.user.save() - self.event = Event.objects.create(slug='EVENT', name='Event') - self.box = Container.objects.create(name='box1') - self.item = Item.objects.create(container=self.box, description="foo", event=self.event) - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - def test_search_empty_result(self): - search_query = b64encode(b'abc').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual([], response.json()) - - def test_search(self): - now = datetime.now() - issue = IssueThread.objects.create( - name="test issue Abc", - event=self.event, - ) - mail1 = Email.objects.create( - subject='test', - body='test aBc', - sender='bar@test', - recipient='2@test', - issue_thread=issue, - timestamp=now, - ) - mail2 = Email.objects.create( - subject='Re: test', - body='test', - sender='2@test', - recipient='1@test', - issue_thread=issue, - in_reply_to=mail1.reference, - timestamp=now + timedelta(seconds=2), - ) - assignment = Assignment.objects.create( - issue_thread=issue, - assigned_to=self.user, - timestamp=now + timedelta(seconds=3), - ) - comment = Comment.objects.create( - issue_thread=issue, - comment="test deF", - timestamp=now + timedelta(seconds=4), - ) - match = ItemRelation.objects.create( - issue_thread=issue, - item=self.item, - timestamp=now + timedelta(seconds=5), - ) - item_comment = ItemComment.objects.create( - item=self.item, - comment="baz", - timestamp=now + timedelta(seconds=6), - ) - search_query = b64encode(b'abC').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.json())) - self.assertEqual(issue.id, response.json()[0]['id']) - score2 = response.json()[0]['search_score'] - - search_query = b64encode(b'dEf').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.json())) - self.assertEqual(issue.id, response.json()[0]['id']) - score1 = response.json()[0]['search_score'] - - search_query = b64encode(b'ghi').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.json())) - - search_query = b64encode(b'Abc def').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.json())) - self.assertEqual(issue.id, response.json()[0]['id']) - score3 = response.json()[0]['search_score'] - - self.assertGreater(score3, score2) - self.assertGreater(score2, score1) - self.assertGreater(score1, 0) - - search_query = b64encode(b'foo').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.json())) - self.assertEqual(issue.id, response.json()[0]['id']) - - search_query = b64encode(b'bar').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.json())) - self.assertEqual(issue.id, response.json()[0]['id']) - - search_query = b64encode(b'baz').decode('utf-8') - response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') - self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.json())) - self.assertEqual(issue.id, response.json()[0]['id']) diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php new file mode 100644 index 0000000..bf9496b --- /dev/null +++ b/database/factories/ModelFactory.php @@ -0,0 +1,19 @@ +define(App\User::class, function (Faker\Generator $faker) { + return [ + 'name' => $faker->name, + 'email' => $faker->email, + ]; +}); diff --git a/core/authentication/__init__.py b/database/migrations/.gitkeep similarity index 100% rename from core/authentication/__init__.py rename to database/migrations/.gitkeep diff --git a/database/migrations/2019_11_14_213133_create_events_table.php b/database/migrations/2019_11_14_213133_create_events_table.php new file mode 100644 index 0000000..7c0d7cc --- /dev/null +++ b/database/migrations/2019_11_14_213133_create_events_table.php @@ -0,0 +1,41 @@ +charset = 'utf8'; + $table->collation = 'utf8_unicode_ci'; + $table->bigIncrements('eid'); + $table->string('name'); + $table->string('slug'); + $table->timestamps(); + $table->timestamp('start')->nullable(); + $table->timestamp('end')->nullable(); + $table->timestamp('pre_start')->nullable(); + $table->timestamp('post_end')->nullable(); + + $table->unique('slug','uslug'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('events'); + } +} diff --git a/database/migrations/2019_11_14_214212_create_containers_table.php b/database/migrations/2019_11_14_214212_create_containers_table.php new file mode 100644 index 0000000..2ee7e5a --- /dev/null +++ b/database/migrations/2019_11_14_214212_create_containers_table.php @@ -0,0 +1,32 @@ +bigIncrements('cid'); + $table->string('name'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('container'); + } +} diff --git a/database/migrations/2019_11_14_232911_create_items_table.php b/database/migrations/2019_11_14_232911_create_items_table.php new file mode 100644 index 0000000..164ecd1 --- /dev/null +++ b/database/migrations/2019_11_14_232911_create_items_table.php @@ -0,0 +1,43 @@ +bigIncrements('iid'); + $table->unsignedBigInteger('item_uid'); + $table->string('bezeichnung'); + $table->timestamp('wann'); + $table->string('wo'); + $table->unsignedBigInteger('eid'); + $table->unsignedBigInteger('cid'); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['eid', 'item_uid']); + + $table->foreign('eid')->references('eid')->on('events'); + $table->foreign('cid')->references('cid')->on('containers'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('items'); + } +} diff --git a/database/migrations/2019_11_28_035156_create_files_table.php b/database/migrations/2019_11_28_035156_create_files_table.php new file mode 100644 index 0000000..e5da094 --- /dev/null +++ b/database/migrations/2019_11_28_035156_create_files_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->timestamps(); + + $table->unsignedBigInteger('iid'); + $table->string('hash'); + + + $table->foreign('iid')->references('iid')->on('items'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('file'); + } +} diff --git a/database/migrations/2019_11_29_005038_container_add_soft_deletes.php b/database/migrations/2019_11_29_005038_container_add_soft_deletes.php new file mode 100644 index 0000000..45bb207 --- /dev/null +++ b/database/migrations/2019_11_29_005038_container_add_soft_deletes.php @@ -0,0 +1,30 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2019_12_15_203050_rename__itemfields.php b/database/migrations/2019_12_15_203050_rename__itemfields.php new file mode 100644 index 0000000..202cab0 --- /dev/null +++ b/database/migrations/2019_12_15_203050_rename__itemfields.php @@ -0,0 +1,34 @@ +renameColumn('bezeichnung','description'); + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('items', function (Blueprint $table) { + $table->renameColumn('description', 'bezeichnung'); + }); + } +} diff --git a/database/migrations/2019_12_24_134319_delete_id_on_file.php b/database/migrations/2019_12_24_134319_delete_id_on_file.php new file mode 100644 index 0000000..1e87a6a --- /dev/null +++ b/database/migrations/2019_12_24_134319_delete_id_on_file.php @@ -0,0 +1,31 @@ +dropColumn('id'); + $table->primary('hash'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2019_12_27_193619_add_item_returned_at_member.php b/database/migrations/2019_12_27_193619_add_item_returned_at_member.php new file mode 100644 index 0000000..aa4fb03 --- /dev/null +++ b/database/migrations/2019_12_27_193619_add_item_returned_at_member.php @@ -0,0 +1,33 @@ +timestamp('returned_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('items', function (Blueprint $table) { + $table->dropColumn('returned_at'); + }); + } +} diff --git a/database/migrations/2020_01_18_001554_add_currentfiles_view.php b/database/migrations/2020_01_18_001554_add_currentfiles_view.php new file mode 100644 index 0000000..7220cb3 --- /dev/null +++ b/database/migrations/2020_01_18_001554_add_currentfiles_view.php @@ -0,0 +1,36 @@ +dropColumn('wo'); + }); + Schema::table('items', function (Blueprint $table) { + $table->dropColumn('wann'); + }); + Schema::table('items', function (Blueprint $table) { + $table->renameColumn('item_uid','uid'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php new file mode 100644 index 0000000..23526c9 --- /dev/null +++ b/database/seeds/DatabaseSeeder.php @@ -0,0 +1,16 @@ +call('UsersTableSeeder'); + } +} diff --git a/deploy/.gitignore b/deploy/.gitignore deleted file mode 100644 index 611281b..0000000 --- a/deploy/.gitignore +++ /dev/null @@ -1 +0,0 @@ -inventory.yml \ No newline at end of file diff --git a/deploy/ansible/inventory.yml.sample b/deploy/ansible/inventory.yml.sample deleted file mode 100644 index 2a50efd..0000000 --- a/deploy/ansible/inventory.yml.sample +++ /dev/null @@ -1,16 +0,0 @@ ---- -c3lf-nodes: - hosts: - : - ansible_ssh_host: - ansible_ssh_user: root - web_domain: - git_branch: master - git_repo: - db_password: - mail_domain: - main_email: - legacy_api_user: - legacy_api_password: - debug_mode_active: false - django_secret_key: 'django-insecure-tm*$w_14iqbiy-!7(8#ba7j+_@(7@rf2&a^!=shs&$03b%2*rv' \ No newline at end of file diff --git a/deploy/ansible/playbooks/deploy-c3lf-sys3.yml b/deploy/ansible/playbooks/deploy-c3lf-sys3.yml deleted file mode 100644 index 4005146..0000000 --- a/deploy/ansible/playbooks/deploy-c3lf-sys3.yml +++ /dev/null @@ -1,370 +0,0 @@ -- name: 'deploy c3lf-sys3' - hosts: 'c3lf-nodes' - handlers: - - name: restart nginx - service: - name: nginx - state: restarted - - - name: restart postfix - service: - name: postfix - state: restarted - - - name: restart rspamd - service: - name: rspamd - state: restarted - - - name: restart mariadb - service: - name: mariadb - state: restarted - - - name: restart c3lf-sys3 - service: - name: c3lf-sys3 - state: restarted - - tasks: - - name: Update apt-get repo and cache - apt: update_cache=yes force_apt_get=yes cache_valid_time=3600 - - - name: Upgrade all apt packages - apt: upgrade=dist force_apt_get=yes - - - name: Ansible apt-get to install base tools - apt: - name: - - htop - - tcpdump - - jq - - curl - - libsensors5 - - prometheus-node-exporter - - openssh-server - state: present - force_apt_get: yes - - - name: Remove useless packages from the cache - apt: - autoclean: yes - - - name: Remove dependencies that are no longer required - apt: - autoremove: yes - - - name: Check if a reboot is needed for debian - register: reboot_required_file - stat: path=/var/run/reboot-required get_checksum=no - - - name: Reboot the Debian or Ubuntu server - reboot: - msg: "Reboot initiated by Ansible due to kernel updates" - connect_timeout: 5 - reboot_timeout: 300 - pre_reboot_delay: 0 - post_reboot_delay: 30 - test_command: uptime - when: reboot_required_file.stat.exists - - - name: Ansible apt-get to install sys3 requirements - apt: - name: - - ufw - - fail2ban - - nginx - - redis - - python3 - - python3-pip - - python3-venv - - python3-passlib - - certbot - - python3-certbot-nginx - - mariadb-server - - python3-dev - - python3-mysqldb - - default-libmysqlclient-dev - - build-essential - - postfix - - rspamd - - git - - pkg-config - - npm - state: present - - - name: remove default nginx site - file: - path: /etc/nginx/sites-enabled/default - state: absent - - - name: remove default nginx site - file: - path: /etc/nginx/sites-available/default - state: absent - - - name: UFW allow SSH - ufw: - rule: allow - port: 22 - proto: tcp - state: enabled - - - name: UFW logging off - ufw: - logging: off - - - name: Configure nginx - template: - src: templates/nginx.conf.j2 - dest: /etc/nginx/sites-available/c3lf-sys3.conf - notify: - - restart nginx - - - name: UFW allow http - ufw: - rule: allow - port: 80 - proto: tcp - state: enabled - - - name: UFW allow https - ufw: - rule: allow - port: 443 - proto: tcp - state: enabled - - - name: Check if initial certbot certificate is needed - stat: - path: /etc/letsencrypt/live/{{web_domain}}/fullchain.pem - register: certbot_cert_exists - - - name: Check nginx ssl config - stat: - path: /etc/letsencrypt/options-ssl-nginx.conf - register: nginx_ssl_config_exists - - - block: - - name: stop nginx - service: - name: nginx - state: stopped - - name: disable c3lf-sys3 site - file: - path: /etc/nginx/sites-enabled/c3lf-sys3.conf - state: absent - - name: add certbot domain - command: "certbot certonly --standalone -d {{web_domain}} --non-interactive --agree-tos --email {{main_email}}" - - name: install letsencrypt ssl config - command: "certbot install --nginx --non-interactive" - - name: enable c3lf-sys3 site - file: - src: /etc/nginx/sites-available/c3lf-sys3.conf - dest: /etc/nginx/sites-enabled/c3lf-sys3.conf - state: link - - name: start nginx - service: - name: nginx - state: started - when: certbot_cert_exists.stat.exists == false or nginx_ssl_config_exists.stat.exists == false - - - name: Enable certbot auto renew - cron: - name: "certbot-auto renew" - minute: "0" - hour: "12" - job: "certbot renew --quiet --no-self-upgrade --nginx --cert-name {{web_domain}}" - state: present - - - name: Configure basic auth - htpasswd: - path: /etc/nginx/conf.d/lf-prod.htpasswd - name: "{{ legacy_api_user }}" - password: "{{ legacy_api_password }}" - state: present - notify: - - restart nginx - - - name: Enable nginx site - file: - src: /etc/nginx/sites-available/c3lf-sys3.conf - dest: /etc/nginx/sites-enabled/c3lf-sys3.conf - state: link - notify: - - restart nginx - - - name: Initially start nginx - service: - name: nginx - state: started - enabled: yes - - - name: create database - mysql_db: - name: c3lf_sys3 - state: present - login_unix_socket: /var/run/mysqld/mysqld.sock - - - name: create database user - mysql_user: - name: c3lf_sys3 - password: "{{ db_password }}" - priv: "c3lf_sys3.*:ALL" - state: present - login_unix_socket: /var/run/mysqld/mysqld.sock - - - name: configure webdir - file: - path: /var/www - state: directory - owner: www-data - group: www-data - mode: 0755 - - - name: configure webdir - file: - path: /var/www/c3lf-sys3 - state: directory - owner: www-data - group: www-data - mode: 0755 - - - name: install python app - become: true - become_user: www-data - become_method: su - become_flags: '-s /bin/bash' - block: - - name: create repo dir - git: - repo: "{{ git_repo }}" - dest: /var/www/c3lf-sys3/repo - version: "{{ git_branch }}" - force: yes - recursive: yes - single_branch: yes - register: git_repo - notify: - - restart c3lf-sys3 - - - name: check if venv exists - stat: - path: /var/www/c3lf-sys3/venv/bin/python3 - register: venv_exists - - - name: create venv - command: "python3 -m venv /var/www/c3lf-sys3/venv" - when: venv_exists.stat.exists == false - - - name: install requirements - pip: - requirements: /var/www/c3lf-sys3/repo/core/requirements.prod.txt - virtualenv: /var/www/c3lf-sys3/venv - state: present - when: git_repo.changed == true - notify: - - restart c3lf-sys3 - - - name: configure django - template: - src: templates/django.env.j2 - dest: /var/www/c3lf-sys3/repo/core/.env - - - name: migrate database - shell: "/var/www/c3lf-sys3/venv/bin/python /var/www/c3lf-sys3/repo/core/manage.py migrate" - when: git_repo.changed == true - - - name: create superuser - shell: "/var/www/c3lf-sys3/venv/bin/python /var/www/c3lf-sys3/repo/core/manage.py createsuperuser --noinput || true" - when: git_repo.changed == true - environment: - DJANGO_SUPERUSER_USERNAME: admin - DJANGO_SUPERUSER_PASSWORD: "{{ django_password }}" - DJANGO_SUPERUSER_EMAIL: "{{ main_email }}" - - - name: collect static files - shell: "/var/www/c3lf-sys3/venv/bin/python /var/www/c3lf-sys3/repo/core/manage.py collectstatic --noinput" - when: git_repo.changed == true - - - name: js config - template: - src: templates/config.js.j2 - dest: /var/www/c3lf-sys3/repo/web/src/config.js - - - name: install build dependencies - command: - cmd: "npm install" - chdir: /var/www/c3lf-sys3/repo/web - when: git_repo.changed == true - - - name: build frontend - command: - cmd: "npm run build" - chdir: /var/www/c3lf-sys3/repo/web - when: git_repo.changed == true - - - name: add c3lf-sys3 service - template: - src: templates/c3lf-sys3.service.j2 - dest: /etc/systemd/system/c3lf-sys3.service - notify: - - restart c3lf-sys3 - - - name: reload systemd - systemd: - daemon_reload: yes - - - name: start c3lf-sys3 service - service: - name: c3lf-sys3 - state: started - enabled: yes - - - name: add postfix to www-data group - user: - name: postfix - groups: www-data - append: yes - notify: - - restart postfix - - - name: add custom transport config - lineinfile: - path: /etc/postfix/master.cf - line: "c3lf-sys3 unix - n n - - lmtp" - state: present - create: yes - notify: - - restart postfix - - - name: configure postfix - template: - src: templates/postfix.cf.j2 - dest: /etc/postfix/main.cf - notify: - - restart postfix - - - name: configure rspamd dkim - template: - src: templates/rspamd-dkim.cf.j2 - dest: /etc/rspamd/local.d/dkim_signing.conf - notify: - - restart rspamd - - - name: configure rspamd - copy: - content: | - write_servers = "localhost"; - read_servers = "localhost"; - dest: /etc/rspamd/local.d/redis.conf - notify: - - restart rspamd - - - - name: UFW allow smtp - ufw: - rule: allow - port: 25 - proto: tcp - state: enabled \ No newline at end of file diff --git a/deploy/ansible/playbooks/templates/c3lf-sys3.service.j2 b/deploy/ansible/playbooks/templates/c3lf-sys3.service.j2 deleted file mode 100644 index 515a82d..0000000 --- a/deploy/ansible/playbooks/templates/c3lf-sys3.service.j2 +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=standalone c3lf-sys3 server -After=network.target - -[Service] -Type=notify -WorkingDirectory=/var/www/c3lf-sys3 -ExecStart=/var/www/c3lf-sys3/venv/bin/python3 /var/www/c3lf-sys3/repo/core/server.py -Environment=DJANGO_SETTINGS_MODULE=core.settings -Restart=always -RestartSec=5 -User=www-data -Group=www-data -StandardOutput=append:/var/www/c3lf-sys3/service.info.log -StandardError=append:/var/www/c3lf-sys3/service.error.log - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/deploy/ansible/playbooks/templates/config.js.j2 b/deploy/ansible/playbooks/templates/config.js.j2 deleted file mode 100644 index b95ac7e..0000000 --- a/deploy/ansible/playbooks/templates/config.js.j2 +++ /dev/null @@ -1,9 +0,0 @@ -export default { - service: { - url: 'https://{{ web_domain }}/api', - auth: { - username: '{{ legacy_api_user }}', - password: '{{ legacy_api_password }}' - } - } -}; \ No newline at end of file diff --git a/deploy/ansible/playbooks/templates/django.env.j2 b/deploy/ansible/playbooks/templates/django.env.j2 deleted file mode 100644 index c9b1c83..0000000 --- a/deploy/ansible/playbooks/templates/django.env.j2 +++ /dev/null @@ -1,15 +0,0 @@ -REDIS_HOST=localhost -DB_HOST=localhost -DB_PORT=3306 -DB_NAME=c3lf_sys3 -DB_USER=c3lf_sys3 -DB_PASSWORD={{ db_password }} -HTTP_HOST={{ web_domain }} -MAIL_DOMAIN={{ mail_domain }} -LEGACY_API_USER={{ legacy_api_user }} -LEGACY_API_PASSWORD={{ legacy_api_password }} -MEDIA_ROOT=/var/www/c3lf-sys3/userfiles -STATIC_ROOT=/var/www/c3lf-sys3/staticfiles -ACTIVE_SPAM_TRAINING=True -DEBUG_MODE_ACTIVE={{ debug_mode_active }} -DJANGO_SECRET_KEY={{ django_secret_key }} diff --git a/deploy/ansible/playbooks/templates/nginx.conf.j2 b/deploy/ansible/playbooks/templates/nginx.conf.j2 deleted file mode 100644 index 1bde29a..0000000 --- a/deploy/ansible/playbooks/templates/nginx.conf.j2 +++ /dev/null @@ -1,105 +0,0 @@ -upstream c3lf-sys3 { - server unix:/var/www/c3lf-sys3/web.sock; -} - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -server { - - server_name {{ web_domain }}; - client_max_body_size 1024M; - - location / { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT'; - add_header 'Content-Security-Policy' "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: wss:;"; - root /var/www/c3lf-sys3/repo/web/dist; - index index.html index.htm index.nginx-debian.html; - try_files $uri $uri/ /index.html; - } - - location /ws { - proxy_http_version 1.1; - #auth_basic off; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_pass http://c3lf-sys3; - } - - location ~ ^/(api|media)/ { - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_redirect off; - proxy_buffering off; - proxy_pass http://c3lf-sys3; - - location ~ ^/api/1 { - auth_basic C3LF; - auth_basic_user_file conf.d/lf-prod.htpasswd; - } - } - - location /djangoadmin { - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_redirect off; - proxy_buffering off; - proxy_pass http://c3lf-sys3; - } - - location /redirect_media/ { - internal; - alias /var/www/c3lf-sys3/userfiles/; - } - - location /redirect_thumbnail/ { - internal; - alias /var/www/c3lf-sys3/userfiles/thumbnails/; - } - - location /static/ { - alias /var/www/c3lf-sys3/staticfiles/; - } - - location /metrics { - allow 95.156.226.90; - allow 127.0.0.1; - allow ::1; - deny all; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_redirect off; - proxy_buffering off; - proxy_pass http://c3lf-sys3; - } - - listen 443 ssl http2; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/{{ web_domain }}/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/{{ web_domain }}/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - -} - -server { - if ($host = {{ web_domain }}) { - return 301 https://$host$request_uri; - } # managed by Certbot - - server_name {{ web_domain }}; - - listen 80; - return 404; # managed by Certbot -} diff --git a/deploy/ansible/playbooks/templates/postfix.cf.j2 b/deploy/ansible/playbooks/templates/postfix.cf.j2 deleted file mode 100644 index f6e0b09..0000000 --- a/deploy/ansible/playbooks/templates/postfix.cf.j2 +++ /dev/null @@ -1,52 +0,0 @@ -# See /usr/share/postfix/main.cf.dist for a commented, more complete version - - -# Debian specific: Specifying a file name will cause the first -# line of that file to be used as the name. The Debian default -# is /etc/mailname. -#myorigin = /etc/mailname - -smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU) -biff = no - -# appending .domain is the MUA's job. -append_dot_mydomain = no - -readme_directory = no - -# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 3.6 on -# fresh installs. -compatibility_level = 3.6 - -# TLS parameters -smtp_use_tls = yes -smtp_force_tls = yes -smtpd_use_tls = yes -smtpd_tls_cert_file=/etc/letsencrypt/live/{{ web_domain }}/fullchain.pem -smtpd_tls_key_file=/etc/letsencrypt/live/{{ web_domain }}/privkey.pem -smtpd_tls_security_level=may - -smtp_tls_CApath=/etc/ssl/certs -smtp_tls_security_level=may -smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache - - -smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination -myhostname = polaris.lab.or.it -alias_maps = hash:/etc/aliases -alias_database = hash:/etc/aliases -myorigin = /etc/mailname -mydestination = $myhostname, , localhost -mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 -mailbox_size_limit = 0 -recipient_delimiter = + -inet_interfaces = all -inet_protocols = all - -maillog_file = /var/log/mail.log - -virtual_mailbox_domains = {{ mail_domain }} -virtual_transport=c3lf-sys3:unix:/var/www/c3lf-sys3/lmtp.sock - -smtpd_milters = inet:localhost:11332 -milter_default_action = accept \ No newline at end of file diff --git a/deploy/ansible/playbooks/templates/rspamd-dkim.cf.j2 b/deploy/ansible/playbooks/templates/rspamd-dkim.cf.j2 deleted file mode 100644 index 9e21aa5..0000000 --- a/deploy/ansible/playbooks/templates/rspamd-dkim.cf.j2 +++ /dev/null @@ -1,79 +0,0 @@ -# local.d/dkim_signing.conf - -enabled = true; - -# If false, messages with empty envelope from are not signed -allow_envfrom_empty = true; - -# If true, envelope/header domain mismatch is ignored -allow_hdrfrom_mismatch = false; - -# If true, multiple from headers are allowed (but only first is used) -allow_hdrfrom_multiple = false; - -# If true, username does not need to contain matching domain -allow_username_mismatch = false; - -# Default path to key, can include '$domain' and '$selector' variables -path = "/var/lib/rspamd/dkim/$domain.$selector.key"; - -# Default selector to use -selector = "dkim"; - -# If false, messages from authenticated users are not selected for signing -sign_authenticated = true; - -# If false, messages from local networks are not selected for signing -sign_local = true; - -# Map file of IP addresses/subnets to consider for signing -# sign_networks = "/some/file"; # or url - -# Symbol to add when message is signed -symbol = "DKIM_SIGNED"; - -# Whether to fallback to global config -try_fallback = true; - -# Domain to use for DKIM signing: can be "header" (MIME From), "envelope" (SMTP From), "recipient" (SMTP To), "auth" (SMTP username) or directly specified domain name -use_domain = "header"; - -# Domain to use for DKIM signing when sender is in sign_networks ("header"/"envelope"/"auth") -#use_domain_sign_networks = "header"; - -# Domain to use for DKIM signing when sender is a local IP ("header"/"envelope"/"auth") -#use_domain_sign_local = "header"; - -# Whether to normalise domains to eSLD -use_esld = true; - -# Whether to get keys from Redis -use_redis = false; - -# Hash for DKIM keys in Redis -key_prefix = "DKIM_KEYS"; - -# map of domains -> names of selectors (since rspamd 1.5.3) -#selector_map = "/etc/rspamd/dkim_selectors.map"; - -# map of domains -> paths to keys (since rspamd 1.5.3) -#path_map = "/etc/rspamd/dkim_paths.map"; - -# If `true` get pubkey from DNS record and check if it matches private key -check_pubkey = false; -# Set to `false` if you want to skip signing if public and private keys mismatch -allow_pubkey_mismatch = true; - -# Domain specific settings -domain { - # Domain name is used as key - c3lf.de { - - # Private key path - path = "/var/lib/rspamd/dkim/{{ mail_domain }}.key"; - - # Selector - selector = "{{ mail_domain }}"; - } -} - diff --git a/deploy/dev/Dockerfile.backend b/deploy/dev/Dockerfile.backend deleted file mode 100644 index 57ab856..0000000 --- a/deploy/dev/Dockerfile.backend +++ /dev/null @@ -1,8 +0,0 @@ -FROM python:3.11-slim-bookworm -LABEL authors="lagertonne" - -ENV PYTHONUNBUFFERED 1 -RUN mkdir /code -WORKDIR /code -COPY requirements.dev.txt /code/ -RUN pip install -r requirements.dev.txt \ No newline at end of file diff --git a/deploy/dev/Dockerfile.frontend b/deploy/dev/Dockerfile.frontend deleted file mode 100644 index a8fd652..0000000 --- a/deploy/dev/Dockerfile.frontend +++ /dev/null @@ -1,6 +0,0 @@ -FROM node:22-alpine - -RUN mkdir /web -WORKDIR /web -COPY package.json /web/ -RUN npm install diff --git a/deploy/dev/docker-compose.yml b/deploy/dev/docker-compose.yml deleted file mode 100644 index cf1bfdc..0000000 --- a/deploy/dev/docker-compose.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: c3lf-sys3-dev -services: - core: - build: - context: ../../core - dockerfile: ../deploy/dev/Dockerfile.backend - command: bash -c 'python manage.py migrate && python testdata.py && python manage.py runserver 0.0.0.0:8000' - environment: - - HTTP_HOST=core - - DB_FILE=.local/dev.db - - DEBUG_MODE_ACTIVE=true - volumes: - - ../../core:/code:ro - - ../testdata.py:/code/testdata.py:ro - - backend_context:/code/.local - ports: - - "8000:8000" - - frontend: - build: - context: ../../web - dockerfile: ../deploy/dev/Dockerfile.frontend - command: npm run serve - volumes: - - ../../web/src:/web/src - - ./vue.config.js:/web/vue.config.js - ports: - - "8080:8080" - depends_on: - - core - -volumes: - backend_context: \ No newline at end of file diff --git a/deploy/dev/vue.config.js b/deploy/dev/vue.config.js deleted file mode 100644 index f8f3c26..0000000 --- a/deploy/dev/vue.config.js +++ /dev/null @@ -1,27 +0,0 @@ -// vue.config.js - -module.exports = { - devServer: { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "*", - "Access-Control-Allow-Methods": "*" - }, - proxy: { - '^/media/2': { - target: 'http://core:8000/', - }, - '^/api/2': { - target: 'http://core:8000/', - }, - '^/api/1': { - target: 'http://core:8000/', - }, - '^/ws/2': { - target: 'http://core:8000/', - ws: true, - logLevel: 'debug', - }, - } - } -} \ No newline at end of file diff --git a/deploy/testdata.py b/deploy/testdata.py deleted file mode 100644 index dca385f..0000000 --- a/deploy/testdata.py +++ /dev/null @@ -1,88 +0,0 @@ -import os - - -def setup(): - from authentication.models import ExtendedUser, EventPermission - from inventory.models import Event - from django.contrib.auth.models import Permission, Group - permissions = ['add_item', 'view_item', 'view_file', 'delete_item', 'change_item'] - if not ExtendedUser.objects.filter(username='admin').exists(): - admin = ExtendedUser.objects.create_superuser('admin', 'admin@example.com', 'admin') - admin.set_password('admin') - admin.user_permissions.add(*Permission.objects.all()) - admin.save() - - if not ExtendedUser.objects.filter(username='testuser').exists(): - testuser = ExtendedUser.objects.create_user('testuser', 'testuser@example.com', 'testuser') - testuser.set_password('testuser') - testuser.user_permissions.add(*Permission.objects.all()) - testuser.save() - - team = Group.objects.get(name='Team') - team.permissions.add( - *Permission.objects.all() - ) - - if not ExtendedUser.objects.filter(username='testuser2').exists(): - testuser2 = ExtendedUser.objects.create_user('testuser2', 'testuser2@example.com', 'testuser2') - testuser2.set_password('testuser2') - testuser2.groups.add(team) - testuser2.save() - - event1 = Event.objects.get_or_create(id=1, name='first test event', slug='TEST1', - start='2023-12-18 00:00:00.000000', end='2023-12-27 00:00:00.000000', - pre_start='2023-12-31 00:00:00.000000', post_end='2024-01-04 00:00:00.000000')[ - 0] - - event2 = Event.objects.get_or_create(id=2, name='second test event', slug='TEST2', - start='2024-12-18 00:00:00.000000', end='2024-12-27 00:00:00.000000', - pre_start='2024-12-31 00:00:00.000000', post_end='2025-01-04 00:00:00.000000')[ - 0] - - # for permission in permissions: - # EventPermission.objects.create(event=event_37c3, user=foo, - # permission=Permission.objects.get(codename=permission)) - - from tickets.models import IssueThread - - from mail.models import Email - - issue_thread = IssueThread.objects.get_or_create( - id=1, - name="test", - event=Event.objects.get(slug='TEST1') - )[0] - mail1 = Email.objects.get_or_create( - id=1, - subject='test subject', - body='test', - sender='test1@test', - recipient='test2@test', - issue_thread=issue_thread, - )[0] - mail1_reply = Email.objects.get_or_create( - id=2, - subject='Message received', - body='Thank you for your message.', - sender='test2@test', - recipient='test1@test', - in_reply_to=mail1.reference, - issue_thread=issue_thread, - )[0] - - -def main(): - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") - import django - - django.setup() - - from django.core.management import call_command - call_command('migrate') - - setup() - print('testdata initialised') - - -if __name__ == '__main__': - main() diff --git a/deploy/testing/Dockerfile.backend b/deploy/testing/Dockerfile.backend deleted file mode 100644 index 06e494f..0000000 --- a/deploy/testing/Dockerfile.backend +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:3.11-slim-bookworm -LABEL authors="lagertonne" - -ENV PYTHONUNBUFFERED 1 -RUN mkdir /code -WORKDIR /code -RUN apt update && apt install -y pkg-config mariadb-client default-libmysqlclient-dev build-essential -RUN pip install mysqlclient -COPY requirements.prod.txt /code/ -RUN pip install -r requirements.prod.txt -COPY .. /code/ \ No newline at end of file diff --git a/deploy/testing/Dockerfile.frontend b/deploy/testing/Dockerfile.frontend deleted file mode 100644 index a8fd652..0000000 --- a/deploy/testing/Dockerfile.frontend +++ /dev/null @@ -1,6 +0,0 @@ -FROM node:22-alpine - -RUN mkdir /web -WORKDIR /web -COPY package.json /web/ -RUN npm install diff --git a/deploy/testing/docker-compose.yml b/deploy/testing/docker-compose.yml deleted file mode 100644 index 4a82289..0000000 --- a/deploy/testing/docker-compose.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: c3lf-sys3-testing -services: - redis: - image: redis - ports: - - "6379:6379" - - db: - image: mariadb - environment: - MARIADB_RANDOM_ROOT_PASSWORD: true - MARIADB_DATABASE: system3 - MARIADB_USER: system3 - MARIADB_PASSWORD: system3 - volumes: - - mariadb_data:/var/lib/mysql - ports: - - "3306:3306" - - core: - build: - context: ../../core - dockerfile: ../deploy/testing/Dockerfile.backend - command: bash -c 'python manage.py migrate && python testdata.py && python /code/server.py' - environment: - - HTTP_HOST=core - - REDIS_HOST=redis - - DB_HOST=db - - DB_PORT=3306 - - DB_NAME=system3 - - DB_USER=system3 - - DB_PASSWORD=system3 - - MAIL_DOMAIN=mail:1025 - volumes: - - ../../core:/code:ro - - ../testdata.py:/code/testdata.py:ro - - backend_context:/code - ports: - - "8000:8000" - depends_on: - - db - - redis - - mail - - frontend: - build: - context: ../../web - dockerfile: ../deploy/testing/Dockerfile.frontend - command: npm run serve - volumes: - - ../../web:/web:ro - - ./vue.config.js:/web/vue.config.js:ro - - frontend_context:/web - ports: - - "8080:8080" - depends_on: - - core - - mail: - image: docker.io/axllent/mailpit - volumes: - - mailpit_data:/data - ports: - - 8025:8025 - - 1025:1025 - environment: - MP_MAX_MESSAGES: 5000 - MP_DATABASE: /data/mailpit.db - MP_SMTP_AUTH_ACCEPT_ANY: 1 - MP_SMTP_AUTH_ALLOW_INSECURE: 1 - -volumes: - mariadb_data: - mailpit_data: - frontend_context: - backend_context: diff --git a/deploy/testing/vue.config.js b/deploy/testing/vue.config.js deleted file mode 100644 index f8f3c26..0000000 --- a/deploy/testing/vue.config.js +++ /dev/null @@ -1,27 +0,0 @@ -// vue.config.js - -module.exports = { - devServer: { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "*", - "Access-Control-Allow-Methods": "*" - }, - proxy: { - '^/media/2': { - target: 'http://core:8000/', - }, - '^/api/2': { - target: 'http://core:8000/', - }, - '^/api/1': { - target: 'http://core:8000/', - }, - '^/ws/2': { - target: 'http://core:8000/', - ws: true, - logLevel: 'debug', - }, - } - } -} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..567999f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.6' +services: + webserver: + image: webdevops/php-nginx-dev + ports: + - "80:80" + volumes: + - ./:/app + - ./container/web/init.sh:/entrypoint.d/09-init.sh + - ./container/web/location-root.conf:/opt/docker/etc/nginx/vhost.common.d/10-location-root.conf + - ./container/web/general.conf:/opt/docker/etc/nginx/vhost.common.d/10-general.conf + - ./container/web/images.conf:/opt/docker/etc/nginx/vhost.common.d/20-images.conf + environment: + WEB_DOCUMENT_ROOT: /app/public + XDEBUG_REMOTE_HOST: marvin + depends_on: + - dbserver + dbserver: + image: mariadb:latest + environment: + MYSQL_ROOT_PASSWORD: 'foobar' + MYSQL_DATABASE: ${DB_DATABASE} + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + restart: on-failure + ports: + - "3306:3306" + volumes: + - ./.local/db:/var/lib/mysql + - ./container/db/:/docker-entrypoint-initdb.d diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..50df40c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,28 @@ + + + + + ./tests + + + + + ./app + + + + + + + + + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b75525b --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,21 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Handle Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..04aa086 --- /dev/null +++ b/public/index.php @@ -0,0 +1,28 @@ +run(); diff --git a/public/thumbnail.php b/public/thumbnail.php new file mode 100644 index 0000000..a6339a4 --- /dev/null +++ b/public/thumbnail.php @@ -0,0 +1,41 @@ +setImageFormat('jpeg'); + $imagick->setImageCompression(Imagick::COMPRESSION_JPEG); + $imagick->setImageCompressionQuality($quality); + $imagick->cropThumbnailImage($width, $height); + $imagick->setImagePage(0, 0, 0, 0); + if (file_put_contents($thumb, $imagick) === false) { + log_error("Could not put contents."); + } + }catch(Exception $exception){ + log_error($exception); + $imagick = file_get_contents($img); + } + + header("Content-type: image/jpeg"); + echo $imagick; + exit; +} else { + log_error("No valid image provided with {$img}."); +} + + +?> diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5de300b --- /dev/null +++ b/readme.md @@ -0,0 +1,9 @@ +# lfbackend + +This is the background api of c3lf.de + +### Development server +```bash +cp .env.example .env +docker-compose up +``` diff --git a/core/authentication/migrations/__init__.py b/resources/views/.gitkeep similarity index 100% rename from core/authentication/migrations/__init__.py rename to resources/views/.gitkeep diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..5d32e71 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,54 @@ +get('/', function () use ($router) { + return response()->json(array( + "framework_version" =>$router->app->version(), + "api_min_version"=>"1.0", + "api_max_version"=>"1.0", + )); +}); + + +//API version 1 +// copy this block and bump up version when implementing breaking changes +$router->group(['prefix' => '1'], function () use ($router) { + + // events + $router->get('events', ['uses' => 'EventController@showAllEvents']); + $router->get('event/{id}', ['uses' => 'EventController@showOneEvent']); + $router->post('event', ['uses' => 'EventController@create']); + $router->delete('event/{id}', ['uses' => 'EventController@delete']); + $router->put('event/{id}', ['uses' => 'EventController@update']); + + // containers + $router->get('boxes', ['uses' => 'ContainerController@showAllContainers']); + $router->get('box/{id}', ['uses' => 'ContainerController@showOneContainer']); + $router->post('box', ['uses' => 'ContainerController@create']); + $router->delete('box/{id}', ['uses' => 'ContainerController@delete']); + $router->put('box/{id}', ['uses' => 'ContainerController@update']); + + // files + $router->get('files', ['uses' => 'FileController@showAllFiles']); + $router->get('file/{id}', ['uses' => 'FileController@showOneFile']); + $router->post('file', ['uses' => 'FileController@create']); + $router->delete('file/{id}', ['uses' => 'FileController@delete']); + + // items + $router->get('{event}/items', ['uses' => 'ItemController@showByEvent']); + $router->get('{event}/items/{query}', ['uses' => 'ItemController@searchByEvent']); + $router->get('{event}/item/{id}', ['uses' => 'ItemController@showOneItem']); + $router->post('{event}/item', ['uses' => 'ItemController@create']); + $router->delete('{event}/item/{id}', ['uses' => 'ItemController@delete']); + $router->put('{event}/item/{id}', ['uses' => 'ItemController@update']); +}); diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/ApiTest.php b/tests/ApiTest.php new file mode 100644 index 0000000..76125a0 --- /dev/null +++ b/tests/ApiTest.php @@ -0,0 +1,43 @@ +get('/'); + $this->assertEquals( + $this->app->version(), $this->response->getOriginalContent()['framework_version'] + ); + } + + public function testEvents() + { + $this->get('/1/events'); + $this->assertResponseOk(); + $this->assertEquals('[]',$this->response->getContent()); + } + + public function testContainers() + { + $this->get('/1/boxes'); + $this->assertResponseOk(); + $this->assertEquals('[]',$this->response->getContent()); + } + + public function testFiles() + { + $this->get( '/1/files'); + $this->assertResponseOk(); + $this->assertEquals('[]',$this->response->getContent()); + } + + public function testItems() + { + Event::create(['slug'=>'TEST1','name'=>'Test Event 1']); + $this->get('/1/TEST1/items'); + $this->assertResponseOk(); + $this->assertEquals('[]',$this->response->getContent()); + } + +} diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php new file mode 100644 index 0000000..529bf8f --- /dev/null +++ b/tests/ContainerTest.php @@ -0,0 +1,116 @@ +get('/1/boxes'); + $this->assertResponseOk(); + $this->assertEquals('[]',$this->response->getContent()); + } + + public function testMembers() + { + Container::create(['name'=>'BOX']); + + $this->get('/1/boxes'); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(1, count($response)); + $this->assertEquals(1, $response[0]['cid']); + $this->assertEquals('BOX', $response[0]['name']); + $this->assertEquals(0, $response[0]['itemCount']); + } + + public function testmultiMembers() + { + Container::create(['name'=>'BOX 1']); + Container::create(['name'=>'BOX 2']); + Container::create(['name'=>'BOX 3']); + + $this->get('/1/boxes'); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(3, count($response)); + } + + public function testItemCount() + { + $event = Event::create(['slug'=>'EVENT','name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'item_uid'=>1, 'wann'=>'', 'wo'=>'','description'=>'1']); + Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'item_uid'=>2, 'wann'=>'', 'wo'=>'','description'=>'2']); + + $this->get('/1/boxes'); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(1, count($response)); + $this->assertEquals(2, $response[0]['itemCount']); + } + + public function testCreateContainer(){ + $this->post('/1/box', ['name'=>'BOX']); + $response = $this->response->getOriginalContent(); + + $this->assertResponseStatus(201); + $this->assertEquals(1, $response['cid']); + $this->assertEquals('BOX', $response['name']); + $this->assertEquals(0, $response['itemCount']); + + $boxes = Container::all(); + + $this->assertEquals(1, count($boxes)); + $this->assertEquals(1, $boxes[0]['cid']); + $this->assertEquals('BOX', $boxes[0]['name']); + $this->assertEquals(0, $boxes[0]['itemCount']); + } + + public function testUpdateContainer(){ + $box = Container::create(['name'=>'BOX 1']); + + $this->assertEquals(1, $box['cid']); + $this->assertEquals('BOX 1', $box['name']); + $this->assertEquals(0, $box['itemCount']); + + $this->put('/1/box/'.$box->cid, ['name'=>'BOX 2']); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(1, $response['cid']); + $this->assertEquals('BOX 2', $response['name']); + $this->assertEquals(0, $response['itemCount']); + + $boxes = Container::all(); + + $this->assertEquals(1, count($boxes)); + $this->assertEquals(1, $boxes[0]['cid']); + $this->assertEquals('BOX 2', $boxes[0]['name']); + $this->assertEquals(0, $boxes[0]['itemCount']); + } + + public function testDeleteContainer(){ + $box = Container::create(['name'=>'BOX 1']); + Container::create(['name'=>'BOX 2']); + + $this->assertEquals(2, count(Container::all())); + + $this->delete('/1/box/'.$box->cid); + + $this->assertResponseOk(); + $this->assertEquals(1, count(Container::all())); + } + +} diff --git a/tests/EventTest.php b/tests/EventTest.php new file mode 100644 index 0000000..55c5259 --- /dev/null +++ b/tests/EventTest.php @@ -0,0 +1,104 @@ +get('/1/events'); + $this->assertResponseOk(); + $this->assertEquals('[]',$this->response->getContent()); + } + + public function testMembers() + { + Event::create(['slug'=>'EVENT','name'=>'Event']); + + $this->get('/1/events'); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(1, count($response)); + $this->assertEquals(1, $response[0]['eid']); + $this->assertEquals('EVENT', $response[0]['slug']); + $this->assertEquals('Event', $response[0]['name']); + } + + public function testmultiMembers() + { + Event::create(['slug'=>'EVENT1','name'=>'Event 1']); + Event::create(['slug'=>'EVENT2','name'=>'Event 2']); + Event::create(['slug'=>'EVENT3','name'=>'Event 3']); + + $this->get('/1/events'); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(3, count($response)); + } + + public function testCreateEvent() + { + $this->post('/1/event', ['slug'=>'EVENT', 'name'=>'Event']); + $response = $this->response->getOriginalContent(); + + $this->assertResponseStatus(201); + $this->assertEquals(1, $response['eid']); + $this->assertEquals('EVENT', $response['slug']); + $this->assertEquals('Event', $response['name']); + + $events = Event::all(); + + $this->assertEquals(1, count($events)); + $this->assertEquals('EVENT', $events[0]['slug']); + $this->assertEquals('Event', $events[0]['name']); + } + + public function testUpdateEvent() + { + $event = Event::create(['slug'=>'EVENT1','name'=>'Event 1']); + + $this->assertEquals(1, $event['eid']); + $this->assertEquals('EVENT1', $event['slug']); + $this->assertEquals('Event 1', $event['name']); + + $this->put('/1/event/'.$event->eid, ['slug'=>'EVENT2', 'name'=>'Event 2']); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(1, $response['eid']); + $this->assertEquals('EVENT2', $response['slug']); + $this->assertEquals('Event 2', $response['name']); + + $events = Event::all(); + + $this->assertEquals(1, count($events)); + $this->assertEquals('EVENT2', $events[0]['slug']); + $this->assertEquals('Event 2', $events[0]['name']); + } + + public function testRemoveEvent() + { + $event = Event::create(['slug'=>'EVENT1','name'=>'Event 1']); + Event::create(['slug'=>'EVENT2','name'=>'Event 2']); + + $this->assertEquals(1, $event['eid']); + $this->assertEquals('EVENT1', $event['slug']); + $this->assertEquals('Event 1', $event['name']); + + $this->assertEquals(2, count(Event::all())); + + $this->delete('/1/event/'.$event->eid); + + $this->assertResponseOk(); + $this->assertEquals(1, count(Event::all())); + } + +} diff --git a/tests/FileTest.php b/tests/FileTest.php new file mode 100644 index 0000000..0844897 --- /dev/null +++ b/tests/FileTest.php @@ -0,0 +1,63 @@ +1,'data'=>",".base64_encode("foo")]); + + $this->get( '/1/files'); + $this->assertResponseOk(); + $this->assertEquals(1, json_decode($this->response->getContent(),true)[0]['iid']); + $this->assertEquals($item->hash, json_decode($this->response->getContent(),true)[0]['hash']); + $this->assertEquals(32, strlen(json_decode($this->response->getContent(),true)[0]['hash'])); + } + + public function testOneFile(){ + $item = File::create(['iid'=>1,'data'=>",".base64_encode("foo")]); + + $this->get( '/1/file/'.$item->hash); + $this->assertResponseOk(); + $this->assertEquals(1, json_decode($this->response->getContent(),true)['iid']); + $this->assertEquals($item->hash, json_decode($this->response->getContent(),true)['hash']); + $this->assertEquals(32, strlen(json_decode($this->response->getContent(),true)['hash'])); + } + + public function testCreateFile(){ + $event = Event::create(['slug'=>'EVENT', 'name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'1']); + $item = Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'2']); + + $this->post( '/1/file',['data'=>",".base64_encode("foo"), 'iid'=>$item->iid]); + $this->assertResponseStatus(201); + $this->assertEquals($item->iid, json_decode($this->response->getContent(),true)['iid']); + $this->assertEquals(32, strlen(json_decode($this->response->getContent(),true)['hash'])); + } + + public function testDeleteFile(){ + $event = Event::create(['slug'=>'EVENT', 'name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + $item = Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'1']); + File::create(['iid'=>$item->iid,'data'=>",".base64_encode("foo")]); + $file = File::create(['iid'=>$item->iid,'data'=>",".base64_encode("bar")]); + + $this->assertEquals(2, count(File::all())); + + $this->delete( '/1/file/'.$file->hash); + $this->assertResponseOk(); + + $this->assertEquals(1, count(Item::all())); + } + +} diff --git a/tests/ItemTest.php b/tests/ItemTest.php new file mode 100644 index 0000000..163462f --- /dev/null +++ b/tests/ItemTest.php @@ -0,0 +1,173 @@ +'EVENT','name'=>'Event']); + $this->get('/1/EVENT/items'); + $this->assertResponseOk(); + $this->assertEquals('[]',$this->response->getContent()); + } + + public function testMembers() + { + $event = Event::create(['slug'=>'EVENT','name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'1']); + + $this->get('/1/EVENT/items'); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(1, count($response)); + $this->assertEquals(1, $response[0]['uid']); + $this->assertEquals('1', $response[0]['description']); + $this->assertEquals($box->name, $response[0]['box']); + $this->assertEquals($box->cid, $response[0]['cid']); + $this->assertEquals(null, $response[0]['file']); + } + + public function testMembersWithFile() + { + $event = Event::create(['slug'=>'EVENT','name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + $item = Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'1']); + $file = File::create(['iid'=>$item->iid, 'data'=>",".base64_encode("foo")]); + + $this->get('/1/EVENT/items'); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(1, count($response)); + $this->assertEquals(1, $response[0]['uid']); + $this->assertEquals('1', $response[0]['description']); + $this->assertEquals($box->name, $response[0]['box']); + $this->assertEquals($box->cid, $response[0]['cid']); + $this->assertEquals($file->hash, $response[0]['file']); + } + + public function testmultiMembers() + { + $event = Event::create(['slug'=>'EVENT','name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'1']); + Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'2']); + Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'3']); + + $this->get('/1/EVENT/items'); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(3, count($response)); + } + + public function testCreateItem(){ + + Event::create(['slug'=>'EVENT','name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + $this->post('/1/EVENT/item',['cid'=>$box->cid, 'description'=>'1']); + $response = $this->response->getOriginalContent(); + + $this->assertResponseStatus(201); + $this->assertEquals(1, $response['uid']); + $this->assertEquals('1', $response['description']); + $this->assertEquals($box->name, $response['box']); + $this->assertEquals($box->cid, $response['cid']); + //$this->assertEquals('filename', $response['file']); + + $items = Item::all(); + $this->assertEquals(1, count($items)); + $this->assertEquals(1, $items[0]['uid']); + $this->assertEquals('1', $items[0]['description']); + $this->assertEquals($box->name, $items[0]['box']); + $this->assertEquals($box->cid, $items[0]['cid']); + //$this->assertEquals('filename', $items[0]['file']); + } + + public function testCreateItemFail(){ + + Event::create(['slug'=>'EVENT','name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + $this->post('/1/EVENT/item',[]); + $response = $this->response->getOriginalContent(); + + $this->assertResponseStatus(500); + } + + public function testUpdateItem(){ + + $event = Event::create(['slug'=>'EVENT','name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + $item = Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'1']); + + $this->assertEquals(1, $item['uid']); + $this->assertEquals('1', $item['description']); + $this->assertEquals($box->cid, $item['cid']); + + $this->put('/1/EVENT/item/'.$item->uid,['description'=>'2']); + $response = $this->response->getOriginalContent(); + + $this->assertResponseOk(); + $this->assertEquals(1, $response['uid']); + $this->assertEquals('2', $response['description']); + $this->assertEquals($box->name, $response['box']); + $this->assertEquals($box->cid, $response['cid']); + //$this->assertEquals('filename', $response['file']); + + $items = Item::all(); + $this->assertEquals(1, count($items)); + $this->assertEquals(1, $items[0]['uid']); + $this->assertEquals('2', $items[0]['description']); + $this->assertEquals($box->name, $items[0]['box']); + $this->assertEquals($box->cid, $items[0]['cid']); + //$this->assertEquals('filename', $items[0]['file']); + } + + public function testDeleteItem(){ + $event = Event::create(['slug'=>'EVENT','name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + $item = Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'1']); + Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'2']); + + $this->assertEquals(2, count(Item::all())); + + $this->delete('/1/EVENT/item/'.$item->uid); + + $this->assertResponseOk(); + $this->assertEquals(1, count(Item::all())); + } + + public function testDeleteItem2(){ + $event = Event::create(['slug'=>'EVENT','name'=>'Event']); + $box = Container::create(['name'=>'BOX']); + + Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'1']); + $item2 = Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'2']); + + $this->assertEquals(2, count(Item::all())); + + $this->delete('/1/EVENT/item/'.$item2->uid); + + $this->assertResponseOk(); + $this->assertEquals(1, count(Item::all())); + + $item3 = Item::create(['cid'=>$box->cid, 'eid' => $event->eid, 'description'=>'3']); + + $this->assertEquals(3, $item3['uid']); + $this->assertEquals(2, count(Item::all())); + + } + +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..b96aac8 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,18 @@ +//items`` - -#### ``GET /api///items/`` -Returns all results of a fuzzy search over all items for . The should be url-encoded. - -#### ``GET /api///item/`` - -#### ``DELETE /api///item/`` - -#### ``PUT /api///item/`` - -#### ``POST /api///item`` - -### Boxes - -#### ``GET /api//boxes`` - -#### ``GET /api//box/`` - -#### ``DELETE /api//box/`` - -#### ``PUT /api//box/`` - -#### ``POST /api//box/`` - -### Events -```json -{ -"name":"36. Chaos Communication Congress", -"slug":"36C3", -"start": 1577401200, -"end": 1577746800, -"pre_start": 1576882800, -"post_end": 1577919600 -} -``` - -#### ``GET /api//events`` - -returns a list of all tracked events - -### Files/Images - -#### ``GET /api//images/`` - -#### ``GET /api//thumbs/`` \ No newline at end of file diff --git a/web/README.md b/web/README.md deleted file mode 100644 index a58ee1b..0000000 --- a/web/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# c3cloc - -## Project setup -``` -yarn install -``` - -### Compiles and hot-reloads for development -``` -yarn serve -``` - -### Compiles and minifies for production -``` -yarn build -``` - -### Lints and fixes files -``` -yarn lint -``` - -### Customize configuration -See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/web/babel.config.js b/web/babel.config.js deleted file mode 100644 index c5dc6a5..0000000 --- a/web/babel.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - presets: [ - '@vue/cli-plugin-babel/preset' - ] -}; diff --git a/web/node_modules/.forgit_fordocker b/web/node_modules/.forgit_fordocker deleted file mode 100644 index e69de29..0000000 diff --git a/web/package-lock.json b/web/package-lock.json deleted file mode 100644 index a00f8ee..0000000 --- a/web/package-lock.json +++ /dev/null @@ -1,9399 +0,0 @@ -{ - "name": "c3lf", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "c3lf", - "version": "0.1.0", - "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.5.1", - "@fortawesome/free-solid-svg-icons": "^6.5.1", - "@fortawesome/vue-fontawesome": "^3.0.6", - "base-64": "^1.0.0", - "bootstrap": "^4.3.1", - "core-js": "^3.35.1", - "jquery": "^3.4.1", - "luxon": "^1.21.3", - "popper.js": "^1.16.1", - "ramda": "^0.26.1", - "sass": "^1.19.0", - "sass-loader": "^10.4.1", - "utf8": "^3.0.0", - "vue": "^3.2.47", - "vue-router": "^4.1.6", - "vuex": "^4.1.0", - "yarn": "^1.22.21" - }, - "devDependencies": { - "@vue/cli-plugin-babel": "^5.0.8", - "@vue/cli-service": "^5.0.8", - "express-basic-auth": "^1.2.1", - "http-proxy-middleware": "^2.0.6", - "vue-template-compiler": "^2.6.10", - "webpack": "^5", - "webpack-dev-server": "^4.15.1" - } - }, - "node_modules/@achrinza/node-ipc": { - "version": "9.2.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@node-ipc/js-queue": "2.0.3", - "event-pubsub": "4.3.0", - "js-message": "1.0.7" - }, - "engines": { - "node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.2", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-decorators": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-simple-access": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.25.9", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.25.9", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.25.9", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.25.9", - "@babel/plugin-transform-typeof-symbol": "^7.25.9", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.38.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.26.0", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.6.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.6.0", - "license": "MIT", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.6.0", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/vue-fontawesome": { - "version": "3.0.8", - "license": "MIT", - "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "~1 || ~6", - "vue": ">= 3.0.0 < 4" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@node-ipc/js-queue": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "easy-stack": "1.0.1" - }, - "engines": { - "node": ">=1.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.0", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "dev": true, - "license": "MIT" - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@soda/friendly-errors-webpack-plugin": { - "version": "1.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^3.0.0", - "error-stack-parser": "^2.0.6", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.0.0" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/@soda/get-current-script": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.21", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.15", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.9.0", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.8" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.9.17", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.5.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vue/babel-helper-vue-jsx-merge-props": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@vue/babel-helper-vue-transform-on": { - "version": "1.2.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@vue/babel-plugin-jsx": { - "version": "1.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.6", - "@babel/types": "^7.25.6", - "@vue/babel-helper-vue-transform-on": "1.2.5", - "@vue/babel-plugin-resolve-type": "1.2.5", - "html-tags": "^3.3.1", - "svg-tags": "^1.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - } - } - }, - "node_modules/@vue/babel-plugin-resolve-type": { - "version": "1.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/parser": "^7.25.6", - "@vue/compiler-sfc": "^3.5.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-plugin-transform-vue-jsx": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "html-tags": "^2.0.0", - "lodash.kebabcase": "^4.1.1", - "svg-tags": "^1.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-plugin-transform-vue-jsx/node_modules/html-tags": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@vue/babel-preset-app": { - "version": "5.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.12.16", - "@babel/helper-compilation-targets": "^7.12.16", - "@babel/helper-module-imports": "^7.12.13", - "@babel/plugin-proposal-class-properties": "^7.12.13", - "@babel/plugin-proposal-decorators": "^7.12.13", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.12.13", - "@babel/plugin-transform-runtime": "^7.12.15", - "@babel/preset-env": "^7.12.16", - "@babel/runtime": "^7.12.13", - "@vue/babel-plugin-jsx": "^1.0.3", - "@vue/babel-preset-jsx": "^1.1.2", - "babel-plugin-dynamic-import-node": "^2.3.3", - "core-js": "^3.8.3", - "core-js-compat": "^3.8.3", - "semver": "^7.3.4" - }, - "peerDependencies": { - "@babel/core": "*", - "core-js": "^3", - "vue": "^2 || ^3.2.13" - }, - "peerDependenciesMeta": { - "core-js": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/babel-preset-app/node_modules/semver": { - "version": "7.6.3", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@vue/babel-preset-jsx": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "@vue/babel-sugar-composition-api-inject-h": "^1.4.0", - "@vue/babel-sugar-composition-api-render-instance": "^1.4.0", - "@vue/babel-sugar-functional-vue": "^1.4.0", - "@vue/babel-sugar-inject-h": "^1.4.0", - "@vue/babel-sugar-v-model": "^1.4.0", - "@vue/babel-sugar-v-on": "^1.4.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0", - "vue": "*" - }, - "peerDependenciesMeta": { - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/babel-sugar-composition-api-inject-h": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-composition-api-render-instance": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-functional-vue": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-inject-h": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-v-model": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "camelcase": "^5.0.0", - "html-tags": "^2.0.0", - "svg-tags": "^1.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-v-model/node_modules/html-tags": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@vue/babel-sugar-v-on": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "camelcase": "^5.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/cli-overlay": { - "version": "5.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/@vue/cli-plugin-babel": { - "version": "5.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.12.16", - "@vue/babel-preset-app": "^5.0.8", - "@vue/cli-shared-utils": "^5.0.8", - "babel-loader": "^8.2.2", - "thread-loader": "^3.0.0", - "webpack": "^5.54.0" - }, - "peerDependencies": { - "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - } - }, - "node_modules/@vue/cli-plugin-router": { - "version": "5.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/cli-shared-utils": "^5.0.8" - }, - "peerDependencies": { - "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - } - }, - "node_modules/@vue/cli-plugin-vuex": { - "version": "5.0.8", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - } - }, - "node_modules/@vue/cli-service": { - "version": "5.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.12.16", - "@soda/friendly-errors-webpack-plugin": "^1.8.0", - "@soda/get-current-script": "^1.0.2", - "@types/minimist": "^1.2.0", - "@vue/cli-overlay": "^5.0.8", - "@vue/cli-plugin-router": "^5.0.8", - "@vue/cli-plugin-vuex": "^5.0.8", - "@vue/cli-shared-utils": "^5.0.8", - "@vue/component-compiler-utils": "^3.3.0", - "@vue/vue-loader-v15": "npm:vue-loader@^15.9.7", - "@vue/web-component-wrapper": "^1.3.0", - "acorn": "^8.0.5", - "acorn-walk": "^8.0.2", - "address": "^1.1.2", - "autoprefixer": "^10.2.4", - "browserslist": "^4.16.3", - "case-sensitive-paths-webpack-plugin": "^2.3.0", - "cli-highlight": "^2.1.10", - "clipboardy": "^2.3.0", - "cliui": "^7.0.4", - "copy-webpack-plugin": "^9.0.1", - "css-loader": "^6.5.0", - "css-minimizer-webpack-plugin": "^3.0.2", - "cssnano": "^5.0.0", - "debug": "^4.1.1", - "default-gateway": "^6.0.3", - "dotenv": "^10.0.0", - "dotenv-expand": "^5.1.0", - "fs-extra": "^9.1.0", - "globby": "^11.0.2", - "hash-sum": "^2.0.0", - "html-webpack-plugin": "^5.1.0", - "is-file-esm": "^1.0.0", - "launch-editor-middleware": "^2.2.1", - "lodash.defaultsdeep": "^4.6.1", - "lodash.mapvalues": "^4.6.0", - "mini-css-extract-plugin": "^2.5.3", - "minimist": "^1.2.5", - "module-alias": "^2.2.2", - "portfinder": "^1.0.26", - "postcss": "^8.2.6", - "postcss-loader": "^6.1.1", - "progress-webpack-plugin": "^1.0.12", - "ssri": "^8.0.1", - "terser-webpack-plugin": "^5.1.1", - "thread-loader": "^3.0.0", - "vue-loader": "^17.0.0", - "vue-style-loader": "^4.1.3", - "webpack": "^5.54.0", - "webpack-bundle-analyzer": "^4.4.0", - "webpack-chain": "^6.5.1", - "webpack-dev-server": "^4.7.3", - "webpack-merge": "^5.7.3", - "webpack-virtual-modules": "^0.4.2", - "whatwg-fetch": "^3.6.2" - }, - "bin": { - "vue-cli-service": "bin/vue-cli-service.js" - }, - "engines": { - "node": "^12.0.0 || >= 14.0.0" - }, - "peerDependencies": { - "vue-template-compiler": "^2.0.0", - "webpack-sources": "*" - }, - "peerDependenciesMeta": { - "cache-loader": { - "optional": true - }, - "less-loader": { - "optional": true - }, - "pug-plain-loader": { - "optional": true - }, - "raw-loader": { - "optional": true - }, - "sass-loader": { - "optional": true - }, - "stylus-loader": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - }, - "webpack-sources": { - "optional": true - } - } - }, - "node_modules/@vue/cli-shared-utils": { - "version": "5.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@achrinza/node-ipc": "^9.2.5", - "chalk": "^4.1.2", - "execa": "^1.0.0", - "joi": "^17.4.0", - "launch-editor": "^2.2.1", - "lru-cache": "^6.0.0", - "node-fetch": "^2.6.7", - "open": "^8.0.2", - "ora": "^5.3.0", - "read-pkg": "^5.1.1", - "semver": "^7.3.4", - "strip-ansi": "^6.0.0" - } - }, - "node_modules/@vue/cli-shared-utils/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@vue/cli-shared-utils/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@vue/cli-shared-utils/node_modules/semver": { - "version": "7.6.3", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@vue/cli-shared-utils/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.12", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.12", - "entities": "^4.5.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.12", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.12", - "@vue/shared": "3.5.12" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.12", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.12", - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.47", - "source-map-js": "^1.2.0" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.12", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/shared": "3.5.12" - } - }, - "node_modules/@vue/component-compiler-utils": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "consolidate": "^0.15.1", - "hash-sum": "^1.0.2", - "lru-cache": "^4.1.2", - "merge-source-map": "^1.1.0", - "postcss": "^7.0.36", - "postcss-selector-parser": "^6.0.2", - "source-map": "~0.6.1", - "vue-template-es2015-compiler": "^1.9.0" - }, - "optionalDependencies": { - "prettier": "^1.18.2 || ^2.0.0" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/hash-sum": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { - "version": "4.1.5", - "dev": true, - "license": "ISC", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/@vue/component-compiler-utils/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/yallist": { - "version": "2.1.2", - "dev": true, - "license": "ISC" - }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "license": "MIT" - }, - "node_modules/@vue/reactivity": { - "version": "3.5.12", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.12" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.12", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/shared": "3.5.12" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.12", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/runtime-core": "3.5.12", - "@vue/shared": "3.5.12", - "csstype": "^3.1.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.12", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12" - }, - "peerDependencies": { - "vue": "3.5.12" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.12", - "license": "MIT" - }, - "node_modules/@vue/vue-loader-v15": { - "name": "vue-loader", - "version": "15.11.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/component-compiler-utils": "^3.1.0", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" - }, - "peerDependencies": { - "css-loader": "*", - "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "cache-loader": { - "optional": true - }, - "prettier": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/@vue/vue-loader-v15/node_modules/hash-sum": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@vue/web-component-wrapper": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.13.1", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.12.1" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.12.1", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.12.1", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.13.1", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.12.1", - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.12.1", - "@webassemblyjs/helper-api-error": "1.12.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.12.1", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.13.1", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.13.1", - "@webassemblyjs/helper-buffer": "1.13.1", - "@webassemblyjs/helper-wasm-bytecode": "1.12.1", - "@webassemblyjs/wasm-gen": "1.13.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.12.1", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.12.1", - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.12.1", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.13.1", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.13.1", - "@webassemblyjs/helper-buffer": "1.13.1", - "@webassemblyjs/helper-wasm-bytecode": "1.12.1", - "@webassemblyjs/helper-wasm-section": "1.13.1", - "@webassemblyjs/wasm-gen": "1.13.1", - "@webassemblyjs/wasm-opt": "1.13.1", - "@webassemblyjs/wasm-parser": "1.13.1", - "@webassemblyjs/wast-printer": "1.13.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.13.1", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.13.1", - "@webassemblyjs/helper-wasm-bytecode": "1.12.1", - "@webassemblyjs/ieee754": "1.12.1", - "@webassemblyjs/leb128": "1.12.1", - "@webassemblyjs/utf8": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.13.1", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.13.1", - "@webassemblyjs/helper-buffer": "1.13.1", - "@webassemblyjs/wasm-gen": "1.13.1", - "@webassemblyjs/wasm-parser": "1.13.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.13.1", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.13.1", - "@webassemblyjs/helper-api-error": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.12.1", - "@webassemblyjs/ieee754": "1.12.1", - "@webassemblyjs/leb128": "1.12.1", - "@webassemblyjs/utf8": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.13.1", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.13.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "license": "Apache-2.0" - }, - "node_modules/accepts": { - "version": "1.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/address": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-escapes": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arch": { - "version": "2.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "2.6.4", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.20", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/babel-loader": { - "version": "8.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.4", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-loader/node_modules/loader-utils": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "object.assign": "^4.1.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/base-64": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/batch": { - "version": "0.6.1", - "dev": true, - "license": "MIT" - }, - "node_modules/big.js": { - "version": "5.2.2", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "dev": true, - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.3", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/bonjour-service": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/bootstrap": { - "version": "4.6.2", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "jquery": "1.9.1 - 3", - "popper.js": "^1.16.1" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "devOptional": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.2", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001677", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/case-sensitive-paths-webpack-plugin": { - "version": "2.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clean-css": { - "version": "5.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "dev": true, - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clipboardy": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "arch": "^2.1.1", - "execa": "^1.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/colord": { - "version": "2.9.3", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "8.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/compressible": { - "version": "2.0.18", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.5", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.0.2", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/consolidate": { - "version": "0.15.1", - "dev": true, - "license": "MIT", - "dependencies": { - "bluebird": "^3.1.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/content-type": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/copy-webpack-plugin": { - "version": "9.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.7", - "glob-parent": "^6.0.1", - "globby": "^11.0.3", - "normalize-path": "^3.0.0", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/core-js": { - "version": "3.39.0", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.39.0", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/css-declaration-sorter": { - "version": "6.4.1", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-loader": { - "version": "6.11.0", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.6.3", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "version": "3.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano": "^5.0.6", - "jest-worker": "^27.0.2", - "postcss": "^8.3.5", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@parcel/css": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/css-select": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "5.1.15", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-preset-default": "^5.2.14", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-preset-default": { - "version": "5.2.14", - "dev": true, - "license": "MIT", - "dependencies": { - "css-declaration-sorter": "^6.3.1", - "cssnano-utils": "^3.1.0", - "postcss-calc": "^8.2.3", - "postcss-colormin": "^5.3.1", - "postcss-convert-values": "^5.1.3", - "postcss-discard-comments": "^5.1.2", - "postcss-discard-duplicates": "^5.1.0", - "postcss-discard-empty": "^5.1.1", - "postcss-discard-overridden": "^5.1.0", - "postcss-merge-longhand": "^5.1.7", - "postcss-merge-rules": "^5.1.4", - "postcss-minify-font-values": "^5.1.0", - "postcss-minify-gradients": "^5.1.1", - "postcss-minify-params": "^5.1.4", - "postcss-minify-selectors": "^5.2.1", - "postcss-normalize-charset": "^5.1.0", - "postcss-normalize-display-values": "^5.1.0", - "postcss-normalize-positions": "^5.1.1", - "postcss-normalize-repeat-style": "^5.1.1", - "postcss-normalize-string": "^5.1.0", - "postcss-normalize-timing-functions": "^5.1.0", - "postcss-normalize-unicode": "^5.1.1", - "postcss-normalize-url": "^5.1.0", - "postcss-normalize-whitespace": "^5.1.1", - "postcss-ordered-values": "^5.1.3", - "postcss-reduce-initial": "^5.1.2", - "postcss-reduce-transforms": "^5.1.0", - "postcss-svgo": "^5.1.0", - "postcss-unique-selectors": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-utils": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/csso": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/de-indent": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/debounce": { - "version": "1.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "1.5.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/default-gateway/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/default-gateway/node_modules/execa": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/default-gateway/node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-gateway/node_modules/is-stream": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-gateway/node_modules/npm-run-path": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/default-gateway/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/default-gateway/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/default-gateway/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/default-gateway/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "4.3.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dotenv": { - "version": "10.0.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=10" - } - }, - "node_modules/dotenv-expand": { - "version": "5.1.0", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/duplexer": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/easy-stack": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.52", - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "stackframe": "^1.3.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.5.4", - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.2.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-pubsub": { - "version": "4.3.0", - "dev": true, - "license": "Unlicense", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "4.21.1", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express-basic-auth": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-auth": "^2.0.1" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.17.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/figures": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "devOptional": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.6", - "dev": true, - "license": "Unlicense" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "license": "BSD-2-Clause" - }, - "node_modules/globals": { - "version": "11.12.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "license": "ISC" - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-sum": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/hasown": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "dev": true, - "license": "ISC" - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-entities": { - "version": "2.5.2", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-tags": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.6.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "2.2.0", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "dev": true, - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immutable": { - "version": "4.3.7", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-file-esm": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "read-pkg-up": "^7.0.1" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/javascript-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/joi": { - "version": "17.13.3", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/jquery": { - "version": "3.7.1", - "license": "MIT" - }, - "node_modules/js-message": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/launch-editor": { - "version": "2.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" - } - }, - "node_modules/launch-editor-middleware": { - "version": "2.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "launch-editor": "^2.9.1" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "dev": true, - "license": "MIT" - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "1.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/loader-utils/node_modules/json5": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.defaultsdeep": { - "version": "4.6.1", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.kebabcase": { - "version": "4.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.mapvalues": { - "version": "4.6.0", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-update": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/mimic-fn": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/onetime": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/luxon": { - "version": "1.28.1", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/magic-string": { - "version": "0.30.12", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdn-data": { - "version": "2.0.14", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.5.3", - "dev": true, - "license": "Unlicense", - "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-source-map": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "devOptional": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "dev": true, - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/ajv": { - "version": "8.17.1", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "dev": true, - "license": "ISC" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/module-alias": { - "version": "2.2.3", - "dev": true, - "license": "MIT" - }, - "node_modules/mrmime": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "license": "MIT" - }, - "node_modules/nice-try": { - "version": "1.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/no-case": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.18", - "license": "MIT" - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/opener": { - "version": "1.5.2", - "dev": true, - "license": "(WTFPL OR MIT)", - "bin": { - "opener": "bin/opener-bin.js" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "5.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "0.1.10", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/popper.js": { - "version": "1.16.1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/portfinder": { - "version": "1.0.32", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^2.6.4", - "debug": "^3.2.7", - "mkdirp": "^0.5.6" - }, - "engines": { - "node": ">= 0.12.0" - } - }, - "node_modules/portfinder/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/postcss": { - "version": "8.4.47", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-calc": { - "version": "8.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-colormin": { - "version": "5.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-convert-values": { - "version": "5.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-comments": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-empty": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-loader": { - "version": "6.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.5", - "semver": "^7.3.5" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-loader/node_modules/semver": { - "version": "7.6.3", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "5.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-rules": { - "version": "5.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.1.0", - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "colord": "^2.9.1", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-params": { - "version": "5.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "5.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-string": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-url": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-ordered-values": { - "version": "5.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/progress-webpack-plugin": { - "version": "1.0.16", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^2.1.0", - "figures": "^2.0.0", - "log-update": "^2.3.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "peerDependencies": { - "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" - } - }, - "node_modules/progress-webpack-plugin/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/progress-webpack-plugin/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/progress-webpack-plugin/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/progress-webpack-plugin/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/progress-webpack-plugin/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/progress-webpack-plugin/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pseudomap": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "node_modules/pump": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/ramda": { - "version": "0.26.1", - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/read-pkg": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "4.0.2", - "license": "MIT", - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regexpu-core": { - "version": "6.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", - "regjsgen": "^0.8.0", - "regjsparser": "^0.11.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "dev": true, - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.11.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.0.2" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.8", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/sass": { - "version": "1.80.6", - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/sass-loader": { - "version": "10.5.2", - "license": "MIT", - "dependencies": { - "klona": "^2.0.4", - "loader-utils": "^2.0.0", - "neo-async": "^2.6.2", - "schema-utils": "^3.0.0", - "semver": "^7.3.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "webpack": "^4.36.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/sass-loader/node_modules/loader-utils": { - "version": "2.0.4", - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/sass-loader/node_modules/schema-utils": { - "version": "3.3.0", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/sass-loader/node_modules/semver": { - "version": "7.6.3", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/schema-utils": { - "version": "2.7.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "dev": true, - "license": "ISC" - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shell-quote": { - "version": "1.8.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "dev": true, - "license": "ISC" - }, - "node_modules/sirv": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "dev": true, - "license": "MIT", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.20", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/spdy": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/ssri": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/stable": { - "version": "0.1.8", - "dev": true, - "license": "MIT" - }, - "node_modules/stackframe": { - "version": "1.3.4", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/stylehacks": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-tags": { - "version": "1.0.0", - "dev": true - }, - "node_modules/svgo": { - "version": "2.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.36.0", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "license": "MIT" - }, - "node_modules/thenify": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/thread-loader": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.1.0", - "loader-utils": "^2.0.0", - "neo-async": "^2.6.2", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.27.0 || ^5.0.0" - } - }, - "node_modules/thread-loader/node_modules/loader-utils": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/thread-loader/node_modules/schema-utils": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "license": "0BSD" - }, - "node_modules/type-fest": { - "version": "0.6.0", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "license": "MIT" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.1", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8": { - "version": "3.0.0", - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/utila": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vue": { - "version": "3.5.12", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-sfc": "3.5.12", - "@vue/runtime-dom": "3.5.12", - "@vue/server-renderer": "3.5.12", - "@vue/shared": "3.5.12" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-hot-reload-api": { - "version": "2.3.4", - "dev": true, - "license": "MIT" - }, - "node_modules/vue-loader": { - "version": "17.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "watchpack": "^2.4.0" - }, - "peerDependencies": { - "webpack": "^4.1.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "@vue/compiler-sfc": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/vue-loader/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/vue-router": { - "version": "4.4.5", - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.6.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/vue-style-loader": { - "version": "4.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "hash-sum": "^1.0.2", - "loader-utils": "^1.0.2" - } - }, - "node_modules/vue-style-loader/node_modules/hash-sum": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/vue-template-compiler": { - "version": "2.7.16", - "dev": true, - "license": "MIT", - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/vue-template-es2015-compiler": { - "version": "1.9.1", - "dev": true, - "license": "MIT" - }, - "node_modules/vuex": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.0.0-beta.11" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/watchpack": { - "version": "2.4.2", - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "dev": true, - "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/webpack": { - "version": "5.96.1", - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.10.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "0.5.7", - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "commander": "^7.2.0", - "debounce": "^1.2.1", - "escape-string-regexp": "^4.0.0", - "gzip-size": "^6.0.0", - "html-escaper": "^2.0.2", - "opener": "^1.5.2", - "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/commander": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-chain": { - "version": "6.5.1", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "deepmerge": "^1.5.2", - "javascript-stringify": "^2.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.17.1", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-dev-server": { - "version": "4.15.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.4", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/ajv": { - "version": "8.17.1", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-server/node_modules/ajv-keywords": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-server/node_modules/chokidar": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/webpack-dev-server/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack-dev-server/node_modules/readdirp": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/webpack-merge": { - "version": "5.10.0", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-virtual-modules": { - "version": "0.4.6", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "dev": true, - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "1.3.1", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "7.5.10", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "1.10.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yarn": { - "version": "1.22.22", - "hasInstallScript": true, - "license": "BSD-2-Clause", - "bin": { - "yarn": "bin/yarn.js", - "yarnpkg": "bin/yarn.js" - }, - "engines": { - "node": ">=4.0.0" - } - } - } -} diff --git a/web/package.json b/web/package.json deleted file mode 100644 index 87f6d71..0000000 --- a/web/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "c3lf", - "version": "0.1.0", - "private": true, - "scripts": { - "serve": "vue-cli-service serve --modern", - "build": "vue-cli-service build --modern", - "lint": "vue-cli-service lint" - }, - "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.5.1", - "@fortawesome/free-solid-svg-icons": "^6.5.1", - "@fortawesome/vue-fontawesome": "^3.0.6", - "base-64": "^1.0.0", - "bootstrap": "^4.3.1", - "core-js": "^3.35.1", - "jquery": "^3.4.1", - "luxon": "^1.21.3", - "popper.js": "^1.16.1", - "ramda": "^0.26.1", - "sass": "^1.19.0", - "sass-loader": "^10.4.1", - "utf8": "^3.0.0", - "vue": "^3.2.47", - "vue-router": "^4.1.6", - "vuex": "^4.1.0", - "yarn": "^1.22.21" - }, - "devDependencies": { - "@vue/cli-plugin-babel": "^5.0.8", - "@vue/cli-service": "^5.0.8", - "express-basic-auth": "^1.2.1", - "http-proxy-middleware": "^2.0.6", - "vue-template-compiler": "^2.6.10", - "webpack": "^5", - "webpack-dev-server": "^4.15.1" - }, - "eslintConfig": { - "root": true, - "env": { - "node": true - }, - "extends": [ - "plugin:vue/essential", - "eslint:recommended" - ], - "rules": {}, - "parserOptions": { - "parser": "babel-eslint" - } - }, - "postcss": { - "plugins": { - "autoprefixer": {} - } - }, - "browserslist": [ - "> 1%", - "last 2 versions" - ] -} diff --git a/web/public/favicon.ico b/web/public/favicon.ico deleted file mode 100644 index f693383..0000000 Binary files a/web/public/favicon.ico and /dev/null differ diff --git a/web/public/index.html b/web/public/index.html deleted file mode 100644 index 313803b..0000000 --- a/web/public/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - c3lf - - - -
- - - diff --git a/web/src/App.vue b/web/src/App.vue deleted file mode 100644 index d2c9f7d..0000000 --- a/web/src/App.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - diff --git a/web/src/assets/logo.png b/web/src/assets/logo.png deleted file mode 100644 index 5287100..0000000 Binary files a/web/src/assets/logo.png and /dev/null differ diff --git a/web/src/components/AddBoxModal.vue b/web/src/components/AddBoxModal.vue deleted file mode 100644 index 228f117..0000000 --- a/web/src/components/AddBoxModal.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/AddEventModal.vue b/web/src/components/AddEventModal.vue deleted file mode 100644 index ed25265..0000000 --- a/web/src/components/AddEventModal.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/AddItemModal.vue b/web/src/components/AddItemModal.vue deleted file mode 100644 index 24bd449..0000000 --- a/web/src/components/AddItemModal.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/AddTicketModal.vue b/web/src/components/AddTicketModal.vue deleted file mode 100644 index b407670..0000000 --- a/web/src/components/AddTicketModal.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/AsyncLoader.vue b/web/src/components/AsyncLoader.vue deleted file mode 100644 index 00bf841..0000000 --- a/web/src/components/AsyncLoader.vue +++ /dev/null @@ -1,133 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/AuthenticatedDataLink.vue b/web/src/components/AuthenticatedDataLink.vue deleted file mode 100644 index f121af6..0000000 --- a/web/src/components/AuthenticatedDataLink.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/AuthenticatedImage.vue b/web/src/components/AuthenticatedImage.vue deleted file mode 100644 index 8b463b0..0000000 --- a/web/src/components/AuthenticatedImage.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/Cards.vue b/web/src/components/Cards.vue deleted file mode 100644 index 99ea98e..0000000 --- a/web/src/components/Cards.vue +++ /dev/null @@ -1,74 +0,0 @@ - - - diff --git a/web/src/components/CollapsableCards.vue b/web/src/components/CollapsableCards.vue deleted file mode 100644 index d38206a..0000000 --- a/web/src/components/CollapsableCards.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/web/src/components/ExpandableTable.vue b/web/src/components/ExpandableTable.vue deleted file mode 100644 index 6bd5175..0000000 --- a/web/src/components/ExpandableTable.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/Lightbox.vue b/web/src/components/Lightbox.vue deleted file mode 100644 index 74e878d..0000000 --- a/web/src/components/Lightbox.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/Matrix2D.vue b/web/src/components/Matrix2D.vue deleted file mode 100644 index 17e4b0c..0000000 --- a/web/src/components/Matrix2D.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/Matrix3D.vue b/web/src/components/Matrix3D.vue deleted file mode 100644 index 7a4d7f9..0000000 --- a/web/src/components/Matrix3D.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/Modal.vue b/web/src/components/Modal.vue deleted file mode 100644 index c2c8f0b..0000000 --- a/web/src/components/Modal.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/Navbar.vue b/web/src/components/Navbar.vue deleted file mode 100644 index 7f5e257..0000000 --- a/web/src/components/Navbar.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/Table.vue b/web/src/components/Table.vue deleted file mode 100644 index b9e3619..0000000 --- a/web/src/components/Table.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/Timeline.vue b/web/src/components/Timeline.vue deleted file mode 100644 index ee0ca4d..0000000 --- a/web/src/components/Timeline.vue +++ /dev/null @@ -1,224 +0,0 @@ - - - - - diff --git a/web/src/components/TimelineAssignment.vue b/web/src/components/TimelineAssignment.vue deleted file mode 100644 index d384825..0000000 --- a/web/src/components/TimelineAssignment.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelineComment.vue b/web/src/components/TimelineComment.vue deleted file mode 100644 index cf4bfb1..0000000 --- a/web/src/components/TimelineComment.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelineCreated.vue b/web/src/components/TimelineCreated.vue deleted file mode 100644 index 126272c..0000000 --- a/web/src/components/TimelineCreated.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelineDeleted.vue b/web/src/components/TimelineDeleted.vue deleted file mode 100644 index 076467d..0000000 --- a/web/src/components/TimelineDeleted.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelineMail.vue b/web/src/components/TimelineMail.vue deleted file mode 100644 index 5a76d95..0000000 --- a/web/src/components/TimelineMail.vue +++ /dev/null @@ -1,203 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelinePlacement.vue b/web/src/components/TimelinePlacement.vue deleted file mode 100644 index 220be01..0000000 --- a/web/src/components/TimelinePlacement.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelineRelatedItem.vue b/web/src/components/TimelineRelatedItem.vue deleted file mode 100644 index 2670215..0000000 --- a/web/src/components/TimelineRelatedItem.vue +++ /dev/null @@ -1,208 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelineRelatedTicket.vue b/web/src/components/TimelineRelatedTicket.vue deleted file mode 100644 index 694a4e0..0000000 --- a/web/src/components/TimelineRelatedTicket.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelineReturned.vue b/web/src/components/TimelineReturned.vue deleted file mode 100644 index 6eb740b..0000000 --- a/web/src/components/TimelineReturned.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelineShippingVoucher.vue b/web/src/components/TimelineShippingVoucher.vue deleted file mode 100644 index fb7c575..0000000 --- a/web/src/components/TimelineShippingVoucher.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/TimelineStateChange.vue b/web/src/components/TimelineStateChange.vue deleted file mode 100644 index d771b9e..0000000 --- a/web/src/components/TimelineStateChange.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/inputs/Addon.vue b/web/src/components/inputs/Addon.vue deleted file mode 100644 index 0a1c5b6..0000000 --- a/web/src/components/inputs/Addon.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - diff --git a/web/src/components/inputs/AsyncButton.vue b/web/src/components/inputs/AsyncButton.vue deleted file mode 100644 index 6ae298f..0000000 --- a/web/src/components/inputs/AsyncButton.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - - diff --git a/web/src/components/inputs/ClipboardButton.vue b/web/src/components/inputs/ClipboardButton.vue deleted file mode 100644 index 0f164e6..0000000 --- a/web/src/components/inputs/ClipboardButton.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/web/src/components/inputs/InputCombo.vue b/web/src/components/inputs/InputCombo.vue deleted file mode 100644 index 2a291e0..0000000 --- a/web/src/components/inputs/InputCombo.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - diff --git a/web/src/components/inputs/InputPhoto.vue b/web/src/components/inputs/InputPhoto.vue deleted file mode 100644 index 90745a4..0000000 --- a/web/src/components/inputs/InputPhoto.vue +++ /dev/null @@ -1,142 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/inputs/InputString.vue b/web/src/components/inputs/InputString.vue deleted file mode 100644 index 9586d78..0000000 --- a/web/src/components/inputs/InputString.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/inputs/SearchBox.vue b/web/src/components/inputs/SearchBox.vue deleted file mode 100644 index 79fb798..0000000 --- a/web/src/components/inputs/SearchBox.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/main.js b/web/src/main.js deleted file mode 100644 index f6fe706..0000000 --- a/web/src/main.js +++ /dev/null @@ -1,56 +0,0 @@ -import {createApp} from 'vue' -import App from './App.vue'; -import store from './store'; -import router from './router'; - -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'bootstrap/dist/js/bootstrap.min.js'; - -// fontawesome -import {library} from '@fortawesome/fontawesome-svg-core'; -import { - faPlus, - faCheckCircle, - faEdit, - faTrash, - faCat, - faSyncAlt, - faSort, - faSortUp, - faSortDown, - faTh, - faList, - faWindowClose, - faCamera, - faStop, - faPen, - faCheck, - faTimes, - faSave, - faEye, - faComment, - faEnvelope, - faUser, - faComments, - faArchive, - faMinus, - faHourglass, - faExclamation, - faClipboard, - faTasks, - faAngleRight, - faAngleDown, - faTruck, - faObjectGroup -} from '@fortawesome/free-solid-svg-icons'; -import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'; - -library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, - faWindowClose, faCamera, faStop, faPen, faCheck, faTimes, faSave, faEye, faComment, faUser, faComments, faEnvelope, - faArchive, faMinus, faExclamation, faHourglass, faClipboard, faTasks, faAngleDown, faAngleRight, faTruck, faObjectGroup); - - -const app = createApp(App).use(store).use(router); - -app.component('font-awesome-icon', FontAwesomeIcon); -app.mount('#app') \ No newline at end of file diff --git a/web/src/mixins/data-container.js b/web/src/mixins/data-container.js deleted file mode 100644 index 2077540..0000000 --- a/web/src/mixins/data-container.js +++ /dev/null @@ -1,39 +0,0 @@ -import * as R from 'ramda'; - -export default { - props: ['columns', 'items', 'actions', 'keyName'], - data: (self) => ({ - sortBy: self.keyName, - ascend: true, - filters: R.fromPairs(self.columns.map(column => [column, ''])) - }), - computed: { - internalItems() { - const filtered = this.items.filter(item => this.columns - .map(column => { - const field = item[column] + ''; - const filter = this.filters[column]; - return field.toLowerCase().includes(filter.toLowerCase()); - }).reduce((acc, nxt) => acc && nxt, true) - ); - const sortByOrd = R.sortBy(R.prop(this.sortBy)); - const sorted = sortByOrd(filtered, [this.sortBy]); - return this.ascend ? sorted : R.reverse(sorted); - } - }, - methods: { - getSortIcon(column) { - if (column !== this.sortBy) return 'sort'; - if (this.ascend) return 'sort-up'; - return 'sort-down'; - }, - toggleSort(column) { - if (column === this.sortBy) - this.ascend = !this.ascend; - this.sortBy = column; - }, - setFilter(column, filter) { - this.filters[column] = filter; - } - } -}; \ No newline at end of file diff --git a/web/src/persistent-state-plugin/index.js b/web/src/persistent-state-plugin/index.js deleted file mode 100644 index 14f5577..0000000 --- a/web/src/persistent-state-plugin/index.js +++ /dev/null @@ -1,74 +0,0 @@ -import {isProxy, toRaw} from 'vue'; - -export default (config) => (store) => { - if (!('isLoadedKey' in config)) { - throw new Error("isLoadedKey not defined in config"); - } - - const initialize = () => { - config.state.forEach(k => { - try { - if (config.debug) console.log("localStorage init", k, localStorage.getItem(config.prefix + k)); - const parsed = JSON.parse(localStorage.getItem(config.prefix + k)); - if (parsed !== store.state[k] && parsed !== null) { - store.state[k] = parsed; - } else { - if (config.debug) console.log("localStorage not loaded", k, localStorage.getItem(config.prefix + k)); - } - } catch (e) { - if (config.debug) console.log("localStorage parse error", k, e); - } - }); - store.state[config.isLoadedKey] = true; - if ('validate' in config) { - config.validate(store.state); - } - } - - const reload = initialize; - - if (store.state[config.isLoadedKey] !== true) - initialize(); - - addEventListener('storage', reload); - - if ('state' in config) { - config.state.forEach((member) => { - store.watch((state, getters) => state[member], (newValue, oldValue) => { - try { - if (config.debug) console.log('watch', member, - isProxy(newValue) ? toRaw(newValue) : newValue, - isProxy(oldValue) ? toRaw(oldValue) : oldValue); - const key = config.prefix + member; - const encoded = JSON.stringify(isProxy(newValue) ? toRaw(newValue) : newValue); - if (encoded !== localStorage.getItem(key)) { - if (config.debug) console.log("localStorage replace", member, localStorage.getItem(key), encoded); - if (newValue === null) - localStorage.removeItem(key); - else - localStorage.setItem(key, encoded); - } else { - if (config.debug) console.log("localStorage not saved", member, localStorage.getItem(key), encoded); - } - } catch (e) { - if (config.debug) console.log("localsorage save error", member, e); - } - }); - }); - } - - if ('clearingMutation' in config) { - store.subscribe((mutation, state) => { - if (mutation.type === config.clearingMutation) { - removeEventListener('storage', reload) - for (let key in config.state) { - localStorage.removeItem(config.prefix + key); - } - for (let key in config.state) { - store.state[key] = null; - } - addEventListener('storage', reload) - } - }); - } -}; \ No newline at end of file diff --git a/web/src/router.js b/web/src/router.js deleted file mode 100644 index 03c1c14..0000000 --- a/web/src/router.js +++ /dev/null @@ -1,123 +0,0 @@ -import {createRouter, createWebHistory} from 'vue-router' -import store from '@/store'; - -import Item from "@/views/Item.vue"; -import ItemSearch from "@/views/ItemSearch.vue"; -import Items from '@/views/Items'; -import Boxes from '@/views/Boxes'; -import Files from '@/views/Files'; -import HowTo from '@/views/HowTo'; -import Login from '@/views/Login.vue'; -import Register from '@/views/Register.vue'; -import Dashboard from "@/views/admin/Dashboard.vue"; -import Tickets from "@/views/Tickets.vue"; -import TicketSearch from "@/views/TicketSearch.vue"; -import Ticket from "@/views/Ticket.vue"; -import Admin from "@/views/admin/Admin.vue"; -import Empty from "@/views/Empty.vue"; -import Events from "@/views/admin/Events.vue"; -import AccessControl from "@/views/admin/AccessControl.vue"; -import {default as BoxesAdmin} from "@/views/admin/Boxes.vue" -import Shipping from "@/views/admin/Shipping.vue"; - -const routes = [ - {path: '/', redirect: '/37C3/items', meta: {requiresAuth: false}}, - {path: '/login/', name: 'login', component: Login, meta: {requiresAuth: false}}, - {path: '/register/', name: 'register', component: Register, meta: {requiresAuth: false}}, - {path: '/howto/', name: 'howto', component: HowTo, meta: {requiresAuth: true}}, - { - path: '/:event/items/', name: 'items', component: Items, meta: - {requiresAuth: true, requiresPermission: 'view_item'} - }, - { - path: '/:event/items/:search', name: 'item_search', component: ItemSearch, meta: - {requiresAuth: true, requiresPermission: 'view_item'} - }, - { - path: '/:event/item/:id/', name: 'item', component: Item, meta: - {requiresAuth: true, requiresPermission: 'view_item'} - }, - { - path: '/:event/boxes/', name: 'boxes', component: Boxes, meta: - {requiresAuth: true, requiresPermission: 'view_container'} - }, - { - path: '/:event/box/:uid/', name: 'box', component: Boxes, meta: - {requiresAuth: true, requiresPermission: 'view_container'} - }, - { - path: '/:event/tickets/', name: 'tickets', component: Tickets, meta: - {requiresAuth: true, requiresPermission: 'view_issuethread'} - }, - { - path: '/:event/tickets/:search', name: 'ticket_search', component: TicketSearch, meta: - {requiresAuth: true, requiresPermission: 'view_issuethread'} - }, - { - path: '/:event/ticket/:id/', name: 'ticket', component: Ticket, meta: - {requiresAuth: true, requiresPermission: 'view_issuethread'} - }, - { - path: '/admin/', component: Admin, meta: - {requiresAuth: true, requiresPermission: 'delete_event'}, - children: [ - { - path: 'files/', name: 'files', component: Files, meta: - {requiresAuth: true, requiresPermission: 'delete_event'} - }, - { - path: 'events/', name: 'events', component: Events, meta: - {requiresAuth: true, requiresPermission: 'delete_event'} - }, - { - path: '', name: 'admin', component: Dashboard, meta: - {requiresAuth: true, requiresPermission: 'delete_event'} - }, - { - path: 'users/', name: 'users', component: AccessControl, meta: - {requiresAuth: true, requiresPermission: 'delete_event'} - }, - { - path: 'boxes/', name: 'admin_boxes', component: BoxesAdmin, meta: - {requiresAuth: true, requiresPermission: 'delete_event'} - }, - { - path: 'shipping/', name: 'shipping', component: Shipping, meta: - {requiresAuth: true, requiresPermission: 'delete_event'} - }, - ] - }, - {path: '/user', name: 'user', component: Empty, meta: {requiresAuth: true}}, -]; - -const router = createRouter({ - history: createWebHistory(), - linkActiveClass: "active", - routes, -}); - -router.beforeEach((to, from, next) => { - if (to.meta.requiresAuth && !store.getters.isLoggedIn) { - console.log("Not logged in, redirecting to login page") - next({ - name: 'login', - query: {redirect: to.fullPath}, - }) - } else if (to.meta.requiresPermission && !store.getters.checkPermission(to.params.event || "*", to.meta.requiresPermission)) { - console.log("Not enough permissions, redirecting to empty page") - next({ - path: '/user', - }) - } else { - next() - } -}); - -router.afterEach((to, from) => { - if (to.params.event && to.params.event !== store.state.lastEvent) { - //console.log('update last event', to.params.event); - store.commit('updateLastEvent', to.params.event); - } -}); - -export default router; diff --git a/web/src/scss/navbar.scss b/web/src/scss/navbar.scss deleted file mode 100644 index f74958c..0000000 --- a/web/src/scss/navbar.scss +++ /dev/null @@ -1,10 +0,0 @@ -button.btn-heading { - font-size: 2em; - padding: 0 .75rem; - line-height: 1em; -} - -.btn-heading::after { - font-size: 0.5em; - line-height: 2em; -} diff --git a/web/src/shared-state-plugin/index.js b/web/src/shared-state-plugin/index.js deleted file mode 100644 index 67397d0..0000000 --- a/web/src/shared-state-plugin/index.js +++ /dev/null @@ -1,345 +0,0 @@ -import {isProxy, toRaw} from 'vue'; - -export default (config) => { - if (!('isLoadedKey' in config)) { - throw new Error("isLoadedKey not defined in config"); - } - if (('asyncFetch' in config) && !('lastfetched' in config)) { - throw new Error("asyncFetch defined but lastfetched not defined in config"); - } - - if (config.debug) console.log('plugin created'); - - const clone = (obj) => { - if (isProxy(obj)) { - obj = toRaw(obj); - } - if (obj === null || typeof obj !== 'object') { - return obj; - } - if (obj.__proto__ === ({}).__proto__) { - return Object.assign({}, obj); - } - if (obj.__proto__ === [].__proto__) { - return obj.slice(); - } - return obj; - } - - const deepEqual = (a, b) => { - if (a === b) { - return true; - } - if (a === null || b === null) { - return false; - } - if (a.__proto__ === ({}).__proto__ && b.__proto__ === ({}).__proto__) { - - if (Object.keys(a).length !== Object.keys(b).length) { - return false; - } - for (let key in b) { - if (!(key in a)) { - return false; - } - } - for (let key in a) { - if (!(key in b)) { - return false; - } - if (!deepEqual(a[key], b[key])) { - return false; - } - } - return true; - } - if (a.__proto__ === [].__proto__ && b.__proto__ === [].__proto__) { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - if (!deepEqual(a[i], b[i])) { - return false; - } - } - return true; - } - return false; - } - - const toRawRecursive = (obj) => { - if (isProxy(obj)) { - obj = toRaw(obj); - } - if (obj === null || typeof obj !== 'object') { - return obj; - } - if (obj.__proto__ === ({}).__proto__) { - const new_obj = {}; - for (let key in obj) { - new_obj[key] = toRawRecursive(obj[key]); - } - return new_obj; - } - if (obj.__proto__ === [].__proto__) { - return obj.map((item) => toRawRecursive(item)); - } - return obj; - } - - /** may only be called from worker */ - const worker_fun = function (self, ctx) { - /* globals WebSocket, SharedWorker, onconnect, onmessage, postMessage, close, location */ - - let intialized = false; - let state = {}; - let ports = []; - let notify_socket; - - const tryConnect = () => { - if (self.WebSocket === undefined) { - if (ctx.debug) console.log("no websocket support"); - return; - } - if (!notify_socket || notify_socket.readyState !== WebSocket.OPEN) { - // global location is not useful in worker loaded from data url - const scheme = ctx.location.protocol === "https:" ? "wss" : "ws"; - if (ctx.debug) console.log("connecting to", scheme + '://' + ctx.location.host + '/ws/2/notify/'); - notify_socket = new WebSocket(scheme + '://' + ctx.location.host + '/ws/2/notify/'); - notify_socket.onopen = (e) => { - if (ctx.debug) console.log("open", JSON.stringify(e)); - }; - notify_socket.onclose = (e) => { - if (ctx.debug) console.log("close", JSON.stringify(e)); - setTimeout(() => { - tryConnect(); - }, 1000); - }; - notify_socket.onerror = (e) => { - if (ctx.debug) console.log("error", JSON.stringify(e)); - setTimeout(() => { - tryConnect(); - }, 1000); - }; - notify_socket.onmessage = (e) => { - let data = JSON.parse(e.data); - if (ctx.debug) console.log("message", data); - //this.loadEventItems() - //this.loadTickets() - } - } - } - - const deepEqual = (a, b) => { - if (a === b) { - return true; - } - if (a === null || b === null) { - return false; - } - if (a.__proto__ === ({}).__proto__ && b.__proto__ === ({}).__proto__) { - - if (Object.keys(a).length !== Object.keys(b).length) { - return false; - } - for (let key in b) { - if (!(key in a)) { - return false; - } - } - for (let key in a) { - if (!(key in b)) { - return false; - } - if (!deepEqual(a[key], b[key])) { - return false; - } - } - return true; - } - if (a.__proto__ === [].__proto__ && b.__proto__ === [].__proto__) { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - if (!deepEqual(a[i], b[i])) { - return false; - } - } - return true; - } - return false; - } - - const handle_message = (message_data, reply, others, all) => { - switch (message_data.type) { - case 'state_init': - if (!intialized) { - intialized = true; - state = message_data.state; - reply({type: 'state_init', first: true}); - } else { - reply({type: 'state_init', first: false, state: state}); - } - break; - case 'state_diff': - if (message_data.key in state) { - if (!deepEqual(state[message_data.key], message_data.old_value)) { - if (ctx.debug) console.log("state diff old value mismatch | state:", state[message_data.key], " old:", message_data.old_value); - } - if (!deepEqual(state[message_data.key], message_data.new_value)) { - if (ctx.debug) console.log("state diff changed | state:", state[message_data.key], " new:", message_data.new_value); - state[message_data.key] = message_data.new_value; - others(message_data); - } else { - if (ctx.debug) console.log("state diff no change | state:", state[message_data.key], " new:", message_data.new_value); - } - } else { - if (ctx.debug) console.log("state diff key not found", message_data.key); - } - break; - default: - if (ctx.debug) console.log("unknown message", message_data); - } - } - - onconnect = (connect_event) => { - const port = connect_event.ports[0]; - ports.push(port); - port.onmessage = (message_event) => { - const reply = (message_data) => { - port.postMessage(message_data); - } - const others = (message_data) => { - for (let i = 0; i < ports.length; i++) { - if (ports[i] !== port) { - ports[i].postMessage(message_data); - } - } - } - const all = (message_data) => { - for (let i = 0; i < ports.length; i++) { - ports[i].postMessage(message_data); - } - } - handle_message(message_event.data, reply, others, all); - } - port.start(); - if (ctx.debug) console.log("worker connected", JSON.stringify(connect_event)); - tryConnect(); - } - - if (ctx.debug) console.log("worker loaded"); - } - - const worker_context = { - location: { - protocol: location.protocol, host: location.host - }, bug: config.debug - } - const worker_code = '(' + worker_fun.toString() + ')(self,' + JSON.stringify(worker_context) + ')'; - const worker_url = 'data:application/javascript;base64,' + btoa(worker_code); - - const worker = new SharedWorker(worker_url, 'vuex-shared-state-plugin'); - worker.port.start(); - if (config.debug) console.log('worker started'); - - const updateWorkerState = (key, new_value, old_value = null) => { - if (new_value === old_value) { - if (config.debug) console.log('updateWorkerState: no change', key, new_value); - return; - } - if (new_value === undefined) { - if (config.debug) console.log('updateWorkerState: undefined', key, new_value); - return; - } - - worker.port.postMessage({ - type: 'state_diff', - key: key, - new_value: isProxy(new_value) ? toRawRecursive(new_value) : new_value, - old_value: isProxy(old_value) ? toRawRecursive(old_value) : old_value - }); - } - - const registerInitialState = (keys, local_state) => { - const value = keys.reduce((obj, key) => { - obj[key] = isProxy(local_state[key]) ? toRawRecursive(local_state[key]) : local_state[key]; - return obj; - }, {}); - if (config.debug) console.log('registerInitilState', value); - worker.port.postMessage({ - type: 'state_init', state: value - }); - } - - return (store) => { - - worker.port.onmessage = function (e) { - switch (e.data.type) { - case 'state_init': - if (config.debug) console.log('state_init', e.data); - if (e.data.first) { - if (config.debug) console.log('worker state initialized'); - } else { - for (let key in e.data.state) { - if (key in store.state) { - if (config.debug) console.log('worker state init received', key, clone(e.data.state[key])); - if (!deepEqual(store.state[key], e.data.state[key])) { - store.state[key] = e.data.state[key]; - } - } else { - if (config.debug) console.log("state init key not found", key); - } - } - } - store.state[config.isLoadedKey] = true; - if ('afterInit' in config) { - setTimeout(() => { - store.dispatch(config.afterInit); - }, 0); - } - break; - case 'state_diff': - if (config.debug) console.log('state_diff', e.data); - if (e.data.key in store.state) { - if (config.debug) console.log('worker state update', e.data.key, clone(e.data.new_value)); - //TODO this triggers the watcher again, but we don't want that - store.state[e.data.key] = e.data.new_value; - } else { - if (config.debug) console.log("state diff key not found", e.data.key); - } - break; - default: - if (config.debug) console.log("unknown message", e.data); - } - }; - - registerInitialState(config.state, store.state); - - if ('mutations' in config) { - store.subscribe((mutation, state) => { - if (mutation.type in config.mutations) { - console.log(mutation.type, mutation.payload); - console.log(state); - } - }); - } - /*if ('actions' in config) { - store.subscribeAction((action, state) => { - if (action.type in config.actions) { - console.log(action.type, action.payload); - console.log(state); - } - }); - }*/ - if ('state' in config) { - config.watch.forEach((member) => { - store.watch((state, getters) => state[member], (newValue, oldValue) => { - if (config.debug) console.log('watch', member, clone(newValue), clone(oldValue)); - updateWorkerState(member, newValue, oldValue); - }); - }); - } - }; -} \ No newline at end of file diff --git a/web/src/store.js b/web/src/store.js deleted file mode 100644 index 34650e4..0000000 --- a/web/src/store.js +++ /dev/null @@ -1,619 +0,0 @@ -import {createStore} from 'vuex'; -import router from './router'; - -import * as base64 from 'base-64'; -import * as utf8 from 'utf8'; -import {ticketStateColorLookup, ticketStateIconLookup, http, http_session} from "@/utils"; -import sharedStatePlugin from "@/shared-state-plugin"; -import persistentStatePlugin from "@/persistent-state-plugin"; - -const store = createStore({ - state: { - //shared - keyIncrement: 0, - events: [], - items: [], - loadedBoxes: [], - toasts: [], - users: [], - groups: [], - state_options: [], - shippingVouchers: [], - - loadedItems: {}, - loadedTickets: {}, - - loadedItemSearchResults: {}, - loadedTicketSearchResults: {}, - - //local - lastEvent: 'all', - lastUsed: {}, - remember: false, - user: { - username: null, - password: null, - permissions: [], - token: null, - expiry: null, - }, - - lightboxHash: null, - thumbnailCache: {}, - fetchedData: { - events: 0, - items: 0, - boxes: 0, - tickets: 0, - users: 0, - groups: 0, - states: 0, - shippingVouchers: 0, - }, - persistent_loaded: false, - shared_loaded: false, - afterInitHandlers: [], - - showAddBoxModal: false, - showAddEventModal: false, - - shippingVoucherTypes: { - '2kg-de': '2kg Paket (DE)', - '5kg-de': '5kg Paket (DE)', - '10kg-de': '10kg Paket (DE)', - '5kg-eu': '5kg Paket (EU)', - '10kg-eu': '10kg Paket (EU)', - } - }, - getters: { - route: state => router.currentRoute.value, - session: state => http_session(state.user.token), - getEventSlug: state => router.currentRoute.value.params.event ? router.currentRoute.value.params.event : state.lastEvent, - searchQuery: state => router.currentRoute.value.params.search, - getAllItems: state => Object.values(state.loadedItems).flat(), - getAllTickets: state => Object.values(state.loadedTickets).flat(), - getEventItems: (state, getters) => getters.getEventSlug === 'all' ? getters.getAllItems : getters.getAllItems.filter(t => t.event === getters.getEventSlug || (t.event == null && getters.getEventSlug === 'none')), - getEventTickets: (state, getters) => getters.getEventSlug === 'all' ? getters.getAllTickets : getters.getAllTickets.filter(t => t.event === getters.getEventSlug || (t.event == null && getters.getEventSlug === 'none')), - isItemsLoaded: (state, getters) => (getters.getEventSlug === 'all' || getters.getEventSlug === 'none') ? !!state.loadedItems : Object.keys(state.loadedItems).includes(getters.getEventSlug), - isTicketsLoaded: (state, getters) => (getters.getEventSlug === 'all' || getters.getEventSlug === 'none') ? !!state.loadedTickets : Object.keys(state.loadedTickets).includes(getters.getEventSlug), - getItemsSearchResults: (state, getters) => { - if (getters.getEventSlug === 'all') { - return state.events.map(e => { - return state.loadedItemSearchResults[e.slug + '/' + base64.encode(utf8.encode(getters.searchQuery))] || [] - }).flat(); - } else { - return state.loadedItemSearchResults[getters.getEventSlug + '/' + base64.encode(utf8.encode(getters.searchQuery))] || [] - } - }, - getTicketsSearchResults: (state, getters) => { - if (getters.getEventSlug === 'all') { - return state.events.map(e => { - return state.loadedTicketSearchResults[e.slug + '/' + base64.encode(utf8.encode(getters.searchQuery))] || [] - }).flat(); - } else { - return state.loadedTicketSearchResults[getters.getEventSlug + '/' + base64.encode(utf8.encode(getters.searchQuery))] || [] - } - }, - isItemsSearchLoaded: (state, getters) => Object.keys(state.loadedItemSearchResults).includes(getters.getEventSlug + '/' + base64.encode(utf8.encode(getters.searchQuery))) || getters.getEventSlug === 'all', - isTicketsSearchLoaded: (state, getters) => Object.keys(state.loadedTicketSearchResults).includes(getters.getEventSlug + '/' + base64.encode(utf8.encode(getters.searchQuery))) || getters.getEventSlug === 'all', - getActiveView: state => router.currentRoute.value.name || 'items', - getFilters: state => router.currentRoute.value.query, - getBoxes: state => state.loadedBoxes, - checkPermission: state => (event, perm) => state.user.permissions && - (state.user.permissions.includes(`${event}:${perm}`) || state.user.permissions.includes(`*:${perm}`)), - hasPermissions: state => state.user.permissions && state.user.permissions.length > 0, - activeUser: state => state.user.username || 'anonymous', - stateInfo: state => (slug) => { - const obj = state.state_options.filter((s) => s.value === slug)[0]; - if (obj) { - return { - color: ticketStateColorLookup(obj.value), - icon: ticketStateIconLookup(obj.value), - slug: obj.value, - text: obj.text, - } - } else { - return { - color: 'danger', - icon: 'exclamation', - slug: slug, - text: 'Unknown' - } - } - }, - availableShippingVoucherTypes: state => { - return Object.keys(state.shippingVoucherTypes).map(key => { - var count = state.shippingVouchers.filter(voucher => voucher.type === key && voucher.issue_thread === null).length; - return {id: key, count: count, name: state.shippingVoucherTypes[key]}; - }); - }, - layout: (state, getters) => { - if (router.currentRoute.value.query.layout) - return router.currentRoute.value.query.layout; - if (getters.getActiveView === 'items' || getters.getActiveView === 'item_search') - return 'cards'; - if (getters.getActiveView === 'tickets' || getters.getActiveView === 'ticket_search') - return 'tasks'; - }, - isLoggedIn(state) { - return state.user && state.user.username !== null && state.user.token !== null; - }, - getThumbnail: (state) => (url) => { - if (!url) return null; - if (!(url in state.thumbnailCache)) - return null; - return state.thumbnailCache[url]; - }, - }, - mutations: { - updateLastUsed(state, diff) { - state.lastUsed = {...state.lastUsed, ...diff}; - }, - updateLastEvent(state, slug) { - state.lastEvent = slug; - }, - replaceEvents(state, events) { - state.events = events; - state.fetchedData = {...state.fetchedData, events: Date.now()}; - }, - replaceTicketStates(state, states) { - state.state_options = states; - state.fetchedData = {...state.fetchedData, states: Date.now()}; - }, - changeView(state, {view, slug}) { - router.push({path: `/${slug}/${view}`}); - }, - replaceBoxes(state, loadedBoxes) { - state.loadedBoxes = loadedBoxes; - state.fetchedData = {...state.fetchedData, boxes: Date.now()}; - }, - setItems(state, {slug, items}) { - state.loadedItems[slug] = items; - state.loadedItems = {...state.loadedItems}; - }, - setItemSearchResults(state, {slug, query, items}) { - state.loadedItemSearchResults[slug + '/' + query] = items; - state.loadedItemSearchResults = {...state.loadedItemSearchResults}; - }, - replaceItems(state, items) { - const groups = Object.groupBy(items, i => i.event ? i.event : 'none') - for (const [key, value] of Object.entries(groups)) state.loadedItems[key] = value; - state.loadedItems = {...state.loadedItems}; - }, - updateItem(state, updatedItem) { - const item = state.loadedItems[updatedItem.event ? updatedItem.event : 'none'].filter( - ({id}) => id === updatedItem.id)[0]; - Object.assign(item, updatedItem); - }, - removeItem(state, item) { - state.loadedItems[item.event ? item.event : 'none'] = state.loadedItems[item.event].filter(it => it !== item); - }, - appendItem(state, item) { - state.loadedItems[item.event ? item.event : 'none'].push(item); - }, - setTickets(state, {slug, tickets}) { - state.loadedTickets[slug] = tickets; - state.loadedTickets = {...state.loadedTickets}; - }, - setTicketSearchResults(state, {slug, query, items}) { - state.loadedTicketSearchResults[slug + '/' + query] = items; - state.loadedTicketSearchResults = {...state.loadedTicketSearchResults}; - }, - replaceTickets(state, tickets) { - const groups = Object.groupBy(tickets, t => t.event ? t.event : 'none') - for (const [key, value] of Object.entries(groups)) state.loadedTickets[key] = value; - state.loadedTickets = {...state.loadedTickets}; - }, - updateTicket(state, updatedTicket) { - const ticket = state.loadedTickets[updatedTicket.event ? updatedTicket.event : 'none'].filter( - ({id}) => id === updatedTicket.id)[0]; - Object.assign(ticket, updatedTicket); - state.loadedTickets = {...state.loadedTickets}; - }, - replaceUsers(state, users) { - state.users = users; - state.fetchedData = {...state.fetchedData, users: Date.now()}; - }, - replaceGroups(state, groups) { - state.groups = groups; - state.fetchedData = {...state.fetchedData, groups: Date.now()}; - }, - openLightboxModalWith(state, hash) { - state.lightboxHash = hash; - }, - openAddBoxModal(state) { - state.showAddBoxModal = true; - }, - closeAddBoxModal(state) { - state.showAddBoxModal = false; - }, - openAddEventModal(state) { - state.showAddEventModal = true; - }, - closeAddEventModal(state) { - state.showAddEventModal = false; - }, - createToast(state, {title, message, color}) { - var toast = {title, message, color, key: state.keyIncrement} - state.toasts.push(toast); - state.keyIncrement += 1; - return toast; - }, - removeToast(state, key) { - state.toasts = state.toasts.filter(toast => toast.key !== key); - }, - setRemember(state, remember) { - state.remember = remember; - }, - setUser(state, user) { - state.user.username = user; - }, - setPassword(state, password) { - state.user.password = password; - }, - setPermissions(state, permissions) { - state.user.permissions = permissions; - }, - setToken(state, {token, expiry}) { - const user = {...state.user}; - user.token = token; - user.expiry = expiry; - state.user = user; - }, - setUserInfo(state, user) { - state.user = user; - }, - logout(state) { - const user = {...state.user}; - user.user = null; - user.password = null; - user.token = null; - user.expiry = null; - user.permissions = null; - state.user = user; - }, - setThumbnail(state, {url, data}) { - state.thumbnailCache[url] = data; - }, - setShippingVouchers(state, codes) { - state.shippingVouchers = codes; - state.fetchedData = {...state.fetchedData, shippingVouchers: Date.now()}; - }, - }, - actions: { - async login({commit}, {username, password, remember}) { - commit('setRemember', remember); - try { - const data = await fetch('/api/2/login/', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({username: username, password: password}), - credentials: 'omit' - }).then(r => r.json()) - if (data && data.token) { - const {data: {permissions}} = await http.get('/2/self/', data.token); - commit('setUserInfo', {...data, permissions, username, password}); - return true; - } else { - return false; - } - } catch (e) { - console.error(e); - return false; - } - }, - async reloadToken({commit, state}) { - try { - if (state.user.username && state.user.password) { - const data = await fetch('/api/2/login/', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({username: state.user.username, password: state.user.password}), - credentials: 'omit' - }).then(r => r.json()).catch(e => console.error(e)) - if (data && data.token) { - commit('setToken', data); - return true; - } - } - } catch (e) { - console.error(e); - } - //credentials failed, logout - store.commit('logout'); - }, - //async verifyToken({commit, state}) { - async afterLogin({dispatch, state}) { - let promises = []; - promises.push(dispatch('loadBoxes')); - promises.push(dispatch('fetchTicketStates')); - promises.push(dispatch('loadEventItems')); - promises.push(dispatch('loadTickets')); - if (!state.user.permissions) { - promises.push(dispatch('loadUserInfo')); - } - await Promise.all(promises); - }, - async afterSharedInit({dispatch, state}) { - const handlers = state.afterInitHandlers; - state.afterInitHandlers = []; - await Promise.all(handlers.map(h => h()).flat()); - }, - scheduleAfterInit({dispatch, state}, handler) { - if (state.shared_loaded) { - Promise.all(handler()).then(() => { - }); - } else { - state.afterInitHandlers.push(handler); - } - }, - async fetchImage({state}, url) { - return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}}); - }, - async loadUserInfo({commit, getters}) { - const {data, success} = await getters.session.get('/2/self/'); - commit('setPermissions', data.permissions); - }, - async loadEvents({commit, state, getters}) { - if (!state.user.token) return; - if (state.fetchedData.events > Date.now() - 1000 * 60 * 60 * 24) return; - const {data, success} = await getters.session.get('/2/events/'); - if (data && success) commit('replaceEvents', data); - }, - async createEvent({commit, dispatch, state, getters}, event) { - const {data, success} = await getters.session.post('/2/events/', event); - if (data && success) commit('replaceEvents', [...state.events, data]); - }, - async deleteEvent({commit, dispatch, state, getters}, event_id) { - const {data, success} = await getters.session.delete(`/2/events/${event_id}/`); - if (success) { - await dispatch('loadEvents') - commit('replaceEvents', [...state.events.filter(e => e.id !== event_id)]) - } - }, - async updateEvent({commit, dispatch, state}, {id, partial_event}) { - const {data, success} = await http.patch(`/2/events/${id}/`, partial_event, state.user.token); - if (success) { - commit('replaceEvents', [...state.events.filter(e => e.id !== id), data]) - } - }, - async fetchTicketStates({commit, state, getters}) { - if (!state.user.token) return; - if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return; - const {data, success} = await getters.session.get('/2/tickets/states/'); - if (data && success) commit('replaceTicketStates', data); - }, - async changeEvent({dispatch, getters, commit}, eventName) { - await router.push({path: `/${eventName.slug}/${getters.getActiveView}/`}); - }, - async changeView({getters}, link) { - await router.push({path: `/${getters.getEventSlug}/${link.path}/`}); - }, - async showBoxContent({getters}, box) { - await router.push({path: `/${getters.getEventSlug}/items/`, query: {box}}); - }, - async loadEventItems({commit, getters, state}) { - if (!state.user.token) return; - const load = async (slug) => { - try { - const {data, success} = await getters.session.get(`/2/${slug}/items/`); - if (data && success) { - commit('setItems', {slug, items: data}); - } - } catch (e) { - console.error("Error loading items"); - } - } - const slug = getters.getEventSlug; - if (slug === 'all') { - await Promise.all(state.events.map(e => load(e.slug))); - } else { - await load(slug); - } - }, - async searchEventItems({commit, getters, state}, query) { - const encoded_query = base64.encode(utf8.encode(query)); - const load = async (slug) => { - if (Object.keys(state.loadedItemSearchResults).includes(slug + '/' + encoded_query)) return; - const { - data, success - } = await getters.session.get(`/2/${slug}/items/${encoded_query}/`); - if (data && success) { - commit('setItemSearchResults', {slug, query: encoded_query, items: data}); - } - } - const slug = getters.getEventSlug; - if (slug === 'all') { - await Promise.all(state.events.map(e => load(e.slug))); - } else { - await load(slug); - } - }, - async loadBoxes({commit, state, getters}) { - if (!state.user.token) return; - if (state.fetchedData.boxes > Date.now() - 1000 * 60 * 60 * 24) return; - const {data, success} = await getters.session.get('/2/boxes/'); - if (data && success) commit('replaceBoxes', data); - }, - async createBox({commit, dispatch, state, getters}, box) { - const {data, success} = await getters.session.post('/2/boxes/', box); - commit('replaceBoxes', data); - dispatch('loadBoxes').then(() => { - commit('closeAddBoxModal'); - }); - }, - async deleteBox({commit, dispatch, state, getters}, box_id) { - await getters.session.delete(`/2/boxes/${box_id}/`); - dispatch('loadBoxes'); - }, - async updateItem({commit, getters, state}, item) { - const { - data, success - } = await getters.session.put(`/2/${getters.getEventSlug}/item/${item.id}/`, item); - commit('updateItem', data); - }, - async markItemReturned({commit, getters, state}, item) { - await getters.session.patch(`/2/${getters.getEventSlug}/item/${item.id}/`, {returned: true}, - state.user.token); - commit('removeItem', item); - }, - async deleteItem({commit, getters, state}, item) { - await getters.session.delete(`/2/${getters.getEventSlug}/item/${item.id}/`, item); - commit('removeItem', item); - }, - async postItem({commit, getters, state}, item) { - commit('updateLastUsed', {box: item.box, cid: item.cid}); - const {data, success} = await getters.session.post(`/2/${getters.getEventSlug}/item/`, item); - commit('appendItem', data); - }, - async loadTickets({commit, state, getters}) { - if (!state.user.token) return; - //if (state.fetchedData.tickets > Date.now() - 1000 * 60 * 60 * 24) return; - const {data, success} = await getters.session.get('/2/tickets/'); - if (data && success) commit('replaceTickets', data); - }, - async searchEventTickets({commit, getters, state}, query) { - const encoded_query = base64.encode(utf8.encode(query)); - const load = async (slug) => { - if (Object.keys(state.loadedTicketSearchResults).includes(slug + '/' + encoded_query)) return; - const { - data, success - } = await getters.session.get(`/2/${slug}/tickets/${encoded_query}/`); - if (data && success) commit('setTicketSearchResults', {slug, query: encoded_query, items: data}); - } - const slug = getters.getEventSlug; - if (slug === 'all') { - await Promise.all(state.events.map(e => load(e.slug))); - } else { - await load(slug); - } - }, - async sendMail({commit, dispatch, state, getters}, {id, message}) { - const {data, success} = await getters.session.post(`/2/tickets/${id}/reply/`, {message}, - state.user.token); - if (data && success) { - state.fetchedData.tickets = 0; - await dispatch('loadTickets'); - } - }, - async postManualTicket({commit, dispatch, state, getters}, {sender, message, title,}) { - const slug = getters.getEventSlug; - const {data, success} = await getters.session.post(`/2/${slug !== 'all' ? slug : 'none'}/tickets/manual/`, { - name: title, sender, body: message, recipient: 'mail@c3lf.de' - }); - await dispatch('loadTickets'); - }, - async postComment({commit, dispatch, state, getters}, {id, message}) { - const {data, success} = await getters.session.post(`/2/tickets/${id}/comment/`, {comment: message}); - if (data && success) { - state.fetchedData.tickets = 0; - await dispatch('loadTickets'); - } - }, - async postItemComment({commit, dispatch, state, getters}, {id, message}) { - const { - data, - success - } = await getters.session.post(`/2/${getters.getEventSlug}/item/${id}/comment/`, {comment: message}); - if (data && success) { - state.fetchedData.items = 0; - await dispatch('loadEventItems'); - } - }, - async loadUsers({commit, state, getters}) { - if (!state.user.token) return; - if (state.fetchedData.users > Date.now() - 1000 * 60 * 60 * 24) return; - const {data, success} = await getters.session.get('/2/users/'); - if (data && success) commit('replaceUsers', data); - }, - async loadGroups({commit, state, getters}) { - if (!state.user.token) return; - if (state.fetchedData.groups > Date.now() - 1000 * 60 * 60 * 24) return; - const {data, success} = await getters.session.get('/2/groups/'); - if (data && success) commit('replaceGroups', data); - }, - async updateTicket({commit, state, getters}, ticket) { - const {data, success} = await getters.session.put(`/2/tickets/${ticket.id}/`, ticket); - commit('updateTicket', data); - }, - async updateTicketPartial({commit, state, getters}, {id, ...ticket}) { - const {data, success} = await getters.session.patch(`/2/tickets/${id}/`, ticket); - commit('updateTicket', data); - }, - async fetchShippingVouchers({commit, state, getters}) { - if (!state.user.token) return; - if (state.fetchedData.shippingVouchers > Date.now() - 1000 * 60 * 60 * 24) return; - const {data, success} = await getters.session.get('/2/shipping_vouchers/'); - if (data && success) { - commit('setShippingVouchers', data); - } - }, - async createShippingVoucher({dispatch, state, getters}, code) { - const {data, success} = await getters.session.post('/2/shipping_vouchers/', code); - if (data && success) { - state.fetchedData.shippingVouchers = 0; - dispatch('fetchShippingVouchers'); - } - }, - async claimShippingVoucher({dispatch, state, getters}, {ticket, shipping_voucher_type}) { - const id = state.shippingVouchers.filter(voucher => voucher.type === shipping_voucher_type && voucher.issue_thread === null)[0].id; - const {data, success} = await getters.session.patch(`/2/shipping_vouchers/${id}/`, {issue_thread: ticket}); - if (data && success) { - state.fetchedData.shippingVouchers = 0; - state.fetchedData.tickets = 0; - await Promise.all([dispatch('loadTickets'), dispatch('fetchShippingVouchers')]); - } - }, - async linkTicketItem({dispatch, state, getters}, {ticket_id, item_id}) { - const {data, success} = await getters.session.post(`/2/matches/`, {issue_thread: ticket_id, item: item_id}); - if (data && success) { - state.fetchedData.tickets = 0; - state.fetchedData.items = 0; - await Promise.all([dispatch('loadTickets'), dispatch('loadEventItems')]); - } - } - }, - plugins: [persistentStatePlugin({ // TODO change remember to some kind of enable field - prefix: "lf_", - debug: false, - isLoadedKey: "persistent_loaded", - validate: (state) => { - if (state.user && state.user.expiry && state.user.token) { - const as_date = new Date(state.user.expiry); - if (as_date < new Date()) { - state.user.token = null; - state.user.expiry = null; - } - } - }, - state: ["remember", "user", "events", "lastUsed",] - }), sharedStatePlugin({ - debug: false, - isLoadedKey: "shared_loaded", - clearingMutation: "logout", - afterInit: "afterSharedInit", - state: ["test", "state_options", "fetchedData", "loadedItems", "users", "groups", "loadedBoxes", "loadedTickets", "shippingVouchers",], - watch: ["test", "state_options", "fetchedData", "loadedItems", "users", "groups", "loadedBoxes", "loadedTickets", "shippingVouchers",], - mutations: [//"replaceTickets", - ], - }),], -}); - -store.watch((state) => state.user, (user) => { - if (store.getters.isLoggedIn) { - if (router.currentRoute.value.name === 'login' && router.currentRoute.value.query.redirect) { - router.push(router.currentRoute.value.query.redirect); - } else if (router.currentRoute.value.name === 'login') { - router.push('/'); - } - } else { - if (router.currentRoute.value.name !== 'login') { - router.push({ - name: 'login', - query: {redirect: router.currentRoute.value.fullPath}, - }); - } - } -}); - -export default store; diff --git a/web/src/utils.js b/web/src/utils.js deleted file mode 100644 index d753623..0000000 --- a/web/src/utils.js +++ /dev/null @@ -1,138 +0,0 @@ -import store from '@/store' - -function ticketStateColorLookup(ticket) { - if (ticket.startsWith('closed_')) { - return 'secondary'; - } - if (ticket.startsWith('pending_')) { - return 'warning'; - } - if (ticket.startsWith('waiting_')) { - return 'primary'; - } - return 'danger'; -} - -function ticketStateIconLookup(ticket) { - if (ticket.startsWith('closed_')) { - return 'check'; - } - if (ticket.startsWith('pending_')) { - return 'exclamation'; - } - if (ticket.startsWith('waiting_')) { - return 'hourglass'; - } - return 'exclamation'; -} - -const http = { - get: async (url, token) => { - if (!token) { - return null; - } - const response = await fetch('/api' + url, { - method: 'GET', - headers: { - "Content-Type": "application/json", - "Authorization": `Token ${token}`, - }, - }); - if (response.status === 401) - throw {http_status: response.status}; - const success = response.status === 200 || response.status === 201; - return {data: await response.json() || {}, success}; - }, - post: async (url, data, token) => { - if (!token) { - return null; - } - const response = await fetch('/api' + url, { - method: 'POST', - headers: { - "Content-Type": "application/json", - "Authorization": `Token ${token}`, - }, - body: JSON.stringify(data), - }); - if (response.status === 401) - throw {http_status: response.status}; - const success = response.status === 200 || response.status === 201; - return {data: await response.json() || {}, success}; - }, - put: async (url, data, token) => { - if (!token) { - return null; - } - const response = await fetch('/api' + url, { - method: 'PUT', - headers: { - "Content-Type": "application/json", - "Authorization": `Token ${token}`, - }, - body: JSON.stringify(data), - }); - if (response.status === 401) - throw {http_status: response.status}; - const success = response.status === 200 || response.status === 201; - return {data: await response.json() || {}, success}; - }, - patch: async (url, data, token) => { - if (!token) { - return null; - } - const response = await fetch('/api' + url, { - method: 'PATCH', - headers: { - "Content-Type": "application/json", - "Authorization": `Token ${token}`, - }, - body: JSON.stringify(data), - }); - if (response.status === 401) - throw {http_status: response.status}; - const success = response.status === 200 || response.status === 201; - return {data: await response.json() || {}, success}; - }, - delete: async (url, token) => { - if (!token) { - return null; - } - const response = await fetch('/api' + url, { - method: 'DELETE', - headers: { - "Content-Type": "application/json", - "Authorization": `Token ${token}`, - }, - }); - if (response.status === 401) - throw {http_status: response.status}; - const success = response.status === 204; - return {data: await response.text() || {}, success}; - } -} - -const http_session = token => ({ - get: async (url) => await http.get(url, token).catch((e) => { - if (e.http_status === 401) store.commit('logout'); - return {data: {}, success: false}; - }), - post: async (url, data) => await http.post(url, data, token).catch((e) => { - if (e.http_status === 401) store.commit('logout'); - return {data: {}, success: false}; - }), - put: async (url, data) => await http.put(url, data, token).catch((e) => { - if (e.http_status === 401) store.commit('logout'); - return {data: {}, success: false}; - }), - patch: async (url, data) => await http.patch(url, data, token).catch((e) => { - if (e.http_status === 401) store.commit('logout'); - return {data: {}, success: false}; - }), - delete: async (url) => await http.delete(url, token).catch((e) => { - if (e.http_status === 401) store.commit('logout'); - return {data: {}, success: false}; - }), -}); - -export {ticketStateColorLookup, ticketStateIconLookup, http, http_session}; \ No newline at end of file diff --git a/web/src/views/Boxes.vue b/web/src/views/Boxes.vue deleted file mode 100644 index 5de5b26..0000000 --- a/web/src/views/Boxes.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/Empty.vue b/web/src/views/Empty.vue deleted file mode 100644 index ef221da..0000000 --- a/web/src/views/Empty.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - - - diff --git a/web/src/views/Error.vue b/web/src/views/Error.vue deleted file mode 100644 index 73e63e5..0000000 --- a/web/src/views/Error.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/web/src/views/Files.vue b/web/src/views/Files.vue deleted file mode 100644 index aac8c7e..0000000 --- a/web/src/views/Files.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/HowTo.vue b/web/src/views/HowTo.vue deleted file mode 100644 index 8e4526d..0000000 --- a/web/src/views/HowTo.vue +++ /dev/null @@ -1,141 +0,0 @@ - - - - - diff --git a/web/src/views/Item.vue b/web/src/views/Item.vue deleted file mode 100644 index 7b9f484..0000000 --- a/web/src/views/Item.vue +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - diff --git a/web/src/views/ItemSearch.vue b/web/src/views/ItemSearch.vue deleted file mode 100644 index 8e9dcc4..0000000 --- a/web/src/views/ItemSearch.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/Items.vue b/web/src/views/Items.vue deleted file mode 100644 index 72b4079..0000000 --- a/web/src/views/Items.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue deleted file mode 100644 index d002cc6..0000000 --- a/web/src/views/Login.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/Register.vue b/web/src/views/Register.vue deleted file mode 100644 index 15412f7..0000000 --- a/web/src/views/Register.vue +++ /dev/null @@ -1,169 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/Ticket.vue b/web/src/views/Ticket.vue deleted file mode 100644 index 6a195f6..0000000 --- a/web/src/views/Ticket.vue +++ /dev/null @@ -1,223 +0,0 @@ - - - - - diff --git a/web/src/views/TicketSearch.vue b/web/src/views/TicketSearch.vue deleted file mode 100644 index f5a4a7d..0000000 --- a/web/src/views/TicketSearch.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/Tickets.vue b/web/src/views/Tickets.vue deleted file mode 100644 index 60d0d1d..0000000 --- a/web/src/views/Tickets.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/admin/AccessControl.vue b/web/src/views/admin/AccessControl.vue deleted file mode 100644 index 339e35a..0000000 --- a/web/src/views/admin/AccessControl.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/admin/Admin.vue b/web/src/views/admin/Admin.vue deleted file mode 100644 index 5a71223..0000000 --- a/web/src/views/admin/Admin.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/admin/Boxes.vue b/web/src/views/admin/Boxes.vue deleted file mode 100644 index 3355b0c..0000000 --- a/web/src/views/admin/Boxes.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/admin/Dashboard.vue b/web/src/views/admin/Dashboard.vue deleted file mode 100644 index 6272d71..0000000 --- a/web/src/views/admin/Dashboard.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/admin/Events.vue b/web/src/views/admin/Events.vue deleted file mode 100644 index e156d56..0000000 --- a/web/src/views/admin/Events.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/admin/Shipping.vue b/web/src/views/admin/Shipping.vue deleted file mode 100644 index 4ba1ed3..0000000 --- a/web/src/views/admin/Shipping.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/vue.config.js b/web/vue.config.js deleted file mode 100644 index 6a50fe3..0000000 --- a/web/vue.config.js +++ /dev/null @@ -1,31 +0,0 @@ -// vue.config.js - -module.exports = { - devServer: { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "*", - "Access-Control-Allow-Methods": "*" - }, - proxy: { - '^/media/2': { - target: 'https://staging.c3lf.de/', - changeOrigin: true - }, - '^/api/2': { - target: 'https://staging.c3lf.de/', - changeOrigin: true, - }, - '^/api/1': { - target: 'https://staging.c3lf.de/', - changeOrigin: true, - }, - '^/ws/2': { - target: 'http://127.0.0.1:8082/', - //changeOrigin: true, - ws: true, - logLevel: 'debug', - }, - } - } -} \ No newline at end of file