├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── .prettierignore ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── app.vue ├── assets ├── core.scss └── tailwindcss.css ├── build └── launch.sh ├── changelog.md ├── components ├── AccountSidebar.vue ├── AddLibraryButton.vue ├── Auth │ ├── OpenID.vue │ └── Simple.vue ├── CarouselPagination.vue ├── CreateCollectionModal.vue ├── DeleteCollectionModal.vue ├── DeleteNewsModal.vue ├── DropLogo.vue ├── DropWordmark.vue ├── GameCarousel.vue ├── GamePanel.vue ├── GameSearchResultWidget.vue ├── Icons │ ├── DiscordLogo.vue │ ├── GithubLogo.vue │ ├── LinuxLogo.vue │ ├── MacLogo.vue │ ├── SSOLogo.vue │ ├── SimpleAuthenticationLogo.vue │ └── WindowsLogo.vue ├── LibraryDirectory.vue ├── NewsArticleCreateButton.vue ├── NewsDirectory.vue ├── NotificationItem.vue ├── PanelWidget.vue ├── PlatformSelector.vue ├── SkeletonCard.vue ├── SourceOptions │ └── Filesystem.vue ├── UploadFileDialog.vue ├── UserFooter.vue ├── UserHeader.vue └── UserHeader │ ├── NotificationWidgetPanel.vue │ ├── UserWidget.vue │ └── Widget.vue ├── composables ├── collection.ts ├── current-page-engine.ts ├── icons.ts ├── news.ts ├── notifications.ts ├── objects.ts ├── request.ts ├── task.ts ├── types.ts ├── user.ts └── ws.ts ├── deploy-template └── compose.yml ├── dev-tools └── compose.yml ├── error.vue ├── eslint.config.mjs ├── layouts ├── admin.vue └── default.vue ├── middleware └── require-user.global.ts ├── nuxt.config.ts ├── package.json ├── pages ├── account.vue ├── account │ ├── devices.vue │ ├── index.vue │ ├── notifications.vue │ ├── security.vue │ └── settings.vue ├── admin │ ├── index.vue │ ├── library │ │ ├── [id] │ │ │ ├── import.vue │ │ │ └── index.vue │ │ ├── import.vue │ │ ├── index.vue │ │ └── sources │ │ │ └── index.vue │ ├── metadata │ │ ├── games │ │ │ ├── [id] │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ └── index.vue │ ├── task │ │ └── [id] │ │ │ └── index.vue │ └── users │ │ ├── auth │ │ ├── index.vue │ │ └── simple │ │ │ └── index.vue │ │ └── index.vue ├── auth │ ├── register.vue │ └── signin.vue ├── client │ └── [id] │ │ └── callback.vue ├── index.vue ├── library.vue ├── library │ ├── collection │ │ └── [id] │ │ │ └── index.vue │ ├── game │ │ └── [id] │ │ │ └── index.vue │ └── index.vue ├── news.vue ├── news │ ├── [id] │ │ └── index.vue │ └── index.vue └── store │ ├── [id] │ └── index.vue │ └── index.vue ├── plugins └── vuedraggable.ts ├── prisma ├── migrations │ ├── 20240928081254_create_user_and_auth_mechanisms │ │ └── migration.sql │ ├── 20240928085121_move_to_json_for_credentials │ │ └── migration.sql │ ├── 20240929000950_add_game_data │ │ └── migration.sql │ ├── 20240929010842_updates_to_metadata_schema │ │ └── migration.sql │ ├── 20241004020835_unique_constraints │ │ └── migration.sql │ ├── 20241004025235_add_dev_pub_websites │ │ └── migration.sql │ ├── 20241007043002_add_user_admin │ │ └── migration.sql │ ├── 20241007065541_add_client │ │ └── migration.sql │ ├── 20241008062519_remove_shared_token_and_add_last_connected │ │ └── migration.sql │ ├── 20241009032354_add_account_details │ │ └── migration.sql │ ├── 20241010062956_add_constraints │ │ └── migration.sql │ ├── 20241010095344_various_fixes │ │ └── migration.sql │ ├── 20241010104439_added_original_query_field │ │ └── migration.sql │ ├── 20241010104722_fix_unique_constraints │ │ └── migration.sql │ ├── 20241011035227_add_droplet_manifest_to_game_versions │ │ └── migration.sql │ ├── 20241011093950_update_game_images_system │ │ └── migration.sql │ ├── 20241011101243_revert_banner_system │ │ └── migration.sql │ ├── 20241011103116_add_cover_image │ │ └── migration.sql │ ├── 20241014052934_add_delta_and_order │ │ └── migration.sql │ ├── 20241014053941_remove_version_order │ │ └── migration.sql │ ├── 20241025091103_add_invitations │ │ └── migration.sql │ ├── 20241102000813_create_application_configuration │ │ └── migration.sql │ ├── 20241105221904_different_client_capabilities │ │ └── migration.sql │ ├── 20241105222110_trackable_names_for_capabilities │ │ └── migration.sql │ ├── 20241105225732_peer_api_configuration │ │ └── migration.sql │ ├── 20241105230021_move_to_endpoint_configuration │ │ └── migration.sql │ ├── 20241107080421_add_expiry_for_invitations │ │ └── migration.sql │ ├── 20241116053120_add_notifications │ │ └── migration.sql │ ├── 20241116054212_add_created_time_stamp_to_notifications │ │ └── migration.sql │ ├── 20241122215933_add_created_timestamps_for_games │ │ └── migration.sql │ ├── 20241124042825_add_released_date_for_the_game │ │ └── migration.sql │ ├── 20241223022005_add_umu_id_to_game_version │ │ └── migration.sql │ ├── 20241223100329_add_referential_deletion_for_game_versions │ │ └── migration.sql │ ├── 20241223100418_update_to_prisma_6 │ │ └── migration.sql │ ├── 20241226065709_rename_custom_to_manual │ │ └── migration.sql │ ├── 20241226230207_add_image_carousel │ │ └── migration.sql │ ├── 20241227033610_move_image_carousel_to_image_ids │ │ └── migration.sql │ ├── 20241230053403_add_args_and_only_setup │ │ └── migration.sql │ ├── 20250103202348_add_collections │ │ └── migration.sql │ ├── 20250109005948_use_collection_entry_to_ensure_unique_games │ │ └── migration.sql │ ├── 20250128060446_casacad_e_delete_for_collection_entries │ │ └── migration.sql │ ├── 20250128102738_add_news │ │ └── migration.sql │ ├── 20250204010021_add_tokens │ │ └── migration.sql │ ├── 20250204020918_add_collection_entry_casacade_delete │ │ └── migration.sql │ ├── 20250208004345_add_api_token_name │ │ └── migration.sql │ ├── 20250208005625_add_id_to_token │ │ └── migration.sql │ ├── 20250211230021_ensure_non_null_launch_and_setup_commands │ │ └── migration.sql │ ├── 20250309234300_news_articles │ │ └── migration.sql │ ├── 20250309234801_make_tags_unique │ │ └── migration.sql │ ├── 20250309234846_make_tokens_unique │ │ └── migration.sql │ ├── 20250311073601_add_macos_as_a_platform │ │ └── migration.sql │ ├── 20250312230736_add_metadata_providers_to_appconfig │ │ └── migration.sql │ ├── 20250313042306_add_igdb_pcgamingwiki_metadata │ │ └── migration.sql │ ├── 20250313053250_add_enable_fields_to_auth_and_users │ │ └── migration.sql │ ├── 20250314153636_store_ca_and_session_in_db │ │ └── migration.sql │ ├── 20250324014736_add_auth_mek_version │ │ └── migration.sql │ ├── 20250401082200_add_save_slots │ │ └── migration.sql │ ├── 20250401082605_add_save_slot_limits_to_application_settings │ │ └── migration.sql │ ├── 20250401083942_rename_save_to_cloud_saves │ │ └── migration.sql │ ├── 20250401084907_add_history_limit │ │ └── migration.sql │ ├── 20250401085406_add_default_to_playtime │ │ └── migration.sql │ ├── 20250401091937_add_history_and_hashes │ │ └── migration.sql │ ├── 20250403233442_apply_store_changes │ │ └── migration.sql │ ├── 20250405062945_make_last_accessed_optional_on_save_slots │ │ └── migration.sql │ ├── 20250407090729_add_client_token_mode │ │ └── migration.sql │ ├── 20250407091012_add_client_token_field_to_apitoken │ │ └── migration.sql │ ├── 20250414002714_add_object_hash │ │ └── migration.sql │ ├── 20250507120031_add_openid_authmek │ │ └── migration.sql │ ├── 20250507223112_remove_authentication_option_from_applicationsettings │ │ └── migration.sql │ ├── 20250508153613_add_screenshots │ │ └── migration.sql │ ├── 20250508224553_cleanup_old_objects │ │ └── migration.sql │ ├── 20250509003340_init_unified_company_metadata │ │ └── migration.sql │ ├── 20250510013650_remove_devlopers_and_publishers │ │ └── migration.sql │ ├── 20250511154134_add_tags_to_games │ │ └── migration.sql │ ├── 20250514193830_allow_notification_nonce_reuse_per_user │ │ └── migration.sql │ ├── 20250515021331_add_game_ratings │ │ └── migration.sql │ ├── 20250515043254_add_acls_to_notifications │ │ └── migration.sql │ ├── 20250601022736_add_database_library │ │ └── migration.sql │ ├── 20250601032211_add_library_relation_to_game │ │ └── migration.sql │ ├── 20250601032938_add_unique_constraint │ │ └── migration.sql │ └── migration_lock.toml ├── models │ ├── app.prisma │ ├── auth.prisma │ ├── client.prisma │ ├── collection.prisma │ ├── content.prisma │ ├── news.prisma │ └── user.prisma └── schema.prisma ├── public ├── favicon.ico ├── fonts │ ├── helvetica │ │ ├── Helvetica-Bold.woff │ │ ├── Helvetica-BoldOblique.woff │ │ ├── Helvetica-Oblique.woff │ │ ├── Helvetica.woff │ │ ├── helvetica-compressed-5871d14b6903a.woff │ │ ├── helvetica-light-587ebe5a59211.woff │ │ ├── helvetica-light-587ebe5a59211.woff2 │ │ └── helvetica-rounded-bold-5871d05ead8de.woff │ ├── inter │ │ ├── InterVariable-Italic.ttf │ │ └── InterVariable.ttf │ └── motiva │ │ ├── MotivaSansBlack.woff.ttf │ │ ├── MotivaSansBold.woff.ttf │ │ ├── MotivaSansExtraBold.ttf │ │ ├── MotivaSansLight.woff.ttf │ │ ├── MotivaSansMedium.woff.ttf │ │ ├── MotivaSansRegular.woff.ttf │ │ └── MotivaSansThin.ttf ├── robots.txt └── wallpapers │ ├── error-wallpaper.jpg │ └── signin.jpg ├── server ├── api │ └── v1 │ │ ├── admin │ │ ├── auth │ │ │ ├── index.get.ts │ │ │ └── invitation │ │ │ │ ├── index.delete.ts │ │ │ │ ├── index.get.ts │ │ │ │ └── index.post.ts │ │ ├── game │ │ │ ├── image │ │ │ │ ├── index.delete.ts │ │ │ │ └── index.post.ts │ │ │ ├── index.delete.ts │ │ │ ├── index.get.ts │ │ │ ├── index.patch.ts │ │ │ ├── metadata.post.ts │ │ │ └── version │ │ │ │ ├── index.delete.ts │ │ │ │ └── index.patch.ts │ │ ├── import │ │ │ ├── game │ │ │ │ ├── index.get.ts │ │ │ │ ├── index.post.ts │ │ │ │ └── search.get.ts │ │ │ └── version │ │ │ │ ├── index.get.ts │ │ │ │ ├── index.post.ts │ │ │ │ └── preload.get.ts │ │ ├── library │ │ │ ├── index.get.ts │ │ │ └── sources │ │ │ │ ├── index.delete.ts │ │ │ │ ├── index.get.ts │ │ │ │ ├── index.patch.ts │ │ │ │ └── index.post.ts │ │ ├── news │ │ │ ├── [id] │ │ │ │ ├── index.delete.ts │ │ │ │ └── index.get.ts │ │ │ ├── index.get.ts │ │ │ └── index.post.ts │ │ └── users │ │ │ ├── [id] │ │ │ └── index.get.ts │ │ │ └── index.get.ts │ │ ├── auth │ │ ├── index.get.ts │ │ ├── signin │ │ │ └── simple.post.ts │ │ └── signup │ │ │ ├── simple.get.ts │ │ │ └── simple.post.ts │ │ ├── client │ │ ├── auth │ │ │ ├── callback │ │ │ │ ├── index.get.ts │ │ │ │ └── index.post.ts │ │ │ ├── handshake.post.ts │ │ │ ├── initiate.post.ts │ │ │ └── session.post.ts │ │ ├── capability │ │ │ └── index.post.ts │ │ ├── chunk.get.ts │ │ ├── collection │ │ │ ├── [id] │ │ │ │ ├── entry.delete.ts │ │ │ │ ├── entry.post.ts │ │ │ │ ├── index.delete.ts │ │ │ │ └── index.get.ts │ │ │ ├── default │ │ │ │ ├── entry.delete.ts │ │ │ │ ├── entry.post.ts │ │ │ │ └── index.get.ts │ │ │ ├── index.get.ts │ │ │ └── index.post.ts │ │ ├── game │ │ │ ├── [id] │ │ │ │ └── index.get.ts │ │ │ ├── manifest.get.ts │ │ │ ├── version.get.ts │ │ │ └── versions.get.ts │ │ ├── news │ │ │ ├── [id] │ │ │ │ └── index.get.ts │ │ │ └── index.get.ts │ │ ├── object │ │ │ └── [id] │ │ │ │ └── index.get.ts │ │ ├── saves │ │ │ ├── [gameid] │ │ │ │ ├── [slotindex] │ │ │ │ │ ├── index.delete.ts │ │ │ │ │ ├── index.get.ts │ │ │ │ │ └── push.post.ts │ │ │ │ ├── index.get.ts │ │ │ │ └── index.post.ts │ │ │ ├── index.get.ts │ │ │ └── settings.get.ts │ │ └── user │ │ │ ├── index.get.ts │ │ │ ├── library.get.ts │ │ │ └── webtoken.post.ts │ │ ├── collection │ │ ├── [id] │ │ │ ├── entry.delete.ts │ │ │ ├── entry.post.ts │ │ │ ├── index.delete.ts │ │ │ └── index.get.ts │ │ ├── default │ │ │ ├── entry.delete.ts │ │ │ ├── entry.post.ts │ │ │ └── index.get.ts │ │ ├── index.get.ts │ │ └── index.post.ts │ │ ├── games │ │ └── [id] │ │ │ └── index.get.ts │ │ ├── index.get.ts │ │ ├── news │ │ ├── [id] │ │ │ └── index.get.ts │ │ └── index.get.ts │ │ ├── notifications │ │ ├── [id] │ │ │ ├── index.delete.ts │ │ │ ├── index.get.ts │ │ │ └── read.post.ts │ │ ├── index.get.ts │ │ ├── readall.post.ts │ │ └── ws.get.ts │ │ ├── object │ │ └── [id] │ │ │ ├── index.delete.ts │ │ │ ├── index.get.ts │ │ │ ├── index.head.ts │ │ │ └── index.post.ts │ │ ├── screenshots │ │ ├── [id] │ │ │ ├── index.delete.ts │ │ │ └── index.get.ts │ │ ├── game │ │ │ └── [id] │ │ │ │ ├── index.get.ts │ │ │ │ └── index.post.ts │ │ └── index.get.ts │ │ ├── store │ │ ├── recent.get.ts │ │ ├── released.get.ts │ │ └── updated.get.ts │ │ ├── task │ │ └── index.get.ts │ │ └── user │ │ ├── client │ │ ├── [id] │ │ │ └── index.delete.ts │ │ └── index.get.ts │ │ ├── index.get.ts │ │ └── token │ │ ├── [id] │ │ └── index.delete.ts │ │ ├── acls.get.ts │ │ ├── index.get.ts │ │ └── index.post.ts ├── arktype.ts ├── h3.d.ts ├── internal │ ├── acls │ │ ├── descriptions.ts │ │ └── index.ts │ ├── cache │ │ ├── cacheHandler.ts │ │ └── index.ts │ ├── clients │ │ ├── README.md │ │ ├── ca-store.ts │ │ ├── ca.ts │ │ ├── capabilities.ts │ │ ├── event-handler.ts │ │ └── handler.ts │ ├── config │ │ ├── application-configuration.ts │ │ └── sys-conf.ts │ ├── consts.ts │ ├── db │ │ └── database.ts │ ├── downloads │ │ ├── coordinator.ts │ │ └── manifest.ts │ ├── library │ │ ├── README.md │ │ ├── filesystem.ts │ │ ├── index.ts │ │ └── provider.ts │ ├── metadata │ │ ├── giantbomb.ts │ │ ├── igdb.ts │ │ ├── index.ts │ │ ├── manual.ts │ │ ├── pcgamingwiki.ts │ │ └── types.d.ts │ ├── news │ │ └── index.ts │ ├── notifications │ │ └── index.ts │ ├── objects │ │ ├── fsBackend.ts │ │ ├── index.ts │ │ ├── objectHandler.ts │ │ └── transactional.ts │ ├── oidc │ │ └── index.ts │ ├── saves │ │ └── index.ts │ ├── screenshots │ │ └── index.ts │ ├── security │ │ └── simple.ts │ ├── session │ │ ├── cache.ts │ │ ├── db.ts │ │ ├── index.ts │ │ ├── memory.ts │ │ └── types.d.ts │ ├── tasks │ │ └── index.ts │ ├── userlibrary │ │ └── index.ts │ └── utils │ │ ├── handlefileupload.ts │ │ ├── parseplatform.ts │ │ ├── prioritylist.ts │ │ ├── recursivedirs.ts │ │ └── types.d.ts ├── plugins │ ├── 01.system-init.ts │ ├── 02.setup-admin.ts │ ├── 03.metadata-init.ts │ ├── 04.auth-init.ts │ ├── 05.library-init.ts │ ├── ca.ts │ ├── redirect.ts │ └── tasks.ts ├── routes │ └── auth │ │ ├── callback │ │ └── oidc.get.ts │ │ ├── oidc.get.ts │ │ └── signout.get.ts ├── tasks │ ├── check │ │ └── update.ts │ └── cleanup │ │ ├── invitations.ts │ │ ├── objects.ts │ │ └── sessions.ts └── tsconfig.json ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | .yarn 12 | 13 | # Logs 14 | logs 15 | *.log 16 | 17 | # Misc 18 | .DS_Store 19 | .fleet 20 | .idea 21 | 22 | # Local env files 23 | .env 24 | .env.* 25 | !.env.example 26 | 27 | # deploy template 28 | deploy-template/ 29 | 30 | # generated prisma client 31 | /prisma/client 32 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgres://drop:drop@127.0.0.1:5432/drop" 2 | 3 | CLIENT_CERTIFICATES="./.data/ca" 4 | 5 | FS_BACKEND_PATH="./.data/objects" 6 | 7 | GIANT_BOMB_API_KEY="" 8 | 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | * text=auto eol=lf 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | typecheck: 7 | name: Typecheck 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out the repo 11 | uses: actions/checkout@v4 12 | with: 13 | submodules: true 14 | 15 | - name: Setup Node.js environment 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: lts/* 19 | cache: "yarn" 20 | 21 | - name: Install dependencies 22 | run: yarn install --immutable --network-timeout 1000000 23 | 24 | - name: Typecheck 25 | run: yarn typecheck 26 | 27 | lint: 28 | name: Lint 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Check out the repo 32 | uses: actions/checkout@v4 33 | with: 34 | submodules: true 35 | 36 | - name: Setup Node.js environment 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: lts/* 40 | cache: "yarn" 41 | 42 | - name: Install dependencies 43 | run: yarn install --immutable --network-timeout 1000000 44 | 45 | - name: Lint 46 | run: yarn lint 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | .yarn 12 | 13 | # Logs 14 | logs 15 | *.log 16 | 17 | # Misc 18 | .DS_Store 19 | .fleet 20 | .idea 21 | 22 | # Local env files 23 | .env 24 | .env.* 25 | !.env.example 26 | 27 | .data 28 | 29 | 30 | # deploy template 31 | deploy-template/* 32 | 33 | !deploy-template/compose.yml 34 | 35 | # generated prisma client 36 | /prisma/client 37 | /prisma/validate -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "drop-base"] 2 | path = drop-base 3 | url = https://github.com/Drop-OSS/drop-base.git 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | drop-base/ 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "spellchecker.ignoreWordsList": ["mTLS", "Wireguard"], 3 | "sqltools.connections": [ 4 | { 5 | "previewLimit": 50, 6 | "server": "localhost", 7 | "port": 5432, 8 | "driver": "PostgreSQL", 9 | "name": "drop", 10 | "database": "drop", 11 | "username": "drop", 12 | "password": "drop" 13 | } 14 | ], 15 | // allow autocomplete for ArkType expressions like "string | num" 16 | "editor.quickSuggestions": { 17 | "strings": "on" 18 | }, 19 | // prioritize ArkType's "type" for autoimports 20 | "typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"] 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Unified deps builder 4 | FROM node:lts-alpine AS deps 5 | WORKDIR /app 6 | COPY package.json yarn.lock ./ 7 | RUN yarn install --network-timeout 1000000 --ignore-scripts 8 | 9 | # Build for app 10 | FROM node:lts-alpine AS build-system 11 | # setup workdir - has to be the same filepath as app because fuckin' Prisma 12 | WORKDIR /app 13 | 14 | ENV NODE_ENV=production 15 | ENV NUXT_TELEMETRY_DISABLED=1 16 | 17 | # add git so drop can determine its git ref at build 18 | RUN apk add --no-cache git 19 | 20 | # copy deps and rest of project files 21 | COPY --from=deps /app/node_modules ./node_modules 22 | COPY . . 23 | 24 | ARG BUILD_DROP_VERSION="v0.0.0-unknown.1" 25 | ARG BUILD_GIT_REF 26 | 27 | # build 28 | RUN yarn postinstall 29 | RUN yarn build 30 | 31 | # create run environment for Drop 32 | FROM node:lts-alpine AS run-system 33 | WORKDIR /app 34 | 35 | ENV NODE_ENV=production 36 | ENV NUXT_TELEMETRY_DISABLED=1 37 | 38 | RUN yarn add --network-timeout 1000000 --no-lockfile prisma@6.7.0 39 | 40 | COPY --from=build-system /app/package.json ./ 41 | COPY --from=build-system /app/.output ./app 42 | COPY --from=build-system /app/prisma ./prisma 43 | COPY --from=build-system /app/build ./startup 44 | 45 | ENV LIBRARY="/library" 46 | ENV DATA="/data" 47 | 48 | CMD ["sh", "/app/startup/launch.sh"] 49 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | To report a vulnerability, please DO NOT create an issue for it 4 | as this may lead to the vulnerability being exploited before it 5 | can be fixed. Instead, please email [security@deepcore.dev](mailto:security@deepcore.dev) 6 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /assets/tailwindcss.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "@tailwindcss/typography"; 3 | @plugin "@tailwindcss/forms"; 4 | @config "../tailwind.config.js"; 5 | -------------------------------------------------------------------------------- /build/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file starts up the Drop server by running migrations and then starting the executable 4 | echo "[Drop] performing migrations..." 5 | yarn prisma migrate deploy 6 | 7 | # Actually start the application 8 | node /app/app/server/index.mjs 9 | -------------------------------------------------------------------------------- /components/Auth/OpenID.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /components/CarouselPagination.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | -------------------------------------------------------------------------------- /components/DropLogo.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /components/DropWordmark.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /components/GamePanel.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 55 | 56 | 62 | -------------------------------------------------------------------------------- /components/GameSearchResultWidget.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /components/Icons/MacLogo.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /components/Icons/SSOLogo.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /components/Icons/SimpleAuthenticationLogo.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /components/Icons/WindowsLogo.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /components/PanelWidget.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/SkeletonCard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /components/SourceOptions/Filesystem.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /components/UserHeader/NotificationWidgetPanel.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /components/UserHeader/Widget.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 38 | -------------------------------------------------------------------------------- /composables/collection.ts: -------------------------------------------------------------------------------- 1 | import type { Collection, CollectionEntry, Game } from "~/prisma/client"; 2 | import type { SerializeObject } from "nitropack"; 3 | 4 | type FullCollection = Collection & { 5 | entries: Array }>; 6 | }; 7 | 8 | export const useCollections = async () => { 9 | // @ts-expect-error undefined is used to tell if value has been fetched or not 10 | const state = useState("collections", () => undefined); 11 | if (state.value === undefined) { 12 | state.value = await $dropFetch("/api/v1/collection"); 13 | } 14 | 15 | return state; 16 | }; 17 | 18 | export async function refreshCollection(id: string) { 19 | const state = useState("collections"); 20 | const collection = await $dropFetch( 21 | `/api/v1/collection/${id}`, 22 | ); 23 | const index = state.value.findIndex((e) => e.id == id); 24 | if (index == -1) { 25 | state.value.push(collection); 26 | return; 27 | } 28 | state.value[index] = collection; 29 | } 30 | 31 | export const useLibrary = async () => { 32 | // @ts-expect-error undefined is used to tell if value has been fetched or not 33 | const state = useState("library", () => undefined); 34 | if (state.value === undefined) { 35 | await refreshLibrary(); 36 | } 37 | 38 | return state; 39 | }; 40 | 41 | export async function refreshLibrary() { 42 | const state = useState("library"); 43 | state.value = await $dropFetch("/api/v1/collection/default"); 44 | } 45 | -------------------------------------------------------------------------------- /composables/current-page-engine.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized } from "vue-router"; 2 | import type { NavigationItem } from "./types"; 3 | 4 | export const useCurrentNavigationIndex = ( 5 | navigation: Array, 6 | ) => { 7 | const router = useRouter(); 8 | const route = useRoute(); 9 | 10 | const currentNavigation = ref(-1); 11 | 12 | function calculateCurrentNavIndex(to: RouteLocationNormalized) { 13 | const validOptions = navigation 14 | .map((e, i) => ({ ...e, index: i })) 15 | .filter((e) => to.fullPath.startsWith(e.prefix)); 16 | const bestOption = validOptions 17 | .sort((a, b) => b.route.length - a.route.length) 18 | .at(0); 19 | 20 | return bestOption?.index ?? -1; 21 | } 22 | 23 | currentNavigation.value = calculateCurrentNavIndex(route); 24 | 25 | router.afterEach((to) => { 26 | currentNavigation.value = calculateCurrentNavIndex(to); 27 | }); 28 | 29 | return currentNavigation; 30 | }; 31 | -------------------------------------------------------------------------------- /composables/icons.ts: -------------------------------------------------------------------------------- 1 | import { IconsLinuxLogo, IconsWindowsLogo, IconsMacLogo } from "#components"; 2 | import { PlatformClient } from "./types"; 3 | 4 | export const PLATFORM_ICONS = { 5 | [PlatformClient.Linux]: IconsLinuxLogo, 6 | [PlatformClient.Windows]: IconsWindowsLogo, 7 | [PlatformClient.macOS]: IconsMacLogo, 8 | }; 9 | -------------------------------------------------------------------------------- /composables/news.ts: -------------------------------------------------------------------------------- 1 | import type { Article } from "~/prisma/client"; 2 | import type { SerializeObject } from "nitropack"; 3 | 4 | export const useNews = () => 5 | useState< 6 | | Array< 7 | SerializeObject< 8 | Article & { 9 | tags: Array<{ id: string; name: string }>; 10 | author: { displayName: string; id: string } | null; 11 | } 12 | > 13 | > 14 | | undefined 15 | >("news", () => undefined); 16 | 17 | export const fetchNews = async (options?: { 18 | limit?: number; 19 | skip?: number; 20 | orderBy?: "asc" | "desc"; 21 | tags?: string[]; 22 | search?: string; 23 | }) => { 24 | const query = new URLSearchParams(); 25 | 26 | if (options?.limit) query.set("limit", options.limit.toString()); 27 | if (options?.skip) query.set("skip", options.skip.toString()); 28 | if (options?.orderBy) query.set("order", options.orderBy); 29 | if (options?.tags?.length) query.set("tags", options.tags.join(",")); 30 | if (options?.search) query.set("search", options.search); 31 | 32 | const news = useNews(); 33 | 34 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 35 | // @ts-ignore forget why this ignor exists 36 | const newValue = await $dropFetch(`/api/v1/news?${query.toString()}`); 37 | 38 | news.value = newValue; 39 | 40 | return newValue; 41 | }; 42 | -------------------------------------------------------------------------------- /composables/notifications.ts: -------------------------------------------------------------------------------- 1 | import type { Notification } from "~/prisma/client"; 2 | 3 | const ws = new WebSocketHandler("/api/v1/notifications/ws"); 4 | 5 | export const useNotifications = () => 6 | useState>("notifications", () => []); 7 | 8 | ws.listen((e) => { 9 | const notification = JSON.parse(e) as Notification; 10 | const notifications = useNotifications(); 11 | notifications.value.push(notification); 12 | }); 13 | -------------------------------------------------------------------------------- /composables/objects.ts: -------------------------------------------------------------------------------- 1 | export const useObject = (id: string) => `/api/v1/object/${id}`; 2 | -------------------------------------------------------------------------------- /composables/request.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ExtractedRouteMethod, 3 | NitroFetchOptions, 4 | NitroFetchRequest, 5 | TypedInternalResponse, 6 | } from "nitropack/types"; 7 | 8 | interface DropFetch< 9 | DefaultT = unknown, 10 | DefaultR extends NitroFetchRequest = NitroFetchRequest, 11 | > { 12 | < 13 | T = DefaultT, 14 | R extends NitroFetchRequest = DefaultR, 15 | O extends NitroFetchOptions = NitroFetchOptions, 16 | >( 17 | request: R, 18 | opts?: O, 19 | ): Promise< 20 | // sometimes there is an error, other times there isn't 21 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 22 | // @ts-ignore 23 | TypedInternalResponse< 24 | R, 25 | T, 26 | NitroFetchOptions extends O ? "get" : ExtractedRouteMethod 27 | > 28 | >; 29 | } 30 | 31 | export const $dropFetch: DropFetch = async (request, opts) => { 32 | if (!getCurrentInstance()?.proxy) { 33 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 34 | // @ts-ignore Excessive stack depth comparing types 35 | return await $fetch(request, opts); 36 | } 37 | const id = request.toString(); 38 | 39 | const state = useState(id); 40 | if (state.value) { 41 | // Deep copy 42 | const object = JSON.parse(JSON.stringify(state.value)); 43 | // Never use again on client 44 | state.value = undefined; 45 | return object; 46 | } 47 | 48 | const headers = useRequestHeaders(["cookie", "authorization"]); 49 | const data = await $fetch(request, { 50 | ...opts, 51 | headers: { ...opts?.headers, ...headers }, 52 | }); 53 | if (import.meta.server) state.value = data; 54 | return data; 55 | }; 56 | -------------------------------------------------------------------------------- /composables/types.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "vue"; 2 | 3 | export type NavigationItem = { 4 | prefix: string; 5 | route: string; 6 | label: string; 7 | }; 8 | 9 | export type QuickActionNav = { 10 | icon: Component; 11 | notifications?: Ref; 12 | action: () => Promise; 13 | }; 14 | 15 | export enum PlatformClient { 16 | Windows = "Windows", 17 | Linux = "Linux", 18 | macOS = "macOS", 19 | } 20 | -------------------------------------------------------------------------------- /composables/user.ts: -------------------------------------------------------------------------------- 1 | import type { User } from "~/prisma/client"; 2 | 3 | // undefined = haven't check 4 | // null = check, no user 5 | // {} = check, user 6 | 7 | export const useUser = () => useState(undefined); 8 | export const updateUser = async () => { 9 | const user = useUser(); 10 | if (user.value === null) return; 11 | 12 | user.value = await $dropFetch("/api/v1/user"); 13 | }; 14 | -------------------------------------------------------------------------------- /deploy-template/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | # using alpine image to reduce image size 4 | image: postgres:alpine 5 | ports: 6 | - 5432:5432 7 | healthcheck: 8 | test: pg_isready -d drop -U drop 9 | interval: 30s 10 | timeout: 60s 11 | retries: 5 12 | start_period: 10s 13 | volumes: 14 | - ./db:/var/lib/postgresql/data 15 | environment: 16 | - POSTGRES_PASSWORD=drop 17 | - POSTGRES_USER=drop 18 | - POSTGRES_DB=drop 19 | drop: 20 | image: ghcr.io/drop-oss/drop:latest 21 | stdin_open: true 22 | tty: true 23 | init: true 24 | depends_on: 25 | postgres: 26 | condition: service_healthy 27 | ports: 28 | - 3000:3000 29 | volumes: 30 | - ./library:/library 31 | - ./data:/data 32 | environment: 33 | - DATABASE_URL=postgres://drop:drop@postgres:5432/drop 34 | -------------------------------------------------------------------------------- /dev-tools/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:14-alpine 4 | user: "1000:1000" 5 | ports: 6 | - 5432:5432 7 | volumes: 8 | - ../.data/db:/var/lib/postgresql/data 9 | environment: 10 | - POSTGRES_PASSWORD=drop 11 | - POSTGRES_USER=drop 12 | - POSTGRES_DB=drop 13 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import withNuxt from "./.nuxt/eslint.config.mjs"; 3 | import eslintConfigPrettier from "eslint-config-prettier/flat"; 4 | 5 | export default withNuxt([eslintConfigPrettier]); 6 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | -------------------------------------------------------------------------------- /middleware/require-user.global.ts: -------------------------------------------------------------------------------- 1 | const whitelistedPrefixes = ["/auth", "/api", "/setup"]; 2 | const requireAdmin = ["/admin"]; 3 | 4 | export default defineNuxtRouteMiddleware(async (to, _from) => { 5 | if (import.meta.server) return; 6 | const error = useError(); 7 | if (error.value !== undefined) return; 8 | if (whitelistedPrefixes.findIndex((e) => to.fullPath.startsWith(e)) != -1) 9 | return; 10 | 11 | const user = useUser(); 12 | if (user === undefined) { 13 | await updateUser(); 14 | } 15 | if (!user.value) { 16 | return navigateTo({ 17 | path: "/auth/signin", 18 | query: { redirect: to.fullPath }, 19 | }); 20 | } 21 | if ( 22 | requireAdmin.findIndex((e) => to.fullPath.startsWith(e)) != -1 && 23 | !user.value.admin 24 | ) { 25 | return navigateTo({ path: "/" }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /pages/account/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pages/account/notifications.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /pages/account/security.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /pages/account/settings.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pages/admin/metadata/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 50 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /plugins/vuedraggable.ts: -------------------------------------------------------------------------------- 1 | import draggable from "vuedraggable"; 2 | 3 | export default defineNuxtPlugin((nuxtApp) => { 4 | nuxtApp.vueApp.component("draggable", draggable); 5 | }); 6 | -------------------------------------------------------------------------------- /prisma/migrations/20240928081254_create_user_and_auth_mechanisms/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "AuthMec" AS ENUM ('Simple'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "User" ( 6 | "id" TEXT NOT NULL, 7 | "username" TEXT NOT NULL, 8 | 9 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "LinkedAuthMec" ( 14 | "userId" TEXT NOT NULL, 15 | "mec" "AuthMec" NOT NULL, 16 | "credentials" TEXT[], 17 | 18 | CONSTRAINT "LinkedAuthMec_pkey" PRIMARY KEY ("userId","mec") 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "LinkedAuthMec" ADD CONSTRAINT "LinkedAuthMec_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /prisma/migrations/20240928085121_move_to_json_for_credentials/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Changed the type of `credentials` on the `LinkedAuthMec` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "LinkedAuthMec" DROP COLUMN "credentials", 9 | ADD COLUMN "credentials" JSONB NOT NULL; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20240929010842_updates_to_metadata_schema/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `mBanner` to the `Developer` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `mDescription` to the `Developer` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `mLogo` to the `Developer` table without a default value. This is not possible if the table is not empty. 7 | - Added the required column `mShortDescription` to the `Developer` table without a default value. This is not possible if the table is not empty. 8 | - Added the required column `mBanner` to the `Publisher` table without a default value. This is not possible if the table is not empty. 9 | - Added the required column `mDescription` to the `Publisher` table without a default value. This is not possible if the table is not empty. 10 | - Added the required column `mLogo` to the `Publisher` table without a default value. This is not possible if the table is not empty. 11 | - Added the required column `mShortDescription` to the `Publisher` table without a default value. This is not possible if the table is not empty. 12 | 13 | */ 14 | -- AlterTable 15 | ALTER TABLE "Developer" ADD COLUMN "mBanner" TEXT NOT NULL, 16 | ADD COLUMN "mDescription" TEXT NOT NULL, 17 | ADD COLUMN "mLogo" TEXT NOT NULL, 18 | ADD COLUMN "mShortDescription" TEXT NOT NULL; 19 | 20 | -- AlterTable 21 | ALTER TABLE "Publisher" ADD COLUMN "mBanner" TEXT NOT NULL, 22 | ADD COLUMN "mDescription" TEXT NOT NULL, 23 | ADD COLUMN "mLogo" TEXT NOT NULL, 24 | ADD COLUMN "mShortDescription" TEXT NOT NULL; 25 | -------------------------------------------------------------------------------- /prisma/migrations/20241004020835_unique_constraints/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[metadataSource,metadataId]` on the table `Developer` will be added. If there are existing duplicate values, this will fail. 5 | - A unique constraint covering the columns `[metadataSource,metadataId]` on the table `Game` will be added. If there are existing duplicate values, this will fail. 6 | - A unique constraint covering the columns `[metadataSource,metadataId]` on the table `Publisher` will be added. If there are existing duplicate values, this will fail. 7 | 8 | */ 9 | -- CreateIndex 10 | CREATE UNIQUE INDEX "Developer_metadataSource_metadataId_key" ON "Developer"("metadataSource", "metadataId"); 11 | 12 | -- CreateIndex 13 | CREATE UNIQUE INDEX "Game_metadataSource_metadataId_key" ON "Game"("metadataSource", "metadataId"); 14 | 15 | -- CreateIndex 16 | CREATE UNIQUE INDEX "Publisher_metadataSource_metadataId_key" ON "Publisher"("metadataSource", "metadataId"); 17 | -------------------------------------------------------------------------------- /prisma/migrations/20241004025235_add_dev_pub_websites/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `mWebsite` to the `Developer` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `mWebsite` to the `Publisher` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Developer" ADD COLUMN "mWebsite" TEXT NOT NULL; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Publisher" ADD COLUMN "mWebsite" TEXT NOT NULL; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20241007043002_add_user_admin/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "admin" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20241007065541_add_client/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "ClientCapabilities" AS ENUM ('DownloadAggregation'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Client" ( 6 | "sharedToken" TEXT NOT NULL, 7 | "userId" TEXT NOT NULL, 8 | "endpoint" TEXT NOT NULL, 9 | "capabilities" "ClientCapabilities"[], 10 | 11 | CONSTRAINT "Client_pkey" PRIMARY KEY ("sharedToken") 12 | ); 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "Client" ADD CONSTRAINT "Client_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20241008062519_remove_shared_token_and_add_last_connected/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `Client` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - You are about to drop the column `sharedToken` on the `Client` table. All the data in the column will be lost. 6 | - The required column `id` was added to the `Client` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. 7 | - Added the required column `lastConnected` to the `Client` table without a default value. This is not possible if the table is not empty. 8 | - Added the required column `name` to the `Client` table without a default value. This is not possible if the table is not empty. 9 | - Added the required column `platform` to the `Client` table without a default value. This is not possible if the table is not empty. 10 | 11 | */ 12 | -- AlterTable 13 | ALTER TABLE "Client" DROP CONSTRAINT "Client_pkey", 14 | DROP COLUMN "sharedToken", 15 | ADD COLUMN "id" TEXT NOT NULL, 16 | ADD COLUMN "lastConnected" TIMESTAMP(3) NOT NULL, 17 | ADD COLUMN "name" TEXT NOT NULL, 18 | ADD COLUMN "platform" TEXT NOT NULL, 19 | ADD CONSTRAINT "Client_pkey" PRIMARY KEY ("id"); 20 | -------------------------------------------------------------------------------- /prisma/migrations/20241009032354_add_account_details/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `displayName` to the `User` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `email` to the `User` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `profilePicture` to the `User` table without a default value. This is not possible if the table is not empty. 7 | 8 | */ 9 | -- AlterTable 10 | ALTER TABLE "User" ADD COLUMN "displayName" TEXT NOT NULL, 11 | ADD COLUMN "email" TEXT NOT NULL, 12 | ADD COLUMN "profilePicture" TEXT NOT NULL; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20241010062956_add_constraints/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[libraryBasePath]` on the table `Game` will be added. If there are existing duplicate values, this will fail. 5 | - Added the required column `libraryBasePath` to the `Game` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `versionOrder` to the `Game` table without a default value. This is not possible if the table is not empty. 7 | 8 | */ 9 | -- AlterTable 10 | ALTER TABLE "Game" ADD COLUMN "libraryBasePath" TEXT NOT NULL, 11 | ADD COLUMN "versionOrder" TEXT NOT NULL; 12 | 13 | -- CreateTable 14 | CREATE TABLE "GameVersion" ( 15 | "gameId" TEXT NOT NULL, 16 | "versionName" TEXT NOT NULL, 17 | 18 | CONSTRAINT "GameVersion_pkey" PRIMARY KEY ("gameId","versionName") 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "Game_libraryBasePath_key" ON "Game"("libraryBasePath"); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "GameVersion" ADD CONSTRAINT "GameVersion_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /prisma/migrations/20241010095344_various_fixes/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The `versionOrder` column on the `Game` table would be dropped and recreated. This will lead to data loss if there is data in the column. 5 | - Changed the type of `platform` on the `Client` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. 6 | - Added the required column `launchCommand` to the `GameVersion` table without a default value. This is not possible if the table is not empty. 7 | - Added the required column `platform` to the `GameVersion` table without a default value. This is not possible if the table is not empty. 8 | - Added the required column `setupCommand` to the `GameVersion` table without a default value. This is not possible if the table is not empty. 9 | 10 | */ 11 | -- CreateEnum 12 | CREATE TYPE "Platform" AS ENUM ('windows', 'linux'); 13 | 14 | -- AlterTable 15 | ALTER TABLE "Client" DROP COLUMN "platform", 16 | ADD COLUMN "platform" "Platform" NOT NULL; 17 | 18 | -- AlterTable 19 | ALTER TABLE "Game" DROP COLUMN "versionOrder", 20 | ADD COLUMN "versionOrder" TEXT[]; 21 | 22 | -- AlterTable 23 | ALTER TABLE "GameVersion" ADD COLUMN "launchCommand" TEXT NOT NULL, 24 | ADD COLUMN "platform" "Platform" NOT NULL, 25 | ADD COLUMN "setupCommand" TEXT NOT NULL; 26 | -------------------------------------------------------------------------------- /prisma/migrations/20241010104439_added_original_query_field/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `metadataOriginalQuery` to the `Developer` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `metadataOriginalQuery` to the `Publisher` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Developer" ADD COLUMN "metadataOriginalQuery" TEXT NOT NULL; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Publisher" ADD COLUMN "metadataOriginalQuery" TEXT NOT NULL; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20241010104722_fix_unique_constraints/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[metadataSource,metadataId,metadataOriginalQuery]` on the table `Developer` will be added. If there are existing duplicate values, this will fail. 5 | - A unique constraint covering the columns `[metadataSource,metadataId,metadataOriginalQuery]` on the table `Publisher` will be added. If there are existing duplicate values, this will fail. 6 | 7 | */ 8 | -- DropIndex 9 | DROP INDEX "Developer_metadataSource_metadataId_key"; 10 | 11 | -- DropIndex 12 | DROP INDEX "Publisher_metadataSource_metadataId_key"; 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "Developer_metadataSource_metadataId_metadataOriginalQuery_key" ON "Developer"("metadataSource", "metadataId", "metadataOriginalQuery"); 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "Publisher_metadataSource_metadataId_metadataOriginalQuery_key" ON "Publisher"("metadataSource", "metadataId", "metadataOriginalQuery"); 19 | -------------------------------------------------------------------------------- /prisma/migrations/20241011035227_add_droplet_manifest_to_game_versions/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `dropletManifest` to the `GameVersion` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "GameVersion" ADD COLUMN "dropletManifest" JSONB NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20241011093950_update_game_images_system/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `mArt` on the `Game` table. All the data in the column will be lost. 5 | - You are about to drop the column `mBannerId` on the `Game` table. All the data in the column will be lost. 6 | - You are about to drop the column `mScreenshots` on the `Game` table. All the data in the column will be lost. 7 | 8 | */ 9 | -- AlterTable 10 | ALTER TABLE "Game" DROP COLUMN "mArt", 11 | DROP COLUMN "mBannerId", 12 | DROP COLUMN "mScreenshots", 13 | ADD COLUMN "mBannerIndex" INTEGER NOT NULL DEFAULT 0, 14 | ADD COLUMN "mImageLibrary" TEXT[]; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20241011101243_revert_banner_system/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `mBannerIndex` on the `Game` table. All the data in the column will be lost. 5 | - Added the required column `mBannerId` to the `Game` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Game" DROP COLUMN "mBannerIndex", 10 | ADD COLUMN "mBannerId" TEXT NOT NULL; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20241011103116_add_cover_image/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `mCoverId` to the `Game` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Game" ADD COLUMN "mCoverId" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20241014052934_add_delta_and_order/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `versionIndex` to the `GameVersion` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "GameVersion" ADD COLUMN "delta" BOOLEAN NOT NULL DEFAULT false, 9 | ADD COLUMN "versionIndex" INTEGER NOT NULL; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20241014053941_remove_version_order/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `versionOrder` on the `Game` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Game" DROP COLUMN "versionOrder"; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20241025091103_add_invitations/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Invitation" ( 3 | "id" TEXT NOT NULL, 4 | "isAdmin" BOOLEAN NOT NULL DEFAULT false, 5 | "username" TEXT, 6 | "email" TEXT, 7 | 8 | CONSTRAINT "Invitation_pkey" PRIMARY KEY ("id") 9 | ); 10 | -------------------------------------------------------------------------------- /prisma/migrations/20241102000813_create_application_configuration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ApplicationSettings" ( 3 | "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 4 | "enabledAuthencationMechanisms" "AuthMec"[], 5 | 6 | CONSTRAINT "ApplicationSettings_pkey" PRIMARY KEY ("timestamp") 7 | ); 8 | -------------------------------------------------------------------------------- /prisma/migrations/20241105221904_different_client_capabilities/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The values [DownloadAggregation] on the enum `ClientCapabilities` will be removed. If these variants are still used in the database, this will fail. 5 | 6 | */ 7 | -- AlterEnum 8 | BEGIN; 9 | CREATE TYPE "ClientCapabilities_new" AS ENUM ('PeerAPI', 'UserStatus'); 10 | ALTER TABLE "Client" ALTER COLUMN "capabilities" TYPE "ClientCapabilities_new"[] USING ("capabilities"::text::"ClientCapabilities_new"[]); 11 | ALTER TYPE "ClientCapabilities" RENAME TO "ClientCapabilities_old"; 12 | ALTER TYPE "ClientCapabilities_new" RENAME TO "ClientCapabilities"; 13 | DROP TYPE "ClientCapabilities_old"; 14 | COMMIT; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20241105222110_trackable_names_for_capabilities/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The values [PeerAPI,UserStatus] on the enum `ClientCapabilities` will be removed. If these variants are still used in the database, this will fail. 5 | 6 | */ 7 | -- AlterEnum 8 | BEGIN; 9 | CREATE TYPE "ClientCapabilities_new" AS ENUM ('peerAPI', 'userStatus'); 10 | ALTER TABLE "Client" ALTER COLUMN "capabilities" TYPE "ClientCapabilities_new"[] USING ("capabilities"::text::"ClientCapabilities_new"[]); 11 | ALTER TYPE "ClientCapabilities" RENAME TO "ClientCapabilities_old"; 12 | ALTER TYPE "ClientCapabilities_new" RENAME TO "ClientCapabilities"; 13 | DROP TYPE "ClientCapabilities_old"; 14 | COMMIT; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20241105225732_peer_api_configuration/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `endpoint` on the `Client` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Client" DROP COLUMN "endpoint"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "ClientPeerAPIConfiguration" ( 12 | "id" TEXT NOT NULL, 13 | "clientId" TEXT NOT NULL, 14 | "ipConfigurations" TEXT[], 15 | 16 | CONSTRAINT "ClientPeerAPIConfiguration_pkey" PRIMARY KEY ("id") 17 | ); 18 | 19 | -- CreateIndex 20 | CREATE UNIQUE INDEX "ClientPeerAPIConfiguration_clientId_key" ON "ClientPeerAPIConfiguration"("clientId"); 21 | 22 | -- AddForeignKey 23 | ALTER TABLE "ClientPeerAPIConfiguration" ADD CONSTRAINT "ClientPeerAPIConfiguration_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 24 | -------------------------------------------------------------------------------- /prisma/migrations/20241105230021_move_to_endpoint_configuration/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `ipConfigurations` on the `ClientPeerAPIConfiguration` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "ClientPeerAPIConfiguration" DROP COLUMN "ipConfigurations", 9 | ADD COLUMN "endpoints" TEXT[]; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20241107080421_add_expiry_for_invitations/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `expires` to the `Invitation` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Invitation" ADD COLUMN "expires" TIMESTAMP(3) NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20241116053120_add_notifications/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Notification" ( 3 | "id" TEXT NOT NULL, 4 | "nonce" TEXT, 5 | "userId" TEXT NOT NULL, 6 | "title" TEXT NOT NULL, 7 | "description" TEXT NOT NULL, 8 | "actions" TEXT[], 9 | "read" BOOLEAN NOT NULL DEFAULT false, 10 | 11 | CONSTRAINT "Notification_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "Notification_nonce_key" ON "Notification"("nonce"); 16 | 17 | -- AddForeignKey 18 | ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 19 | -------------------------------------------------------------------------------- /prisma/migrations/20241116054212_add_created_time_stamp_to_notifications/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Notification" ADD COLUMN "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20241122215933_add_created_timestamps_for_games/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Game" ADD COLUMN "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 3 | 4 | -- AlterTable 5 | ALTER TABLE "GameVersion" ADD COLUMN "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20241124042825_add_released_date_for_the_game/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `mReleased` to the `Game` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Game" ADD COLUMN "mReleased" TIMESTAMP(3) NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20241223022005_add_umu_id_to_game_version/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GameVersion" ADD COLUMN "umuIdOverride" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20241223100329_add_referential_deletion_for_game_versions/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "GameVersion" DROP CONSTRAINT "GameVersion_gameId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "GameVersion" ADD CONSTRAINT "GameVersion_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20241223100418_update_to_prisma_6/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "_DeveloperToGame" ADD CONSTRAINT "_DeveloperToGame_AB_pkey" PRIMARY KEY ("A", "B"); 3 | 4 | -- DropIndex 5 | DROP INDEX "_DeveloperToGame_AB_unique"; 6 | 7 | -- AlterTable 8 | ALTER TABLE "_GameToPublisher" ADD CONSTRAINT "_GameToPublisher_AB_pkey" PRIMARY KEY ("A", "B"); 9 | 10 | -- DropIndex 11 | DROP INDEX "_GameToPublisher_AB_unique"; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20241226065709_rename_custom_to_manual/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The values [Custom] on the enum `MetadataSource` will be removed. If these variants are still used in the database, this will fail. 5 | 6 | */ 7 | -- AlterEnum 8 | BEGIN; 9 | CREATE TYPE "MetadataSource_new" AS ENUM ('Manual', 'GiantBomb'); 10 | ALTER TABLE "Game" ALTER COLUMN "metadataSource" TYPE "MetadataSource_new" USING ("metadataSource"::text::"MetadataSource_new"); 11 | ALTER TABLE "Developer" ALTER COLUMN "metadataSource" TYPE "MetadataSource_new" USING ("metadataSource"::text::"MetadataSource_new"); 12 | ALTER TABLE "Publisher" ALTER COLUMN "metadataSource" TYPE "MetadataSource_new" USING ("metadataSource"::text::"MetadataSource_new"); 13 | ALTER TYPE "MetadataSource" RENAME TO "MetadataSource_old"; 14 | ALTER TYPE "MetadataSource_new" RENAME TO "MetadataSource"; 15 | DROP TYPE "MetadataSource_old"; 16 | COMMIT; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20241226230207_add_image_carousel/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Game" ADD COLUMN "mImageCarousel" INTEGER[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20241227033610_move_image_carousel_to_image_ids/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Game" ALTER COLUMN "mImageCarousel" SET DATA TYPE TEXT[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20241230053403_add_args_and_only_setup/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GameVersion" ADD COLUMN "launchArgs" TEXT[], 3 | ADD COLUMN "onlySetup" BOOLEAN NOT NULL DEFAULT false, 4 | ADD COLUMN "setupArgs" TEXT[], 5 | ALTER COLUMN "launchCommand" DROP NOT NULL, 6 | ALTER COLUMN "setupCommand" DROP NOT NULL; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20250103202348_add_collections/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Collection" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "isDefault" BOOLEAN NOT NULL DEFAULT false, 6 | "userId" TEXT NOT NULL, 7 | 8 | CONSTRAINT "Collection_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateTable 12 | CREATE TABLE "_CollectionToGame" ( 13 | "A" TEXT NOT NULL, 14 | "B" TEXT NOT NULL, 15 | 16 | CONSTRAINT "_CollectionToGame_AB_pkey" PRIMARY KEY ("A","B") 17 | ); 18 | 19 | -- CreateIndex 20 | CREATE INDEX "_CollectionToGame_B_index" ON "_CollectionToGame"("B"); 21 | 22 | -- AddForeignKey 23 | ALTER TABLE "Collection" ADD CONSTRAINT "Collection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "_CollectionToGame" ADD CONSTRAINT "_CollectionToGame_A_fkey" FOREIGN KEY ("A") REFERENCES "Collection"("id") ON DELETE CASCADE ON UPDATE CASCADE; 27 | 28 | -- AddForeignKey 29 | ALTER TABLE "_CollectionToGame" ADD CONSTRAINT "_CollectionToGame_B_fkey" FOREIGN KEY ("B") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; 30 | -------------------------------------------------------------------------------- /prisma/migrations/20250109005948_use_collection_entry_to_ensure_unique_games/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `_CollectionToGame` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "_CollectionToGame" DROP CONSTRAINT "_CollectionToGame_A_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "_CollectionToGame" DROP CONSTRAINT "_CollectionToGame_B_fkey"; 12 | 13 | -- DropTable 14 | DROP TABLE "_CollectionToGame"; 15 | 16 | -- CreateTable 17 | CREATE TABLE "CollectionEntry" ( 18 | "collectionId" TEXT NOT NULL, 19 | "gameId" TEXT NOT NULL, 20 | 21 | CONSTRAINT "CollectionEntry_pkey" PRIMARY KEY ("collectionId","gameId") 22 | ); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "CollectionEntry" ADD CONSTRAINT "CollectionEntry_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "CollectionEntry" ADD CONSTRAINT "CollectionEntry_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 29 | -------------------------------------------------------------------------------- /prisma/migrations/20250128060446_casacad_e_delete_for_collection_entries/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "CollectionEntry" DROP CONSTRAINT "CollectionEntry_collectionId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "CollectionEntry" ADD CONSTRAINT "CollectionEntry_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20250128102738_add_news/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "news" ( 3 | "id" TEXT NOT NULL, 4 | "title" TEXT NOT NULL, 5 | "content" TEXT NOT NULL, 6 | "excerpt" TEXT NOT NULL, 7 | "tags" TEXT[], 8 | "image" TEXT, 9 | "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "authorId" TEXT NOT NULL, 11 | 12 | CONSTRAINT "news_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "news" ADD CONSTRAINT "news_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20250204010021_add_tokens/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "APITokenMode" AS ENUM ('User', 'System'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "APIToken" ( 6 | "token" TEXT NOT NULL, 7 | "mode" "APITokenMode" NOT NULL, 8 | "userId" TEXT, 9 | "acls" TEXT[], 10 | 11 | CONSTRAINT "APIToken_pkey" PRIMARY KEY ("token") 12 | ); 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "APIToken" ADD CONSTRAINT "APIToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20250204020918_add_collection_entry_casacade_delete/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "CollectionEntry" DROP CONSTRAINT "CollectionEntry_gameId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "CollectionEntry" ADD CONSTRAINT "CollectionEntry_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20250208004345_add_api_token_name/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `name` to the `APIToken` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "APIToken" ADD COLUMN "name" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250208005625_add_id_to_token/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `APIToken` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - The required column `id` was added to the `APIToken` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "APIToken" DROP CONSTRAINT "APIToken_pkey", 10 | ADD COLUMN "id" TEXT NOT NULL, 11 | ADD CONSTRAINT "APIToken_pkey" PRIMARY KEY ("id"); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "APIToken_token_idx" ON "APIToken"("token"); 15 | -------------------------------------------------------------------------------- /prisma/migrations/20250211230021_ensure_non_null_launch_and_setup_commands/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `launchCommand` on table `GameVersion` required. This step will fail if there are existing NULL values in that column. 5 | - Made the column `setupCommand` on table `GameVersion` required. This step will fail if there are existing NULL values in that column. 6 | 7 | */ 8 | UPDATE "GameVersion" 9 | SET "launchCommand" = '' 10 | WHERE "launchCommand" is NULL; 11 | 12 | UPDATE "GameVersion" 13 | SET "setupCommand" = '' 14 | WHERE "launchCommand" is NULL; 15 | 16 | -- AlterTable 17 | ALTER TABLE "GameVersion" ALTER COLUMN "launchCommand" SET NOT NULL, 18 | ALTER COLUMN "launchCommand" SET DEFAULT '', 19 | ALTER COLUMN "setupCommand" SET NOT NULL, 20 | ALTER COLUMN "setupCommand" SET DEFAULT ''; 21 | -------------------------------------------------------------------------------- /prisma/migrations/20250309234300_news_articles/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `news` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "news" DROP CONSTRAINT "news_authorId_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "news"; 12 | 13 | -- CreateTable 14 | CREATE TABLE "Tag" ( 15 | "id" TEXT NOT NULL, 16 | "name" TEXT NOT NULL, 17 | 18 | CONSTRAINT "Tag_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateTable 22 | CREATE TABLE "Article" ( 23 | "id" TEXT NOT NULL, 24 | "title" TEXT NOT NULL, 25 | "description" TEXT NOT NULL, 26 | "content" TEXT NOT NULL, 27 | "image" TEXT, 28 | "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 29 | "authorId" TEXT, 30 | 31 | CONSTRAINT "Article_pkey" PRIMARY KEY ("id") 32 | ); 33 | 34 | -- CreateTable 35 | CREATE TABLE "_ArticleToTag" ( 36 | "A" TEXT NOT NULL, 37 | "B" TEXT NOT NULL, 38 | 39 | CONSTRAINT "_ArticleToTag_AB_pkey" PRIMARY KEY ("A","B") 40 | ); 41 | 42 | -- CreateIndex 43 | CREATE INDEX "_ArticleToTag_B_index" ON "_ArticleToTag"("B"); 44 | 45 | -- AddForeignKey 46 | ALTER TABLE "Article" ADD CONSTRAINT "Article_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 47 | 48 | -- AddForeignKey 49 | ALTER TABLE "_ArticleToTag" ADD CONSTRAINT "_ArticleToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; 50 | 51 | -- AddForeignKey 52 | ALTER TABLE "_ArticleToTag" ADD CONSTRAINT "_ArticleToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; 53 | -------------------------------------------------------------------------------- /prisma/migrations/20250309234801_make_tags_unique/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[name]` on the table `Tag` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250309234846_make_tokens_unique/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[token]` on the table `APIToken` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "APIToken_token_key" ON "APIToken"("token"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250311073601_add_macos_as_a_platform/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "Platform" ADD VALUE 'macos'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250312230736_add_metadata_providers_to_appconfig/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "ApplicationSettings" ADD COLUMN "metadataProviders" TEXT[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250313042306_add_igdb_pcgamingwiki_metadata/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | -- This migration adds more than one value to an enum. 3 | -- With PostgreSQL versions 11 and earlier, this is not possible 4 | -- in a single migration. This can be worked around by creating 5 | -- multiple migrations, each migration adding only one value to 6 | -- the enum. 7 | 8 | 9 | ALTER TYPE "MetadataSource" ADD VALUE 'PCGamingWiki'; 10 | ALTER TYPE "MetadataSource" ADD VALUE 'IGDB'; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20250313053250_add_enable_fields_to_auth_and_users/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "LinkedAuthMec" ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT true; 3 | 4 | -- AlterTable 5 | ALTER TABLE "User" ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT true; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20250314153636_store_ca_and_session_in_db/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Certificate" ( 3 | "id" TEXT NOT NULL, 4 | "privateKey" TEXT NOT NULL, 5 | "certificate" TEXT NOT NULL, 6 | "blacklisted" BOOLEAN NOT NULL DEFAULT false, 7 | 8 | CONSTRAINT "Certificate_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateTable 12 | CREATE TABLE "Session" ( 13 | "token" TEXT NOT NULL, 14 | "data" JSONB NOT NULL, 15 | 16 | CONSTRAINT "Session_pkey" PRIMARY KEY ("token") 17 | ); 18 | -------------------------------------------------------------------------------- /prisma/migrations/20250324014736_add_auth_mek_version/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "LinkedAuthMec" ADD COLUMN "version" INTEGER NOT NULL DEFAULT 1; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250401082200_add_save_slots/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "ClientCapabilities" ADD VALUE 'save'; 3 | 4 | -- CreateTable 5 | CREATE TABLE "SaveSlot" ( 6 | "gameId" TEXT NOT NULL, 7 | "userId" TEXT NOT NULL, 8 | "index" INTEGER NOT NULL, 9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "playtime" DOUBLE PRECISION NOT NULL, 11 | "lastUsedClientId" TEXT NOT NULL, 12 | "data" TEXT[], 13 | 14 | CONSTRAINT "SaveSlot_pkey" PRIMARY KEY ("gameId","userId","index") 15 | ); 16 | 17 | -- AddForeignKey 18 | ALTER TABLE "SaveSlot" ADD CONSTRAINT "SaveSlot_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "SaveSlot" ADD CONSTRAINT "SaveSlot_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "SaveSlot" ADD CONSTRAINT "SaveSlot_lastUsedClientId_fkey" FOREIGN KEY ("lastUsedClientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /prisma/migrations/20250401082605_add_save_slot_limits_to_application_settings/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "ApplicationSettings" ADD COLUMN "saveSlotCountLimit" INTEGER NOT NULL DEFAULT 5, 3 | ADD COLUMN "saveSlotSizeLimit" DOUBLE PRECISION NOT NULL DEFAULT 10; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20250401083942_rename_save_to_cloud_saves/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The values [save] on the enum `ClientCapabilities` will be removed. If these variants are still used in the database, this will fail. 5 | 6 | */ 7 | -- AlterEnum 8 | BEGIN; 9 | CREATE TYPE "ClientCapabilities_new" AS ENUM ('peerAPI', 'userStatus', 'cloudSaves'); 10 | ALTER TABLE "Client" ALTER COLUMN "capabilities" TYPE "ClientCapabilities_new"[] USING ("capabilities"::text::"ClientCapabilities_new"[]); 11 | ALTER TYPE "ClientCapabilities" RENAME TO "ClientCapabilities_old"; 12 | ALTER TYPE "ClientCapabilities_new" RENAME TO "ClientCapabilities"; 13 | DROP TYPE "ClientCapabilities_old"; 14 | COMMIT; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20250401084907_add_history_limit/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "ApplicationSettings" ADD COLUMN "saveSlotHistoryLimit" INTEGER NOT NULL DEFAULT 3; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250401085406_add_default_to_playtime/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "SaveSlot" ALTER COLUMN "playtime" SET DEFAULT 0; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250401091937_add_history_and_hashes/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `data` on the `SaveSlot` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "SaveSlot" DROP COLUMN "data", 9 | ADD COLUMN "history" TEXT[], 10 | ADD COLUMN "historyChecksums" TEXT[]; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20250403233442_apply_store_changes/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `expiresAt` to the `Session` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `userId` to the `Session` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Session" ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL, 10 | ADD COLUMN "userId" TEXT NOT NULL; 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20250405062945_make_last_accessed_optional_on_save_slots/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "SaveSlot" DROP CONSTRAINT "SaveSlot_lastUsedClientId_fkey"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "SaveSlot" ALTER COLUMN "lastUsedClientId" DROP NOT NULL; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "SaveSlot" ADD CONSTRAINT "SaveSlot_lastUsedClientId_fkey" FOREIGN KEY ("lastUsedClientId") REFERENCES "Client"("id") ON DELETE SET NULL ON UPDATE CASCADE; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250407090729_add_client_token_mode/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "APITokenMode" ADD VALUE 'Client'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250407091012_add_client_token_field_to_apitoken/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "APIToken" ADD COLUMN "clientId" TEXT; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "APIToken" ADD CONSTRAINT "APIToken_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20250414002714_add_object_hash/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ObjectHash" ( 3 | "id" TEXT NOT NULL, 4 | "hash" TEXT NOT NULL, 5 | 6 | CONSTRAINT "ObjectHash_pkey" PRIMARY KEY ("id") 7 | ); 8 | -------------------------------------------------------------------------------- /prisma/migrations/20250507120031_add_openid_authmek/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "AuthMec" ADD VALUE 'OpenID'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250507223112_remove_authentication_option_from_applicationsettings/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `enabledAuthencationMechanisms` on the `ApplicationSettings` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "ApplicationSettings" DROP COLUMN "enabledAuthencationMechanisms"; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250508153613_add_screenshots/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Screenshot" ( 3 | "id" TEXT NOT NULL, 4 | "gameId" TEXT NOT NULL, 5 | "userId" TEXT NOT NULL, 6 | "objectId" TEXT NOT NULL, 7 | "private" BOOLEAN NOT NULL DEFAULT true, 8 | "createdAt" TIMESTAMPTZ(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | 10 | CONSTRAINT "Screenshot_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "Screenshot_gameId_userId_idx" ON "Screenshot"("gameId", "userId"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Screenshot" ADD CONSTRAINT "Screenshot_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "Screenshot" ADD CONSTRAINT "Screenshot_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 21 | -------------------------------------------------------------------------------- /prisma/migrations/20250508224553_cleanup_old_objects/migration.sql: -------------------------------------------------------------------------------- 1 | 2 | -- Rename game table columns 3 | ALTER TABLE "Game" RENAME COLUMN "mIconId" TO "mIconObjectId"; 4 | ALTER TABLE "Game" RENAME COLUMN "mBannerId" TO "mBannerObjectId"; 5 | ALTER TABLE "Game" RENAME COLUMN "mCoverId" TO "mCoverObjectId"; 6 | ALTER TABLE "Game" RENAME COLUMN "mImageCarousel" TO "mImageCarouselObjectIds"; 7 | ALTER TABLE "Game" RENAME COLUMN "mImageLibrary" TO "mImageLibraryObjectIds"; 8 | 9 | -- Rename saveslot table columns 10 | ALTER TABLE "SaveSlot" RENAME COLUMN "history" TO "historyObjectIds"; 11 | 12 | -- Rename article table columns 13 | ALTER TABLE "Article" RENAME COLUMN "image" TO "imageObjectId"; 14 | 15 | -- Rename user table columns 16 | ALTER TABLE "User" RENAME COLUMN "profilePicture" TO "profilePictureObjectId"; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20250509003340_init_unified_company_metadata/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Company" ( 3 | "id" TEXT NOT NULL, 4 | "metadataSource" "MetadataSource" NOT NULL, 5 | "metadataId" TEXT NOT NULL, 6 | "metadataOriginalQuery" TEXT NOT NULL, 7 | "mName" TEXT NOT NULL, 8 | "mShortDescription" TEXT NOT NULL, 9 | "mDescription" TEXT NOT NULL, 10 | "mLogoObjectId" TEXT NOT NULL, 11 | "mBannerObjectId" TEXT NOT NULL, 12 | "mWebsite" TEXT NOT NULL, 13 | 14 | CONSTRAINT "Company_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- CreateTable 18 | CREATE TABLE "CompanyGameRelation" ( 19 | "companyId" TEXT NOT NULL, 20 | "gameId" TEXT NOT NULL, 21 | "developer" BOOLEAN NOT NULL DEFAULT false, 22 | "publisher" BOOLEAN NOT NULL DEFAULT false 23 | ); 24 | 25 | -- CreateIndex 26 | CREATE UNIQUE INDEX "Company_metadataSource_metadataId_key" ON "Company"("metadataSource", "metadataId"); 27 | 28 | -- CreateIndex 29 | CREATE UNIQUE INDEX "CompanyGameRelation_companyId_gameId_key" ON "CompanyGameRelation"("companyId", "gameId"); 30 | 31 | -- AddForeignKey 32 | ALTER TABLE "CompanyGameRelation" ADD CONSTRAINT "CompanyGameRelation_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE; 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "CompanyGameRelation" ADD CONSTRAINT "CompanyGameRelation_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; 36 | -------------------------------------------------------------------------------- /prisma/migrations/20250511154134_add_tags_to_games/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "_GameToTag" ( 3 | "A" TEXT NOT NULL, 4 | "B" TEXT NOT NULL, 5 | 6 | CONSTRAINT "_GameToTag_AB_pkey" PRIMARY KEY ("A","B") 7 | ); 8 | 9 | -- CreateIndex 10 | CREATE INDEX "_GameToTag_B_index" ON "_GameToTag"("B"); 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "_GameToTag" ADD CONSTRAINT "_GameToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "_GameToTag" ADD CONSTRAINT "_GameToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20250514193830_allow_notification_nonce_reuse_per_user/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[userId,nonce]` on the table `Notification` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "Notification_nonce_key"; 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "Notification_userId_nonce_key" ON "Notification"("userId", "nonce"); 12 | -------------------------------------------------------------------------------- /prisma/migrations/20250515021331_add_game_ratings/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `mReviewCount` on the `Game` table. All the data in the column will be lost. 5 | - You are about to drop the column `mReviewRating` on the `Game` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterEnum 9 | -- This migration adds more than one value to an enum. 10 | -- With PostgreSQL versions 11 and earlier, this is not possible 11 | -- in a single migration. This can be worked around by creating 12 | -- multiple migrations, each migration adding only one value to 13 | -- the enum. 14 | 15 | 16 | ALTER TYPE "MetadataSource" ADD VALUE 'Metacritic'; 17 | ALTER TYPE "MetadataSource" ADD VALUE 'OpenCritic'; 18 | 19 | -- AlterTable 20 | ALTER TABLE "Game" DROP COLUMN "mReviewCount", 21 | DROP COLUMN "mReviewRating"; 22 | 23 | -- CreateTable 24 | CREATE TABLE "GameRating" ( 25 | "id" TEXT NOT NULL, 26 | "metadataSource" "MetadataSource" NOT NULL, 27 | "metadataId" TEXT NOT NULL, 28 | "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 29 | "mReviewCount" INTEGER NOT NULL, 30 | "mReviewRating" DOUBLE PRECISION NOT NULL, 31 | "mReviewHref" TEXT, 32 | "gameId" TEXT NOT NULL, 33 | 34 | CONSTRAINT "GameRating_pkey" PRIMARY KEY ("id") 35 | ); 36 | 37 | -- CreateIndex 38 | CREATE UNIQUE INDEX "GameRating_metadataSource_metadataId_key" ON "GameRating"("metadataSource", "metadataId"); 39 | 40 | -- AddForeignKey 41 | ALTER TABLE "GameRating" ADD CONSTRAINT "GameRating_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; 42 | -------------------------------------------------------------------------------- /prisma/migrations/20250515043254_add_acls_to_notifications/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Notification" ADD COLUMN "acls" TEXT[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250601022736_add_database_library/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `ClientPeerAPIConfiguration` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- CreateEnum 8 | CREATE TYPE "LibraryBackend" AS ENUM ('Filesystem'); 9 | 10 | -- AlterEnum 11 | ALTER TYPE "ClientCapabilities" ADD VALUE 'trackPlaytime'; 12 | 13 | -- DropForeignKey 14 | ALTER TABLE "ClientPeerAPIConfiguration" DROP CONSTRAINT "ClientPeerAPIConfiguration_clientId_fkey"; 15 | 16 | -- AlterTable 17 | ALTER TABLE "Screenshot" ALTER COLUMN "private" DROP DEFAULT; 18 | 19 | -- DropTable 20 | DROP TABLE "ClientPeerAPIConfiguration"; 21 | 22 | -- CreateTable 23 | CREATE TABLE "Library" ( 24 | "id" TEXT NOT NULL, 25 | "name" TEXT NOT NULL, 26 | "backend" "LibraryBackend" NOT NULL, 27 | "options" JSONB NOT NULL, 28 | 29 | CONSTRAINT "Library_pkey" PRIMARY KEY ("id") 30 | ); 31 | 32 | -- CreateTable 33 | CREATE TABLE "Playtime" ( 34 | "gameId" TEXT NOT NULL, 35 | "userId" TEXT NOT NULL, 36 | "seconds" INTEGER NOT NULL, 37 | "updatedAt" TIMESTAMPTZ(6) NOT NULL, 38 | "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, 39 | 40 | CONSTRAINT "Playtime_pkey" PRIMARY KEY ("gameId","userId") 41 | ); 42 | 43 | -- CreateIndex 44 | CREATE INDEX "Playtime_userId_idx" ON "Playtime"("userId"); 45 | 46 | -- CreateIndex 47 | CREATE INDEX "Screenshot_userId_idx" ON "Screenshot"("userId"); 48 | 49 | -- AddForeignKey 50 | ALTER TABLE "Playtime" ADD CONSTRAINT "Playtime_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; 51 | 52 | -- AddForeignKey 53 | ALTER TABLE "Playtime" ADD CONSTRAINT "Playtime_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 54 | -------------------------------------------------------------------------------- /prisma/migrations/20250601032211_add_library_relation_to_game/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `libraryBasePath` on the `Game` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "Game_libraryBasePath_key"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Game" RENAME COLUMN "libraryBasePath" TO "libraryPath"; 12 | 13 | ALTER TABLE "Game" ADD COLUMN "libraryId" TEXT; 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "Game" 17 | ADD CONSTRAINT "Game_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "Library" ("id") ON DELETE SET NULL ON UPDATE CASCADE; -------------------------------------------------------------------------------- /prisma/migrations/20250601032938_add_unique_constraint/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[libraryId,libraryPath]` on the table `Game` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "Game_libraryId_libraryPath_key" ON "Game"("libraryId", "libraryPath"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /prisma/models/app.prisma: -------------------------------------------------------------------------------- 1 | model ApplicationSettings { 2 | timestamp DateTime @id @default(now()) 3 | 4 | metadataProviders String[] 5 | 6 | saveSlotCountLimit Int @default(5) 7 | saveSlotSizeLimit Float @default(10) // MB 8 | saveSlotHistoryLimit Int @default(3) 9 | } 10 | 11 | enum Platform { 12 | Windows @map("windows") 13 | Linux @map("linux") 14 | macOS @map("macos") 15 | } 16 | 17 | enum LibraryBackend { 18 | Filesystem 19 | } 20 | 21 | model Library { 22 | id String @id @default(uuid()) 23 | name String 24 | 25 | backend LibraryBackend 26 | options Json 27 | 28 | games Game[] 29 | } 30 | -------------------------------------------------------------------------------- /prisma/models/auth.prisma: -------------------------------------------------------------------------------- 1 | enum AuthMec { 2 | Simple 3 | OpenID 4 | } 5 | 6 | model LinkedAuthMec { 7 | userId String 8 | mec AuthMec 9 | enabled Boolean @default(true) 10 | 11 | version Int @default(1) 12 | credentials Json 13 | 14 | user User @relation(fields: [userId], references: [id]) 15 | 16 | @@id([userId, mec]) 17 | } 18 | 19 | model Invitation { 20 | id String @id @default(uuid()) 21 | isAdmin Boolean @default(false) 22 | 23 | username String? 24 | email String? 25 | expires DateTime 26 | } 27 | 28 | enum APITokenMode { 29 | User 30 | System 31 | Client 32 | } 33 | 34 | model APIToken { 35 | id String @id @default(uuid()) 36 | token String @unique @default(uuid()) 37 | mode APITokenMode 38 | name String 39 | 40 | userId String? 41 | user User? @relation(fields: [userId], references: [id]) 42 | 43 | clientId String? 44 | client Client? @relation(fields: [clientId], references: [id], onDelete: Cascade) 45 | 46 | acls String[] 47 | 48 | @@index([token]) 49 | } 50 | 51 | model Certificate { 52 | id String @id @default(uuid()) 53 | 54 | privateKey String 55 | certificate String 56 | 57 | blacklisted Boolean @default(false) 58 | } 59 | 60 | model Session { 61 | token String @id 62 | expiresAt DateTime 63 | 64 | userId String 65 | user User? @relation(fields: [userId], references: [id]) 66 | 67 | data Json // misc extra data 68 | } 69 | -------------------------------------------------------------------------------- /prisma/models/client.prisma: -------------------------------------------------------------------------------- 1 | enum ClientCapabilities { 2 | PeerAPI @map("peerAPI") // other clients can use the HTTP API to P2P with this client 3 | UserStatus @map("userStatus") // this client can report this user's status (playing, online, etc etc) 4 | CloudSaves @map("cloudSaves") // ability to save to save slots 5 | TrackPlaytime @map("trackPlaytime") // ability to track user playtime 6 | } 7 | 8 | // References a device 9 | model Client { 10 | id String @id @default(uuid()) 11 | userId String 12 | user User @relation(fields: [userId], references: [id]) 13 | 14 | capabilities ClientCapabilities[] 15 | 16 | name String 17 | platform Platform 18 | lastConnected DateTime 19 | 20 | lastAccessedSaves SaveSlot[] 21 | tokens APIToken[] 22 | } 23 | -------------------------------------------------------------------------------- /prisma/models/collection.prisma: -------------------------------------------------------------------------------- 1 | model Collection { 2 | id String @id @default(uuid()) 3 | name String 4 | 5 | isDefault Boolean @default(false) 6 | userId String 7 | user User @relation(fields: [userId], references: [id]) 8 | 9 | entries CollectionEntry[] 10 | } 11 | 12 | model CollectionEntry { 13 | collectionId String 14 | collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade) 15 | 16 | gameId String 17 | game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) 18 | 19 | @@id([collectionId, gameId]) 20 | } 21 | -------------------------------------------------------------------------------- /prisma/models/news.prisma: -------------------------------------------------------------------------------- 1 | model Tag { 2 | id String @id @default(uuid()) 3 | name String @unique 4 | 5 | articles Article[] 6 | games Game[] 7 | } 8 | 9 | model Article { 10 | id String @id @default(uuid()) 11 | title String 12 | description String 13 | content String @db.Text 14 | 15 | tags Tag[] 16 | 17 | imageObjectId String? // Object ID 18 | publishedAt DateTime @default(now()) 19 | 20 | author User? @relation(fields: [authorId], references: [id]) // Optional, if no user, it's a system post 21 | authorId String? 22 | } 23 | -------------------------------------------------------------------------------- /prisma/models/user.prisma: -------------------------------------------------------------------------------- 1 | model User { 2 | id String @id @default(uuid()) 3 | username String @unique 4 | admin Boolean @default(false) 5 | enabled Boolean @default(true) 6 | 7 | email String 8 | displayName String 9 | profilePictureObjectId String // Object 10 | 11 | authMecs LinkedAuthMec[] 12 | clients Client[] 13 | notifications Notification[] 14 | collections Collection[] 15 | articles Article[] 16 | 17 | tokens APIToken[] 18 | sessions Session[] 19 | 20 | saves SaveSlot[] 21 | screenshots Screenshot[] 22 | playtime Playtime[] 23 | } 24 | 25 | model Notification { 26 | id String @id @default(uuid()) 27 | 28 | nonce String? 29 | 30 | userId String 31 | user User @relation(fields: [userId], references: [id]) 32 | acls String[] 33 | 34 | created DateTime @default(now()) 35 | title String 36 | description String 37 | actions String[] 38 | 39 | read Boolean @default(false) 40 | 41 | @@unique([userId, nonce]) 42 | } 43 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client" 6 | output = "client" 7 | moduleFormat = "esm" 8 | previewFeatures = ["fullTextSearchPostgres"] 9 | binaryTargets = ["native", "debian-openssl-3.0.x"] 10 | } 11 | 12 | /** 13 | * generator arktype { 14 | * provider = "yarn prismark" 15 | * output = "./validate" 16 | * fileName = "schema.ts" 17 | * nullish = true 18 | * } 19 | */ 20 | 21 | datasource db { 22 | provider = "postgresql" 23 | url = env("DATABASE_URL") 24 | } 25 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/helvetica/Helvetica-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/helvetica/Helvetica-Bold.woff -------------------------------------------------------------------------------- /public/fonts/helvetica/Helvetica-BoldOblique.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/helvetica/Helvetica-BoldOblique.woff -------------------------------------------------------------------------------- /public/fonts/helvetica/Helvetica-Oblique.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/helvetica/Helvetica-Oblique.woff -------------------------------------------------------------------------------- /public/fonts/helvetica/Helvetica.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/helvetica/Helvetica.woff -------------------------------------------------------------------------------- /public/fonts/helvetica/helvetica-compressed-5871d14b6903a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/helvetica/helvetica-compressed-5871d14b6903a.woff -------------------------------------------------------------------------------- /public/fonts/helvetica/helvetica-light-587ebe5a59211.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/helvetica/helvetica-light-587ebe5a59211.woff -------------------------------------------------------------------------------- /public/fonts/helvetica/helvetica-light-587ebe5a59211.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/helvetica/helvetica-light-587ebe5a59211.woff2 -------------------------------------------------------------------------------- /public/fonts/helvetica/helvetica-rounded-bold-5871d05ead8de.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/helvetica/helvetica-rounded-bold-5871d05ead8de.woff -------------------------------------------------------------------------------- /public/fonts/inter/InterVariable-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/inter/InterVariable-Italic.ttf -------------------------------------------------------------------------------- /public/fonts/inter/InterVariable.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/inter/InterVariable.ttf -------------------------------------------------------------------------------- /public/fonts/motiva/MotivaSansBlack.woff.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/motiva/MotivaSansBlack.woff.ttf -------------------------------------------------------------------------------- /public/fonts/motiva/MotivaSansBold.woff.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/motiva/MotivaSansBold.woff.ttf -------------------------------------------------------------------------------- /public/fonts/motiva/MotivaSansExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/motiva/MotivaSansExtraBold.ttf -------------------------------------------------------------------------------- /public/fonts/motiva/MotivaSansLight.woff.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/motiva/MotivaSansLight.woff.ttf -------------------------------------------------------------------------------- /public/fonts/motiva/MotivaSansMedium.woff.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/motiva/MotivaSansMedium.woff.ttf -------------------------------------------------------------------------------- /public/fonts/motiva/MotivaSansRegular.woff.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/motiva/MotivaSansRegular.woff.ttf -------------------------------------------------------------------------------- /public/fonts/motiva/MotivaSansThin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/fonts/motiva/MotivaSansThin.ttf -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/wallpapers/error-wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/wallpapers/error-wallpaper.jpg -------------------------------------------------------------------------------- /public/wallpapers/signin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drop-OSS/drop/2056871dc995a5a8ba653e74980d1ac98842f720/public/wallpapers/signin.jpg -------------------------------------------------------------------------------- /server/api/v1/admin/auth/index.get.ts: -------------------------------------------------------------------------------- 1 | import { AuthMec } from "~/prisma/client"; 2 | import aclManager from "~/server/internal/acls"; 3 | import { enabledAuthManagers } from "~/server/plugins/04.auth-init"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const allowed = await aclManager.allowSystemACL(h3, ["auth:read"]); 7 | if (!allowed) throw createError({ statusCode: 403 }); 8 | 9 | const authData = { 10 | [AuthMec.Simple]: enabledAuthManagers.Simple, 11 | [AuthMec.OpenID]: 12 | enabledAuthManagers.OpenID && 13 | enabledAuthManagers.OpenID.generateConfiguration(), 14 | }; 15 | 16 | return authData; 17 | }); 18 | -------------------------------------------------------------------------------- /server/api/v1/admin/auth/invitation/index.delete.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { throwingArktype } from "~/server/arktype"; 3 | import aclManager from "~/server/internal/acls"; 4 | import prisma from "~/server/internal/db/database"; 5 | 6 | const DeleteInvite = type({ 7 | id: "string", 8 | }).configure(throwingArktype); 9 | 10 | export default defineEventHandler<{ 11 | body: typeof DeleteInvite.infer; 12 | }>(async (h3) => { 13 | const allowed = await aclManager.allowSystemACL(h3, [ 14 | "auth:simple:invitation:delete", 15 | ]); 16 | if (!allowed) throw createError({ statusCode: 403 }); 17 | 18 | const body = await readValidatedBody(h3, DeleteInvite); 19 | 20 | await prisma.invitation.delete({ where: { id: body.id } }); 21 | return {}; 22 | }); 23 | -------------------------------------------------------------------------------- /server/api/v1/admin/auth/invitation/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const allowed = await aclManager.allowSystemACL(h3, [ 6 | "auth:simple:invitation:read", 7 | ]); 8 | if (!allowed) throw createError({ statusCode: 403 }); 9 | 10 | await runTask("cleanup:invitations"); 11 | 12 | const invitations = await prisma.invitation.findMany({}); 13 | return invitations; 14 | }); 15 | -------------------------------------------------------------------------------- /server/api/v1/admin/auth/invitation/index.post.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { throwingArktype } from "~/server/arktype"; 3 | import aclManager from "~/server/internal/acls"; 4 | import prisma from "~/server/internal/db/database"; 5 | 6 | const CreateInvite = type({ 7 | isAdmin: "boolean?", 8 | username: "string?", 9 | email: "string.email?", 10 | expires: "string.date.iso.parse", 11 | }).configure(throwingArktype); 12 | 13 | export default defineEventHandler<{ 14 | body: typeof CreateInvite.infer; 15 | }>(async (h3) => { 16 | const allowed = await aclManager.allowSystemACL(h3, [ 17 | "auth:simple:invitation:new", 18 | ]); 19 | if (!allowed) throw createError({ statusCode: 403 }); 20 | 21 | const body = await readValidatedBody(h3, CreateInvite); 22 | 23 | const invitation = await prisma.invitation.create({ 24 | data: body, 25 | }); 26 | 27 | return invitation; 28 | }); 29 | -------------------------------------------------------------------------------- /server/api/v1/admin/game/image/index.post.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const allowed = await aclManager.allowSystemACL(h3, ["game:image:new"]); 7 | if (!allowed) throw createError({ statusCode: 403 }); 8 | 9 | const form = await readMultipartFormData(h3); 10 | if (!form) 11 | throw createError({ 12 | statusCode: 400, 13 | statusMessage: "This endpoint requires multipart form data.", 14 | }); 15 | 16 | const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]); 17 | if (!uploadResult) 18 | throw createError({ 19 | statusCode: 400, 20 | statusMessage: "Failed to upload file", 21 | }); 22 | 23 | const [ids, options, pull, dump] = uploadResult; 24 | if (ids.length == 0) { 25 | dump(); 26 | throw createError({ 27 | statusCode: 400, 28 | statusMessage: "Did not upload a file", 29 | }); 30 | } 31 | 32 | const gameId = options.id; 33 | if (!gameId) 34 | throw createError({ 35 | statusCode: 400, 36 | statusMessage: "No game ID attached", 37 | }); 38 | 39 | const hasGame = (await prisma.game.count({ where: { id: gameId } })) != 0; 40 | if (!hasGame) { 41 | dump(); 42 | throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); 43 | } 44 | 45 | const result = await prisma.game.update({ 46 | where: { 47 | id: gameId, 48 | }, 49 | data: { 50 | mImageLibraryObjectIds: { 51 | push: ids, 52 | }, 53 | }, 54 | }); 55 | 56 | await pull(); 57 | return result; 58 | }); 59 | -------------------------------------------------------------------------------- /server/api/v1/admin/game/index.delete.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler<{ query: { id: string } }>(async (h3) => { 5 | const allowed = await aclManager.allowSystemACL(h3, ["game:delete"]); 6 | if (!allowed) throw createError({ statusCode: 403 }); 7 | 8 | const query = getQuery(h3); 9 | const gameId = query.id?.toString(); 10 | if (!gameId) 11 | throw createError({ 12 | statusCode: 400, 13 | statusMessage: "Missing id in query", 14 | }); 15 | 16 | await prisma.game.delete({ 17 | where: { 18 | id: gameId, 19 | }, 20 | }); 21 | 22 | return {}; 23 | }); 24 | -------------------------------------------------------------------------------- /server/api/v1/admin/game/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | import libraryManager from "~/server/internal/library"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const allowed = await aclManager.allowSystemACL(h3, ["game:read"]); 7 | if (!allowed) throw createError({ statusCode: 403 }); 8 | 9 | const query = getQuery(h3); 10 | const gameId = query.id?.toString(); 11 | if (!gameId) 12 | throw createError({ 13 | statusCode: 400, 14 | statusMessage: "Missing id in query", 15 | }); 16 | 17 | const game = await prisma.game.findUnique({ 18 | where: { 19 | id: gameId, 20 | }, 21 | include: { 22 | versions: { 23 | orderBy: { 24 | versionIndex: "asc", 25 | }, 26 | select: { 27 | versionIndex: true, 28 | versionName: true, 29 | platform: true, 30 | delta: true, 31 | }, 32 | }, 33 | }, 34 | }); 35 | 36 | if (!game || !game.libraryId) 37 | throw createError({ statusCode: 404, statusMessage: "Game ID not found" }); 38 | 39 | const unimportedVersions = await libraryManager.fetchUnimportedGameVersions( 40 | game.libraryId, 41 | game.libraryPath, 42 | ); 43 | 44 | return { game, unimportedVersions }; 45 | }); 46 | -------------------------------------------------------------------------------- /server/api/v1/admin/game/index.patch.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const allowed = await aclManager.allowSystemACL(h3, ["game:update"]); 6 | if (!allowed) throw createError({ statusCode: 403 }); 7 | 8 | const body = await readBody(h3); 9 | const id = body.id; 10 | if (!id) 11 | throw createError({ statusCode: 400, statusMessage: "Missing id in body" }); 12 | 13 | const restOfTheBody = { ...body }; 14 | delete restOfTheBody["id"]; 15 | 16 | const newObj = await prisma.game.update({ 17 | where: { 18 | id: id, 19 | }, 20 | data: restOfTheBody, 21 | // I would put a select here, but it would be based on the body, and muck up the types 22 | }); 23 | 24 | return newObj; 25 | }); 26 | -------------------------------------------------------------------------------- /server/api/v1/admin/game/version/index.delete.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { throwingArktype } from "~/server/arktype"; 3 | import aclManager from "~/server/internal/acls"; 4 | import prisma from "~/server/internal/db/database"; 5 | 6 | const DeleteVersion = type({ 7 | id: "string", 8 | versionName: "string", 9 | }).configure(throwingArktype); 10 | 11 | export default defineEventHandler<{ body: typeof DeleteVersion }>( 12 | async (h3) => { 13 | const allowed = await aclManager.allowSystemACL(h3, [ 14 | "game:version:delete", 15 | ]); 16 | if (!allowed) throw createError({ statusCode: 403 }); 17 | 18 | const body = await readValidatedBody(h3, DeleteVersion); 19 | 20 | const gameId = body.id.toString(); 21 | const version = body.versionName.toString(); 22 | 23 | await prisma.gameVersion.delete({ 24 | where: { 25 | gameId_versionName: { 26 | gameId: gameId, 27 | versionName: version, 28 | }, 29 | }, 30 | }); 31 | 32 | return {}; 33 | }, 34 | ); 35 | -------------------------------------------------------------------------------- /server/api/v1/admin/game/version/index.patch.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { throwingArktype } from "~/server/arktype"; 3 | import aclManager from "~/server/internal/acls"; 4 | import prisma from "~/server/internal/db/database"; 5 | 6 | const UpdateVersionOrder = type({ 7 | id: "string", 8 | versions: "string[]", 9 | }).configure(throwingArktype); 10 | 11 | export default defineEventHandler<{ body: typeof UpdateVersionOrder }>( 12 | async (h3) => { 13 | const allowed = await aclManager.allowSystemACL(h3, [ 14 | "game:version:update", 15 | ]); 16 | if (!allowed) throw createError({ statusCode: 403 }); 17 | 18 | const body = await readValidatedBody(h3, UpdateVersionOrder); 19 | const gameId = body.id; 20 | // We expect an array of the version names for this game 21 | const versions = body.versions; 22 | 23 | const newVersions = await prisma.$transaction( 24 | versions.map((versionName, versionIndex) => 25 | prisma.gameVersion.update({ 26 | where: { 27 | gameId_versionName: { 28 | gameId: gameId, 29 | versionName: versionName, 30 | }, 31 | }, 32 | data: { 33 | versionIndex: versionIndex, 34 | }, 35 | select: { 36 | versionIndex: true, 37 | versionName: true, 38 | platform: true, 39 | delta: true, 40 | }, 41 | }), 42 | ), 43 | ); 44 | 45 | return newVersions; 46 | }, 47 | ); 48 | -------------------------------------------------------------------------------- /server/api/v1/admin/import/game/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import libraryManager from "~/server/internal/library"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const allowed = await aclManager.allowSystemACL(h3, ["import:game:read"]); 6 | if (!allowed) throw createError({ statusCode: 403 }); 7 | 8 | const unimportedGames = await libraryManager.fetchAllUnimportedGames(); 9 | const iterableUnimportedGames = Object.entries(unimportedGames) 10 | .map(([libraryId, gameArray]) => 11 | gameArray.map((e) => ({ game: e, library: libraryId })), 12 | ) 13 | .flat(); 14 | return { unimportedGames: iterableUnimportedGames }; 15 | }); 16 | -------------------------------------------------------------------------------- /server/api/v1/admin/import/game/index.post.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { throwingArktype } from "~/server/arktype"; 3 | import aclManager from "~/server/internal/acls"; 4 | import libraryManager from "~/server/internal/library"; 5 | import metadataHandler from "~/server/internal/metadata"; 6 | 7 | const ImportGameBody = type({ 8 | library: "string", 9 | path: "string", 10 | ["metadata?"]: { 11 | id: "string", 12 | sourceId: "string", 13 | name: "string", 14 | }, 15 | }).configure(throwingArktype); 16 | 17 | export default defineEventHandler<{ body: typeof ImportGameBody.infer }>( 18 | async (h3) => { 19 | const allowed = await aclManager.allowSystemACL(h3, ["import:game:new"]); 20 | if (!allowed) throw createError({ statusCode: 403 }); 21 | 22 | const { library, path, metadata } = await readValidatedBody( 23 | h3, 24 | ImportGameBody, 25 | ); 26 | 27 | if (!path) 28 | throw createError({ 29 | statusCode: 400, 30 | statusMessage: "Path missing from body", 31 | }); 32 | 33 | const valid = await libraryManager.checkUnimportedGamePath(library, path); 34 | if (!valid) 35 | throw createError({ 36 | statusCode: 400, 37 | statusMessage: "Invalid library or game.", 38 | }); 39 | 40 | if (!metadata) { 41 | return await metadataHandler.createGameWithoutMetadata(library, path); 42 | } else { 43 | return await metadataHandler.createGame(metadata, library, path); 44 | } 45 | }, 46 | ); 47 | -------------------------------------------------------------------------------- /server/api/v1/admin/import/game/search.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import metadataHandler from "~/server/internal/metadata"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const allowed = await aclManager.allowSystemACL(h3, ["import:game:read"]); 6 | if (!allowed) throw createError({ statusCode: 403 }); 7 | 8 | const query = getQuery(h3); 9 | const search = query.q?.toString(); 10 | if (!search) 11 | throw createError({ statusCode: 400, statusMessage: "Invalid search" }); 12 | 13 | const results = await metadataHandler.search(search); 14 | 15 | if (results.length == 0) 16 | throw createError({ 17 | statusCode: 404, 18 | statusMessage: "No metadata provider returned search results.", 19 | }); 20 | 21 | return results; 22 | }); 23 | -------------------------------------------------------------------------------- /server/api/v1/admin/import/version/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | import libraryManager from "~/server/internal/library"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const allowed = await aclManager.allowSystemACL(h3, ["import:version:read"]); 7 | if (!allowed) throw createError({ statusCode: 403 }); 8 | 9 | const query = await getQuery(h3); 10 | const gameId = query.id?.toString(); 11 | if (!gameId) 12 | throw createError({ 13 | statusCode: 400, 14 | statusMessage: "Missing id in request params", 15 | }); 16 | 17 | const game = await prisma.game.findUnique({ 18 | where: { id: gameId }, 19 | select: { libraryId: true, libraryPath: true }, 20 | }); 21 | if (!game || !game.libraryId) 22 | throw createError({ statusCode: 404, statusMessage: "Game not found" }); 23 | 24 | const unimportedVersions = await libraryManager.fetchUnimportedGameVersions( 25 | game.libraryId, 26 | game.libraryPath, 27 | ); 28 | if (!unimportedVersions) 29 | throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); 30 | 31 | return unimportedVersions; 32 | }); 33 | -------------------------------------------------------------------------------- /server/api/v1/admin/import/version/preload.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import libraryManager from "~/server/internal/library"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const allowed = await aclManager.allowSystemACL(h3, ["import:version:read"]); 6 | if (!allowed) throw createError({ statusCode: 403 }); 7 | 8 | const query = await getQuery(h3); 9 | const gameId = query.id?.toString(); 10 | const versionName = query.version?.toString(); 11 | if (!gameId || !versionName) 12 | throw createError({ 13 | statusCode: 400, 14 | statusMessage: "Missing id or version in request params", 15 | }); 16 | 17 | const preload = await libraryManager.fetchUnimportedVersionInformation( 18 | gameId, 19 | versionName, 20 | ); 21 | if (!preload) 22 | throw createError({ 23 | statusCode: 400, 24 | statusMessage: "Invalid game or version id/name", 25 | }); 26 | 27 | return preload; 28 | }); 29 | -------------------------------------------------------------------------------- /server/api/v1/admin/library/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import libraryManager from "~/server/internal/library"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const allowed = await aclManager.allowSystemACL(h3, ["library:read"]); 6 | if (!allowed) throw createError({ statusCode: 403 }); 7 | 8 | const unimportedGames = await libraryManager.fetchAllUnimportedGames(); 9 | const games = await libraryManager.fetchGamesWithStatus(); 10 | 11 | // Fetch other library data here 12 | 13 | return { unimportedGames, games }; 14 | }); 15 | -------------------------------------------------------------------------------- /server/api/v1/admin/library/sources/index.delete.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { throwingArktype } from "~/server/arktype"; 3 | import aclManager from "~/server/internal/acls"; 4 | import prisma from "~/server/internal/db/database"; 5 | 6 | const DeleteLibrarySource = type({ 7 | id: "string", 8 | }).configure(throwingArktype); 9 | 10 | export default defineEventHandler<{ body: typeof DeleteLibrarySource.infer }>( 11 | async (h3) => { 12 | const allowed = await aclManager.allowSystemACL(h3, [ 13 | "library:sources:delete", 14 | ]); 15 | if (!allowed) throw createError({ statusCode: 403 }); 16 | 17 | const body = await readValidatedBody(h3, DeleteLibrarySource); 18 | 19 | return await prisma.library.delete({ 20 | where: { 21 | id: body.id, 22 | }, 23 | }); 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /server/api/v1/admin/library/sources/index.get.ts: -------------------------------------------------------------------------------- 1 | import type { Library } from "~/prisma/client"; 2 | import aclManager from "~/server/internal/acls"; 3 | import libraryManager from "~/server/internal/library"; 4 | 5 | export type WorkingLibrarySource = Library & { working: boolean }; 6 | 7 | export default defineEventHandler(async (h3) => { 8 | const allowed = await aclManager.allowSystemACL(h3, ["library:sources:read"]); 9 | if (!allowed) throw createError({ statusCode: 403 }); 10 | 11 | const sources = await libraryManager.fetchLibraries(); 12 | 13 | // Fetch other library data here 14 | 15 | return sources; 16 | }); 17 | -------------------------------------------------------------------------------- /server/api/v1/admin/news/[id]/index.delete.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, createError } from "h3"; 2 | import aclManager from "~/server/internal/acls"; 3 | import newsManager from "~/server/internal/news"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const allowed = await aclManager.allowSystemACL(h3, ["news:delete"]); 7 | if (!allowed) 8 | throw createError({ 9 | statusCode: 403, 10 | }); 11 | 12 | const id = h3.context.params?.id; 13 | if (!id) { 14 | throw createError({ 15 | statusCode: 400, 16 | message: "Missing news ID", 17 | }); 18 | } 19 | 20 | await newsManager.delete(id); 21 | return { success: true }; 22 | }); 23 | -------------------------------------------------------------------------------- /server/api/v1/admin/news/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, createError } from "h3"; 2 | import aclManager from "~/server/internal/acls"; 3 | import newsManager from "~/server/internal/news"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const allowed = await aclManager.allowSystemACL(h3, ["news:read"]); 7 | if (!allowed) 8 | throw createError({ 9 | statusCode: 403, 10 | }); 11 | 12 | const id = h3.context.params?.id; 13 | if (!id) 14 | throw createError({ 15 | statusCode: 400, 16 | message: "Missing news ID", 17 | }); 18 | 19 | const news = await newsManager.fetchById(id); 20 | if (!news) 21 | throw createError({ 22 | statusCode: 404, 23 | message: "News article not found", 24 | }); 25 | 26 | return news; 27 | }); 28 | -------------------------------------------------------------------------------- /server/api/v1/admin/news/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, getQuery } from "h3"; 2 | import aclManager from "~/server/internal/acls"; 3 | import newsManager from "~/server/internal/news"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const allowed = await aclManager.allowSystemACL(h3, ["news:read"]); 7 | if (!allowed) 8 | throw createError({ 9 | statusCode: 403, 10 | }); 11 | 12 | const query = getQuery(h3); 13 | 14 | const orderBy = query.order as "asc" | "desc"; 15 | if (orderBy) { 16 | if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy)) 17 | throw createError({ statusCode: 400, statusMessage: "Invalid order" }); 18 | } 19 | 20 | const tags = query.tags as string[] | undefined; 21 | if (tags) { 22 | if (typeof tags !== "object" || !Array.isArray(tags)) 23 | throw createError({ statusCode: 400, statusMessage: "Invalid tags" }); 24 | } 25 | 26 | const options = { 27 | take: parseInt(query.limit as string), 28 | skip: parseInt(query.skip as string), 29 | orderBy: orderBy, 30 | ...(tags && { tags: tags.map((e) => e.toString()) }), 31 | search: query.search as string, 32 | }; 33 | 34 | const news = await newsManager.fetch(options); 35 | return news; 36 | }); 37 | -------------------------------------------------------------------------------- /server/api/v1/admin/news/index.post.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, createError } from "h3"; 2 | import aclManager from "~/server/internal/acls"; 3 | import newsManager from "~/server/internal/news"; 4 | import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; 5 | 6 | export default defineEventHandler(async (h3) => { 7 | const allowed = await aclManager.allowSystemACL(h3, ["news:create"]); 8 | if (!allowed) throw createError({ statusCode: 403 }); 9 | 10 | const form = await readMultipartFormData(h3); 11 | if (!form) 12 | throw createError({ 13 | statusCode: 400, 14 | statusMessage: "This endpoint requires multipart form data.", 15 | }); 16 | 17 | const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1); 18 | if (!uploadResult) 19 | throw createError({ 20 | statusCode: 400, 21 | statusMessage: "Failed to upload file", 22 | }); 23 | 24 | const [imageIds, options, pull, _dump] = uploadResult; 25 | 26 | const title = options.title; 27 | const description = options.description; 28 | const content = options.content; 29 | const tags = options.tags ? (JSON.parse(options.tags) as string[]) : []; 30 | const imageId = imageIds.at(0); 31 | 32 | if (!title || !description || !content) 33 | throw createError({ 34 | statusCode: 400, 35 | statusMessage: "Missing or invalid title, description or content.", 36 | }); 37 | 38 | const article = await newsManager.create({ 39 | title: title, 40 | description: description, 41 | content: content, 42 | 43 | tags: tags, 44 | 45 | ...(imageId && { image: imageId }), 46 | authorId: "system", 47 | }); 48 | 49 | await pull(); 50 | 51 | return article; 52 | }); 53 | -------------------------------------------------------------------------------- /server/api/v1/admin/users/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const allowed = await aclManager.allowSystemACL(h3, ["user:read"]); 6 | if (!allowed) throw createError({ statusCode: 403 }); 7 | 8 | const userId = getRouterParam(h3, "id"); 9 | if (!userId) 10 | throw createError({ 11 | statusCode: 400, 12 | statusMessage: "No userId in route.", 13 | }); 14 | 15 | if (userId == "system") 16 | throw createError({ 17 | statusCode: 400, 18 | statusMessage: "Cannot delete system user.", 19 | }); 20 | 21 | const user = await prisma.user.findUnique({ where: { id: userId } }); 22 | if (!user) 23 | throw createError({ statusCode: 404, statusMessage: "User not found." }); 24 | 25 | return user; 26 | }); 27 | -------------------------------------------------------------------------------- /server/api/v1/admin/users/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const allowed = await aclManager.allowSystemACL(h3, ["user:read"]); 6 | if (!allowed) throw createError({ statusCode: 403 }); 7 | 8 | const users = await prisma.user.findMany({ 9 | where: { 10 | id: { not: "system" }, 11 | }, 12 | include: { 13 | authMecs: { 14 | select: { 15 | mec: true, 16 | }, 17 | }, 18 | }, 19 | }); 20 | 21 | return users; 22 | }); 23 | -------------------------------------------------------------------------------- /server/api/v1/auth/index.get.ts: -------------------------------------------------------------------------------- 1 | import { enabledAuthManagers } from "~/server/plugins/04.auth-init"; 2 | 3 | export default defineEventHandler(() => { 4 | const authManagers = Object.entries(enabledAuthManagers) 5 | .filter((e) => !!e[1]) 6 | .map((e) => e[0]); 7 | 8 | return authManagers; 9 | }); 10 | -------------------------------------------------------------------------------- /server/api/v1/auth/signup/simple.get.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/server/internal/db/database"; 2 | 3 | export default defineEventHandler(async (h3) => { 4 | const query = getQuery(h3); 5 | const id = query.id?.toString(); 6 | if (!id) 7 | throw createError({ 8 | statusCode: 400, 9 | statusMessage: "id required in fetching invitation", 10 | }); 11 | 12 | await runTask("cleanup:invitations"); 13 | 14 | const invitation = await prisma.invitation.findUnique({ where: { id: id } }); 15 | if (!invitation) 16 | throw createError({ 17 | statusCode: 404, 18 | statusMessage: "Invalid or expired invitation", 19 | }); 20 | 21 | return invitation; 22 | }); 23 | -------------------------------------------------------------------------------- /server/api/v1/client/auth/callback/index.get.ts: -------------------------------------------------------------------------------- 1 | import clientHandler from "~/server/internal/clients/handler"; 2 | import sessionHandler from "~/server/internal/session"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const user = await sessionHandler.getSession(h3); 6 | if (!user) throw createError({ statusCode: 403 }); 7 | 8 | const query = getQuery(h3); 9 | const providedClientId = query.id?.toString(); 10 | if (!providedClientId) 11 | throw createError({ 12 | statusCode: 400, 13 | statusMessage: "Provide client ID in request params as 'id'", 14 | }); 15 | 16 | const data = await clientHandler.fetchClientMetadata(providedClientId); 17 | if (!data) 18 | throw createError({ 19 | statusCode: 404, 20 | statusMessage: "Request not found.", 21 | }); 22 | 23 | await clientHandler.attachUserId(providedClientId, user.userId); 24 | 25 | return data; 26 | }); 27 | -------------------------------------------------------------------------------- /server/api/v1/client/auth/callback/index.post.ts: -------------------------------------------------------------------------------- 1 | import clientHandler from "~/server/internal/clients/handler"; 2 | import sessionHandler from "~/server/internal/session"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const user = await sessionHandler.getSession(h3); 6 | if (!user) throw createError({ statusCode: 403 }); 7 | 8 | const body = await readBody(h3); 9 | const clientId = await body.id; 10 | 11 | const data = await clientHandler.fetchClientMetadata(clientId); 12 | if (!data) 13 | throw createError({ 14 | statusCode: 400, 15 | statusMessage: "Invalid or expired client ID.", 16 | }); 17 | 18 | const token = await clientHandler.generateAuthToken(clientId); 19 | 20 | return { 21 | redirect: `drop://handshake/${clientId}/${token}`, 22 | token: `${clientId}/${token}`, 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /server/api/v1/client/auth/handshake.post.ts: -------------------------------------------------------------------------------- 1 | import clientHandler from "~/server/internal/clients/handler"; 2 | import { useCertificateAuthority } from "~/server/plugins/ca"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const body = await readBody(h3); 6 | const clientId = body.clientId; 7 | const token = body.token; 8 | if (!clientId || !token) 9 | throw createError({ 10 | statusCode: 400, 11 | statusMessage: "Missing token or client ID from body", 12 | }); 13 | 14 | const metadata = await clientHandler.fetchClient(clientId); 15 | if (!metadata) 16 | throw createError({ 17 | statusCode: 403, 18 | statusMessage: "Invalid client ID", 19 | }); 20 | if (!metadata.authToken || !metadata.userId) 21 | throw createError({ 22 | statusCode: 400, 23 | statusMessage: "Un-authorized client ID", 24 | }); 25 | if (metadata.authToken !== token) 26 | throw createError({ 27 | statusCode: 403, 28 | statusMessage: "Invalid token", 29 | }); 30 | 31 | const certificateAuthority = useCertificateAuthority(); 32 | const bundle = await certificateAuthority.generateClientCertificate( 33 | clientId, 34 | metadata.data.name, 35 | ); 36 | 37 | const client = await clientHandler.finialiseClient(clientId); 38 | await certificateAuthority.storeClientCertificate(clientId, bundle); 39 | 40 | return { 41 | private: bundle.priv, 42 | certificate: bundle.cert, 43 | id: client.id, 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /server/api/v1/client/auth/session.post.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler((_h3) => {}); 2 | -------------------------------------------------------------------------------- /server/api/v1/client/collection/[id]/entry.delete.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | 7 | const id = getRouterParam(h3, "id"); 8 | if (!id) 9 | throw createError({ 10 | statusCode: 400, 11 | statusMessage: "ID required in route params", 12 | }); 13 | 14 | const body = await readBody(h3); 15 | const gameId = body.id; 16 | if (!gameId) 17 | throw createError({ statusCode: 400, statusMessage: "Game ID required" }); 18 | 19 | const successful = await userLibraryManager.collectionRemove( 20 | gameId, 21 | id, 22 | user.id, 23 | ); 24 | if (!successful) 25 | throw createError({ 26 | statusCode: 404, 27 | statusMessage: "Collection not found", 28 | }); 29 | return {}; 30 | }); 31 | -------------------------------------------------------------------------------- /server/api/v1/client/collection/[id]/entry.post.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | 7 | const id = getRouterParam(h3, "id"); 8 | if (!id) 9 | throw createError({ 10 | statusCode: 400, 11 | statusMessage: "ID required in route params", 12 | }); 13 | 14 | const body = await readBody(h3); 15 | const gameId = body.id; 16 | if (!gameId) 17 | throw createError({ statusCode: 400, statusMessage: "Game ID required" }); 18 | 19 | return await userLibraryManager.collectionAdd(gameId, id, user.id); 20 | }); 21 | -------------------------------------------------------------------------------- /server/api/v1/client/collection/[id]/index.delete.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | 7 | const id = getRouterParam(h3, "id"); 8 | if (!id) 9 | throw createError({ 10 | statusCode: 400, 11 | statusMessage: "ID required in route params", 12 | }); 13 | 14 | // Verify collection exists and user owns it 15 | // Will not return the default collection 16 | const collection = await userLibraryManager.fetchCollection(id); 17 | if (!collection) 18 | throw createError({ 19 | statusCode: 404, 20 | statusMessage: "Collection not found", 21 | }); 22 | 23 | if (collection.userId !== user.id) 24 | throw createError({ 25 | statusCode: 403, 26 | statusMessage: "Not authorized to delete this collection", 27 | }); 28 | 29 | await userLibraryManager.deleteCollection(id); 30 | return { success: true }; 31 | }); 32 | -------------------------------------------------------------------------------- /server/api/v1/client/collection/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | 7 | const id = getRouterParam(h3, "id"); 8 | if (!id) 9 | throw createError({ 10 | statusCode: 400, 11 | statusMessage: "ID required in route params", 12 | }); 13 | 14 | // Fetch specific collection 15 | // Will not return the default collection 16 | const collection = await userLibraryManager.fetchCollection(id); 17 | if (!collection) 18 | throw createError({ 19 | statusCode: 404, 20 | statusMessage: "Collection not found", 21 | }); 22 | 23 | // Verify user owns this collection 24 | if (collection.userId !== user.id) 25 | throw createError({ 26 | statusCode: 403, 27 | statusMessage: "Not authorized to access this collection", 28 | }); 29 | 30 | return collection; 31 | }); 32 | -------------------------------------------------------------------------------- /server/api/v1/client/collection/default/entry.delete.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | 7 | const body = await readBody(h3); 8 | 9 | const gameId = body.id; 10 | if (!gameId) 11 | throw createError({ statusCode: 400, statusMessage: "Game ID required" }); 12 | 13 | await userLibraryManager.libraryRemove(gameId, user.id); 14 | return {}; 15 | }); 16 | -------------------------------------------------------------------------------- /server/api/v1/client/collection/default/entry.post.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | 7 | const body = await readBody(h3); 8 | const gameId = body.id; 9 | if (!gameId) 10 | throw createError({ statusCode: 400, statusMessage: "Game ID required" }); 11 | 12 | // Add the game to the default collection 13 | await userLibraryManager.libraryAdd(gameId, user.id); 14 | return {}; 15 | }); 16 | -------------------------------------------------------------------------------- /server/api/v1/client/collection/default/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | 7 | const collection = await userLibraryManager.fetchLibrary(user.id); 8 | 9 | return collection; 10 | }); 11 | -------------------------------------------------------------------------------- /server/api/v1/client/collection/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | 7 | const collections = await userLibraryManager.fetchCollections(user.id); 8 | return collections; 9 | }); 10 | -------------------------------------------------------------------------------- /server/api/v1/client/collection/index.post.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | 7 | const body = await readBody(h3); 8 | 9 | const name = body.name; 10 | if (!name) 11 | throw createError({ statusCode: 400, statusMessage: "Requires name" }); 12 | 13 | // Create the collection using the manager 14 | const newCollection = await userLibraryManager.collectionCreate( 15 | name, 16 | user.id, 17 | ); 18 | return newCollection; 19 | }); 20 | -------------------------------------------------------------------------------- /server/api/v1/client/game/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineClientEventHandler(async (h3) => { 5 | const id = getRouterParam(h3, "id"); 6 | if (!id) 7 | throw createError({ statusCode: 400, statusMessage: "No ID in route" }); 8 | 9 | const game = await prisma.game.findUnique({ 10 | where: { 11 | id, 12 | }, 13 | }); 14 | if (!game) 15 | throw createError({ statusCode: 404, statusMessage: "Game not found" }); 16 | 17 | return game; 18 | }); 19 | -------------------------------------------------------------------------------- /server/api/v1/client/game/manifest.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import manifestGenerator from "~/server/internal/downloads/manifest"; 3 | 4 | export default defineClientEventHandler(async (h3) => { 5 | const query = getQuery(h3); 6 | const id = query.id?.toString(); 7 | const version = query.version?.toString(); 8 | if (!id || !version) 9 | throw createError({ 10 | statusCode: 400, 11 | statusMessage: "Missing id or version in query", 12 | }); 13 | 14 | const manifest = await manifestGenerator.generateManifest(id, version); 15 | if (!manifest) 16 | throw createError({ 17 | statusCode: 400, 18 | statusMessage: "Invalid game or version, or no versions added.", 19 | }); 20 | return manifest; 21 | }); 22 | -------------------------------------------------------------------------------- /server/api/v1/client/game/version.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineClientEventHandler(async (h3) => { 5 | const query = getQuery(h3); 6 | const id = query.id?.toString(); 7 | const version = query.version?.toString(); 8 | if (!id || !version) 9 | throw createError({ 10 | statusCode: 400, 11 | statusMessage: "Missing id or version in query", 12 | }); 13 | 14 | const gameVersion = await prisma.gameVersion.findUnique({ 15 | where: { 16 | gameId_versionName: { 17 | gameId: id, 18 | versionName: version, 19 | }, 20 | }, 21 | }); 22 | 23 | if (!gameVersion) 24 | throw createError({ 25 | statusCode: 404, 26 | statusMessage: "Game version not found", 27 | }); 28 | 29 | return gameVersion; 30 | }); 31 | -------------------------------------------------------------------------------- /server/api/v1/client/game/versions.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineClientEventHandler(async (h3) => { 5 | const query = getQuery(h3); 6 | const id = query.id?.toString(); 7 | if (!id) 8 | throw createError({ 9 | statusCode: 400, 10 | statusMessage: "No ID in request query", 11 | }); 12 | 13 | const versions = await prisma.gameVersion.findMany({ 14 | where: { 15 | gameId: id, 16 | }, 17 | orderBy: { 18 | versionIndex: "desc", // Latest one first 19 | }, 20 | }); 21 | 22 | const mappedVersions = versions 23 | .map((version) => { 24 | if (!version.dropletManifest) return undefined; 25 | 26 | const newVersion = { ...version, dropletManifest: undefined }; 27 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 28 | // @ts-ignore idk why we delete an undefined object 29 | delete newVersion.dropletManifest; 30 | return { 31 | ...newVersion, 32 | }; 33 | }) 34 | .filter((e) => e); 35 | 36 | return mappedVersions; 37 | }); 38 | -------------------------------------------------------------------------------- /server/api/v1/client/news/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import newsManager from "~/server/internal/news"; 3 | 4 | export default defineClientEventHandler(async (h3) => { 5 | const id = h3.context.params?.id; 6 | if (!id) 7 | throw createError({ 8 | statusCode: 400, 9 | message: "Missing news ID", 10 | }); 11 | 12 | const news = await newsManager.fetchById(id); 13 | if (!news) 14 | throw createError({ 15 | statusCode: 404, 16 | message: "News article not found", 17 | }); 18 | 19 | return news; 20 | }); 21 | -------------------------------------------------------------------------------- /server/api/v1/client/news/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import newsManager from "~/server/internal/news"; 3 | 4 | export default defineClientEventHandler(async (h3) => { 5 | const query = getQuery(h3); 6 | 7 | const orderBy = query.order as "asc" | "desc"; 8 | if (orderBy) { 9 | if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy)) 10 | throw createError({ statusCode: 400, statusMessage: "Invalid order" }); 11 | } 12 | 13 | const tags = query.tags as string[] | undefined; 14 | if (tags) { 15 | if (typeof tags !== "object" || !Array.isArray(tags)) 16 | throw createError({ statusCode: 400, statusMessage: "Invalid tags" }); 17 | } 18 | 19 | const options = { 20 | take: parseInt(query.limit as string), 21 | skip: parseInt(query.skip as string), 22 | orderBy: orderBy, 23 | ...(tags && { tags: tags.map((e) => e.toString()) }), 24 | search: query.search as string, 25 | }; 26 | 27 | const news = await newsManager.fetch(options); 28 | return news; 29 | }); 30 | -------------------------------------------------------------------------------- /server/api/v1/client/object/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import objectHandler from "~/server/internal/objects"; 3 | 4 | export default defineClientEventHandler(async (h3, utils) => { 5 | const id = getRouterParam(h3, "id"); 6 | if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); 7 | 8 | const user = await utils.fetchUser(); 9 | 10 | const object = await objectHandler.fetchWithPermissions(id, user.id); 11 | if (!object) 12 | throw createError({ statusCode: 404, statusMessage: "Object not found" }); 13 | 14 | setHeader(h3, "Content-Type", object.mime); 15 | return object.data; 16 | }); 17 | -------------------------------------------------------------------------------- /server/api/v1/client/saves/[gameid]/[slotindex]/push.post.ts: -------------------------------------------------------------------------------- 1 | import { ClientCapabilities } from "~/prisma/client"; 2 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 3 | import prisma from "~/server/internal/db/database"; 4 | import saveManager from "~/server/internal/saves"; 5 | 6 | export default defineClientEventHandler( 7 | async (h3, { fetchClient, fetchUser }) => { 8 | const client = await fetchClient(); 9 | if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) 10 | throw createError({ 11 | statusCode: 403, 12 | statusMessage: "Capability not allowed.", 13 | }); 14 | const user = await fetchUser(); 15 | const gameId = getRouterParam(h3, "gameid"); 16 | if (!gameId) 17 | throw createError({ 18 | statusCode: 400, 19 | statusMessage: "No gameID in route params", 20 | }); 21 | 22 | const slotIndexString = getRouterParam(h3, "slotindex"); 23 | if (!slotIndexString) 24 | throw createError({ 25 | statusCode: 400, 26 | statusMessage: "No slotIndex in route params", 27 | }); 28 | const slotIndex = parseInt(slotIndexString); 29 | if (Number.isNaN(slotIndex)) 30 | throw createError({ 31 | statusCode: 400, 32 | statusMessage: "Invalid slotIndex", 33 | }); 34 | 35 | const game = await prisma.game.findUnique({ 36 | where: { id: gameId }, 37 | select: { id: true }, 38 | }); 39 | if (!game) 40 | throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); 41 | 42 | await saveManager.pushSave( 43 | gameId, 44 | user.id, 45 | slotIndex, 46 | h3.node.req, 47 | client.id, 48 | ); 49 | 50 | return; 51 | }, 52 | ); 53 | -------------------------------------------------------------------------------- /server/api/v1/client/saves/[gameid]/index.get.ts: -------------------------------------------------------------------------------- 1 | import { ClientCapabilities } from "~/prisma/client"; 2 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 3 | import prisma from "~/server/internal/db/database"; 4 | 5 | export default defineClientEventHandler( 6 | async (h3, { fetchClient, fetchUser }) => { 7 | const client = await fetchClient(); 8 | if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) 9 | throw createError({ 10 | statusCode: 403, 11 | statusMessage: "Capability not allowed.", 12 | }); 13 | const user = await fetchUser(); 14 | const gameId = getRouterParam(h3, "gameid"); 15 | if (!gameId) 16 | throw createError({ 17 | statusCode: 400, 18 | statusMessage: "No gameID in route params", 19 | }); 20 | 21 | const game = await prisma.game.findUnique({ 22 | where: { id: gameId }, 23 | select: { id: true }, 24 | }); 25 | if (!game) 26 | throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); 27 | 28 | const saves = await prisma.saveSlot.findMany({ 29 | where: { 30 | userId: user.id, 31 | gameId: gameId, 32 | }, 33 | }); 34 | 35 | return saves; 36 | }, 37 | ); 38 | -------------------------------------------------------------------------------- /server/api/v1/client/saves/index.get.ts: -------------------------------------------------------------------------------- 1 | import { ClientCapabilities } from "~/prisma/client"; 2 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 3 | import prisma from "~/server/internal/db/database"; 4 | 5 | export default defineClientEventHandler( 6 | async (h3, { fetchClient, fetchUser }) => { 7 | const client = await fetchClient(); 8 | if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) 9 | throw createError({ 10 | statusCode: 403, 11 | statusMessage: "Capability not allowed.", 12 | }); 13 | const user = await fetchUser(); 14 | 15 | const saves = await prisma.saveSlot.findMany({ 16 | where: { 17 | userId: user.id, 18 | }, 19 | }); 20 | 21 | return saves; 22 | }, 23 | ); 24 | -------------------------------------------------------------------------------- /server/api/v1/client/saves/settings.get.ts: -------------------------------------------------------------------------------- 1 | import { ClientCapabilities } from "~/prisma/client"; 2 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 3 | import { applicationSettings } from "~/server/internal/config/application-configuration"; 4 | 5 | export default defineClientEventHandler(async (_h3, { fetchClient }) => { 6 | const client = await fetchClient(); 7 | if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) 8 | throw createError({ 9 | statusCode: 403, 10 | statusMessage: "Capability not allowed.", 11 | }); 12 | 13 | const slotLimit = await applicationSettings.get("saveSlotCountLimit"); 14 | const sizeLimit = await applicationSettings.get("saveSlotSizeLimit"); 15 | const history = await applicationSettings.get("saveSlotHistoryLimit"); 16 | return { slotLimit, sizeLimit, history }; 17 | }); 18 | -------------------------------------------------------------------------------- /server/api/v1/client/user/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | 3 | export default defineClientEventHandler(async (h3, { fetchUser }) => { 4 | const user = await fetchUser(); 5 | 6 | return user; 7 | }); 8 | -------------------------------------------------------------------------------- /server/api/v1/client/user/library.get.ts: -------------------------------------------------------------------------------- 1 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineClientEventHandler(async (_h3, { fetchUser }) => { 5 | const user = await fetchUser(); 6 | const library = await userLibraryManager.fetchLibrary(user.id); 7 | return library.entries.map((e) => e.game); 8 | }); 9 | -------------------------------------------------------------------------------- /server/api/v1/client/user/webtoken.post.ts: -------------------------------------------------------------------------------- 1 | import { APITokenMode } from "~/prisma/client"; 2 | import { DateTime } from "luxon"; 3 | import type { UserACL } from "~/server/internal/acls"; 4 | import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; 5 | import prisma from "~/server/internal/db/database"; 6 | 7 | export default defineClientEventHandler( 8 | async (h3, { fetchUser, fetchClient, clientId }) => { 9 | const user = await fetchUser(); 10 | const client = await fetchClient(); 11 | 12 | const acls: UserACL = [ 13 | "read", 14 | "store:read", 15 | "collections:read", 16 | "object:read", 17 | ]; 18 | 19 | const token = await prisma.aPIToken.create({ 20 | data: { 21 | name: `${client.name} Web Access Token ${DateTime.now().toISO()}`, 22 | clientId, 23 | userId: user.id, 24 | mode: APITokenMode.Client, 25 | acls, 26 | }, 27 | }); 28 | 29 | return token.token; 30 | }, 31 | ); 32 | -------------------------------------------------------------------------------- /server/api/v1/collection/[id]/entry.delete.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["collections:remove"]); 6 | if (!userId) 7 | throw createError({ 8 | statusCode: 403, 9 | }); 10 | 11 | const id = getRouterParam(h3, "id"); 12 | if (!id) 13 | throw createError({ 14 | statusCode: 400, 15 | statusMessage: "ID required in route params", 16 | }); 17 | 18 | const body = await readBody(h3); 19 | const gameId = body.id; 20 | if (!gameId) 21 | throw createError({ statusCode: 400, statusMessage: "Game ID required" }); 22 | 23 | const successful = await userLibraryManager.collectionRemove( 24 | gameId, 25 | id, 26 | userId, 27 | ); 28 | if (!successful) 29 | throw createError({ 30 | statusCode: 404, 31 | statusMessage: "Collection not found", 32 | }); 33 | return {}; 34 | }); 35 | -------------------------------------------------------------------------------- /server/api/v1/collection/[id]/entry.post.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["collections:add"]); 6 | if (!userId) 7 | throw createError({ 8 | statusCode: 403, 9 | }); 10 | 11 | const id = getRouterParam(h3, "id"); 12 | if (!id) 13 | throw createError({ 14 | statusCode: 400, 15 | statusMessage: "ID required in route params", 16 | }); 17 | 18 | const body = await readBody(h3); 19 | const gameId = body.id; 20 | if (!gameId) 21 | throw createError({ statusCode: 400, statusMessage: "Game ID required" }); 22 | 23 | return await userLibraryManager.collectionAdd(gameId, id, userId); 24 | }); 25 | -------------------------------------------------------------------------------- /server/api/v1/collection/[id]/index.delete.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["collections:delete"]); 6 | if (!userId) 7 | throw createError({ 8 | statusCode: 403, 9 | statusMessage: "Requires authentication", 10 | }); 11 | 12 | const id = getRouterParam(h3, "id"); 13 | if (!id) 14 | throw createError({ 15 | statusCode: 400, 16 | statusMessage: "ID required in route params", 17 | }); 18 | 19 | // Verify collection exists and user owns it 20 | // Will not return the default collection 21 | const collection = await userLibraryManager.fetchCollection(id); 22 | if (!collection) 23 | throw createError({ 24 | statusCode: 404, 25 | statusMessage: "Collection not found", 26 | }); 27 | 28 | if (collection.userId !== userId) 29 | throw createError({ 30 | statusCode: 403, 31 | statusMessage: "Not authorized to delete this collection", 32 | }); 33 | 34 | await userLibraryManager.deleteCollection(id); 35 | return { success: true }; 36 | }); 37 | -------------------------------------------------------------------------------- /server/api/v1/collection/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["collections:read"]); 6 | if (!userId) 7 | throw createError({ 8 | statusCode: 403, 9 | statusMessage: "Requires authentication", 10 | }); 11 | 12 | const id = getRouterParam(h3, "id"); 13 | if (!id) 14 | throw createError({ 15 | statusCode: 400, 16 | statusMessage: "ID required in route params", 17 | }); 18 | 19 | // Fetch specific collection 20 | // Will not return the default collection 21 | const collection = await userLibraryManager.fetchCollection(id); 22 | if (!collection) 23 | throw createError({ 24 | statusCode: 404, 25 | statusMessage: "Collection not found", 26 | }); 27 | 28 | // Verify user owns this collection 29 | if (collection.userId !== userId) 30 | throw createError({ 31 | statusCode: 403, 32 | statusMessage: "Not authorized to access this collection", 33 | }); 34 | 35 | return collection; 36 | }); 37 | -------------------------------------------------------------------------------- /server/api/v1/collection/default/entry.delete.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["library:remove"]); 6 | if (!userId) 7 | throw createError({ 8 | statusCode: 403, 9 | statusMessage: "Requires authentication", 10 | }); 11 | 12 | const body = await readBody(h3); 13 | 14 | const gameId = body.id; 15 | if (!gameId) 16 | throw createError({ statusCode: 400, statusMessage: "Game ID required" }); 17 | 18 | await userLibraryManager.libraryRemove(gameId, userId); 19 | return {}; 20 | }); 21 | -------------------------------------------------------------------------------- /server/api/v1/collection/default/entry.post.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["library:add"]); 6 | if (!userId) 7 | throw createError({ 8 | statusCode: 403, 9 | statusMessage: "Requires authentication", 10 | }); 11 | 12 | const body = await readBody(h3); 13 | const gameId = body.id; 14 | if (!gameId) 15 | throw createError({ statusCode: 400, statusMessage: "Game ID required" }); 16 | 17 | // Add the game to the default collection 18 | await userLibraryManager.libraryAdd(gameId, userId); 19 | return {}; 20 | }); 21 | -------------------------------------------------------------------------------- /server/api/v1/collection/default/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["collections:read"]); 6 | if (!userId) 7 | throw createError({ 8 | statusCode: 403, 9 | statusMessage: "Requires authentication", 10 | }); 11 | 12 | const collection = await userLibraryManager.fetchLibrary(userId); 13 | 14 | return collection; 15 | }); 16 | -------------------------------------------------------------------------------- /server/api/v1/collection/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["collections:read"]); 6 | if (!userId) 7 | throw createError({ 8 | statusCode: 403, 9 | }); 10 | 11 | const collections = await userLibraryManager.fetchCollections(userId); 12 | return collections; 13 | }); 14 | -------------------------------------------------------------------------------- /server/api/v1/collection/index.post.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import userLibraryManager from "~/server/internal/userlibrary"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["collections:read"]); 6 | if (!userId) 7 | throw createError({ 8 | statusCode: 403, 9 | }); 10 | 11 | const body = await readBody(h3); 12 | 13 | const name = body.name; 14 | if (!name) 15 | throw createError({ statusCode: 400, statusMessage: "Requires name" }); 16 | 17 | // Create the collection using the manager 18 | const newCollection = await userLibraryManager.collectionCreate(name, userId); 19 | return newCollection; 20 | }); 21 | -------------------------------------------------------------------------------- /server/api/v1/games/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["store:read"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const gameId = getRouterParam(h3, "id"); 9 | if (!gameId) 10 | throw createError({ 11 | statusCode: 400, 12 | statusMessage: "Missing gameId in route params (somehow...?)", 13 | }); 14 | 15 | const game = await prisma.game.findUnique({ 16 | where: { id: gameId }, 17 | include: { 18 | versions: true, 19 | }, 20 | }); 21 | 22 | if (!game) 23 | throw createError({ statusCode: 404, statusMessage: "Game not found" }); 24 | 25 | const rating = await prisma.gameRating.aggregate({ 26 | where: { 27 | gameId: game.id, 28 | }, 29 | _avg: { 30 | mReviewRating: true, 31 | }, 32 | _sum: { 33 | mReviewCount: true, 34 | }, 35 | }); 36 | 37 | return { game, rating }; 38 | }); 39 | -------------------------------------------------------------------------------- /server/api/v1/index.get.ts: -------------------------------------------------------------------------------- 1 | import { systemConfig } from "~/server/internal/config/sys-conf"; 2 | 3 | export default defineEventHandler((_h3) => { 4 | return { 5 | appName: "Drop", 6 | version: systemConfig.getDropVersion(), 7 | ref: systemConfig.getGitRef(), 8 | }; 9 | }); 10 | -------------------------------------------------------------------------------- /server/api/v1/news/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, createError } from "h3"; 2 | import aclManager from "~/server/internal/acls"; 3 | import newsManager from "~/server/internal/news"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const userId = await aclManager.getUserIdACL(h3, ["news:read"]); 7 | if (!userId) 8 | throw createError({ 9 | statusCode: 403, 10 | statusMessage: "Requires authentication", 11 | }); 12 | 13 | const id = h3.context.params?.id; 14 | if (!id) 15 | throw createError({ 16 | statusCode: 400, 17 | message: "Missing news ID", 18 | }); 19 | 20 | const news = await newsManager.fetchById(id); 21 | if (!news) 22 | throw createError({ 23 | statusCode: 404, 24 | message: "News article not found", 25 | }); 26 | 27 | return news; 28 | }); 29 | -------------------------------------------------------------------------------- /server/api/v1/news/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, getQuery } from "h3"; 2 | import aclManager from "~/server/internal/acls"; 3 | import newsManager from "~/server/internal/news"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const userId = await aclManager.getUserIdACL(h3, ["news:read"]); 7 | if (!userId) 8 | throw createError({ 9 | statusCode: 403, 10 | statusMessage: "Requires authentication", 11 | }); 12 | 13 | const query = getQuery(h3); 14 | 15 | const orderBy = query.order as "asc" | "desc"; 16 | if (orderBy) { 17 | if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy)) 18 | throw createError({ statusCode: 400, statusMessage: "Invalid order" }); 19 | } 20 | 21 | const tags = query.tags as string[] | undefined; 22 | if (tags) { 23 | if (typeof tags !== "object" || !Array.isArray(tags)) 24 | throw createError({ statusCode: 400, statusMessage: "Invalid tags" }); 25 | } 26 | 27 | const options = { 28 | take: parseInt(query.limit as string), 29 | skip: parseInt(query.skip as string), 30 | orderBy: orderBy, 31 | ...(tags && { tags: tags.map((e) => e.toString()) }), 32 | search: query.search as string, 33 | }; 34 | 35 | const news = await newsManager.fetch(options); 36 | return news; 37 | }); 38 | -------------------------------------------------------------------------------- /server/api/v1/notifications/[id]/index.delete.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["notifications:delete"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const notificationId = getRouterParam(h3, "id"); 9 | if (!notificationId) 10 | throw createError({ 11 | statusCode: 400, 12 | statusMessage: "Missing notification ID", 13 | }); 14 | 15 | const userIds = [userId]; 16 | const hasSystemPerms = await aclManager.allowSystemACL(h3, [ 17 | "notifications:delete", 18 | ]); 19 | if (hasSystemPerms) { 20 | userIds.push("system"); 21 | } 22 | 23 | const notification = await prisma.notification.delete({ 24 | where: { 25 | id: notificationId, 26 | userId: { in: userIds }, 27 | }, 28 | }); 29 | 30 | if (!notification) 31 | throw createError({ 32 | statusCode: 400, 33 | statusMessage: "Invalid notification ID", 34 | }); 35 | 36 | return {}; 37 | }); 38 | -------------------------------------------------------------------------------- /server/api/v1/notifications/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["notifications:read"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const notificationId = getRouterParam(h3, "id"); 9 | if (!notificationId) 10 | throw createError({ 11 | statusCode: 400, 12 | statusMessage: "Missing notification ID", 13 | }); 14 | 15 | const userIds = [userId]; 16 | const hasSystemPerms = await aclManager.allowSystemACL(h3, [ 17 | "notifications:read", 18 | ]); 19 | if (hasSystemPerms) { 20 | userIds.push("system"); 21 | } 22 | 23 | const notification = await prisma.notification.findFirst({ 24 | where: { 25 | id: notificationId, 26 | userId: { in: userIds }, 27 | }, 28 | }); 29 | 30 | if (!notification) 31 | throw createError({ 32 | statusCode: 400, 33 | statusMessage: "Invalid notification ID", 34 | }); 35 | 36 | return notification; 37 | }); 38 | -------------------------------------------------------------------------------- /server/api/v1/notifications/[id]/read.post.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const notificationId = getRouterParam(h3, "id"); 9 | if (!notificationId) 10 | throw createError({ 11 | statusCode: 400, 12 | statusMessage: "Missing notification ID", 13 | }); 14 | 15 | const userIds = [userId]; 16 | const hasSystemPerms = await aclManager.allowSystemACL(h3, [ 17 | "notifications:mark", 18 | ]); 19 | if (hasSystemPerms) { 20 | userIds.push("system"); 21 | } 22 | 23 | const notification = await prisma.notification.update({ 24 | where: { 25 | id: notificationId, 26 | userId: { in: userIds }, 27 | }, 28 | data: { 29 | read: true, 30 | }, 31 | }); 32 | 33 | if (!notification) 34 | throw createError({ 35 | statusCode: 400, 36 | statusMessage: "Invalid notification ID", 37 | }); 38 | 39 | return notification; 40 | }); 41 | -------------------------------------------------------------------------------- /server/api/v1/notifications/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["notifications:read"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const acls = await aclManager.fetchAllACLs(h3); 9 | if (!acls) 10 | throw createError({ 11 | statusCode: 500, 12 | statusMessage: "Got userId but no ACLs - what?", 13 | }); 14 | 15 | const notifications = await prisma.notification.findMany({ 16 | where: { 17 | userId, 18 | acls: { 19 | hasSome: acls, 20 | }, 21 | }, 22 | orderBy: { 23 | created: "desc", // Newest first 24 | }, 25 | }); 26 | 27 | return notifications; 28 | }); 29 | -------------------------------------------------------------------------------- /server/api/v1/notifications/readall.post.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const acls = await aclManager.fetchAllACLs(h3); 9 | if (!acls) 10 | throw createError({ 11 | statusCode: 500, 12 | statusMessage: "Got userId but no ACLs - what?", 13 | }); 14 | 15 | await prisma.notification.updateMany({ 16 | where: { 17 | userId, 18 | acls: { 19 | hasSome: acls, 20 | }, 21 | }, 22 | data: { 23 | read: true, 24 | }, 25 | }); 26 | 27 | return; 28 | }); 29 | -------------------------------------------------------------------------------- /server/api/v1/notifications/ws.get.ts: -------------------------------------------------------------------------------- 1 | import notificationSystem from "~/server/internal/notifications"; 2 | import aclManager from "~/server/internal/acls"; 3 | 4 | // TODO add web socket sessions for horizontal scaling 5 | // Peer ID to user ID 6 | const socketSessions = new Map(); 7 | 8 | export default defineWebSocketHandler({ 9 | async open(peer) { 10 | const h3 = { headers: peer.request?.headers ?? new Headers() }; 11 | const userId = await aclManager.getUserIdACL(h3, ["notifications:listen"]); 12 | if (!userId) { 13 | peer.send("unauthenticated"); 14 | return; 15 | } 16 | 17 | const acls = await aclManager.fetchAllACLs(h3); 18 | if (!acls) { 19 | peer.send("unauthenticated"); 20 | return; 21 | } 22 | 23 | socketSessions.set(peer.id, userId); 24 | 25 | notificationSystem.listen(userId, acls, peer.id, (notification) => { 26 | peer.send(JSON.stringify(notification)); 27 | }); 28 | }, 29 | async close(peer, _details) { 30 | const userId = socketSessions.get(peer.id); 31 | if (!userId) { 32 | console.log(`skipping websocket close for ${peer.id}`); 33 | return; 34 | } 35 | 36 | notificationSystem.unlisten(userId, peer.id); 37 | notificationSystem.unlisten("system", peer.id); // In case we were listening as 'system' 38 | socketSessions.delete(peer.id); 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /server/api/v1/object/[id]/index.delete.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import objectHandler from "~/server/internal/objects"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const id = getRouterParam(h3, "id"); 6 | if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); 7 | 8 | const userId = await aclManager.getUserIdACL(h3, ["object:delete"]); 9 | 10 | const result = await objectHandler.deleteWithPermission(id, userId); 11 | return { success: result }; 12 | }); 13 | -------------------------------------------------------------------------------- /server/api/v1/object/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import objectHandler from "~/server/internal/objects"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const id = getRouterParam(h3, "id"); 6 | if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); 7 | 8 | const userId = await aclManager.getUserIdACL(h3, ["object:read"]); 9 | 10 | const object = await objectHandler.fetchWithPermissions(id, userId); 11 | if (!object) 12 | throw createError({ statusCode: 404, statusMessage: "Object not found" }); 13 | 14 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag 15 | const etagRequestValue = h3.headers.get("If-None-Match"); 16 | const etagActualValue = await objectHandler.fetchHash(id); 17 | if ( 18 | etagRequestValue && 19 | etagActualValue && 20 | etagActualValue === etagRequestValue 21 | ) { 22 | // would compare if etag is valid, but objects should never change 23 | setResponseStatus(h3, 304); 24 | return null; 25 | } 26 | 27 | // TODO: fix undefined etagValue 28 | setHeader(h3, "ETag", etagActualValue ?? ""); 29 | setHeader(h3, "Content-Type", object.mime); 30 | setHeader( 31 | h3, 32 | "Cache-Control", 33 | "private, max-age=31536000, s-maxage=31536000, immutable", 34 | ); 35 | return object.data; 36 | }); 37 | -------------------------------------------------------------------------------- /server/api/v1/object/[id]/index.head.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import objectHandler from "~/server/internal/objects"; 3 | 4 | // this request method is purely used by the browser to check if etag values are still valid 5 | export default defineEventHandler(async (h3) => { 6 | const id = getRouterParam(h3, "id"); 7 | if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); 8 | 9 | const userId = await aclManager.getUserIdACL(h3, ["object:read"]); 10 | 11 | const object = await objectHandler.fetchWithPermissions(id, userId); 12 | if (!object) 13 | throw createError({ statusCode: 404, statusMessage: "Object not found" }); 14 | 15 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag 16 | const etagRequestValue = h3.headers.get("If-None-Match"); 17 | const etagActualValue = await objectHandler.fetchHash(id); 18 | if (etagRequestValue !== null && etagActualValue === etagRequestValue) { 19 | // would compare if etag is valid, but objects should never change 20 | setResponseStatus(h3, 304); 21 | return null; 22 | } 23 | 24 | return null; 25 | }); 26 | -------------------------------------------------------------------------------- /server/api/v1/object/[id]/index.post.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import objectHandler from "~/server/internal/objects"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const id = getRouterParam(h3, "id"); 6 | if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); 7 | 8 | const body = await readRawBody(h3, "binary"); 9 | if (!body) 10 | throw createError({ 11 | statusCode: 400, 12 | statusMessage: "Invalid upload", 13 | }); 14 | 15 | const userId = await aclManager.getUserIdACL(h3, ["object:update"]); 16 | const buffer = Buffer.from(body); 17 | 18 | const result = await objectHandler.writeWithPermissions( 19 | id, 20 | async () => buffer, 21 | userId, 22 | ); 23 | return { success: result }; 24 | }); 25 | -------------------------------------------------------------------------------- /server/api/v1/screenshots/[id]/index.delete.ts: -------------------------------------------------------------------------------- 1 | // get a specific screenshot 2 | import aclManager from "~/server/internal/acls"; 3 | import screenshotManager from "~/server/internal/screenshots"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const userId = await aclManager.getUserIdACL(h3, ["screenshots:delete"]); 7 | if (!userId) throw createError({ statusCode: 403 }); 8 | 9 | const screenshotId = getRouterParam(h3, "id"); 10 | if (!screenshotId) 11 | throw createError({ 12 | statusCode: 400, 13 | statusMessage: "Missing screenshot ID", 14 | }); 15 | 16 | const result = await screenshotManager.get(screenshotId); 17 | if (!result) 18 | throw createError({ 19 | statusCode: 404, 20 | }); 21 | if (result.userId !== userId) 22 | throw createError({ 23 | statusCode: 404, 24 | }); 25 | 26 | await screenshotManager.delete(screenshotId); 27 | }); 28 | -------------------------------------------------------------------------------- /server/api/v1/screenshots/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | // get a specific screenshot 2 | import aclManager from "~/server/internal/acls"; 3 | import screenshotManager from "~/server/internal/screenshots"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const userId = await aclManager.getUserIdACL(h3, ["screenshots:read"]); 7 | if (!userId) throw createError({ statusCode: 403 }); 8 | 9 | const screenshotId = getRouterParam(h3, "id"); 10 | if (!screenshotId) 11 | throw createError({ 12 | statusCode: 400, 13 | statusMessage: "Missing screenshot ID", 14 | }); 15 | 16 | const result = await screenshotManager.get(screenshotId); 17 | if (!result) 18 | throw createError({ 19 | statusCode: 404, 20 | }); 21 | if (result.userId !== userId) 22 | throw createError({ 23 | statusCode: 404, 24 | }); 25 | return result; 26 | }); 27 | -------------------------------------------------------------------------------- /server/api/v1/screenshots/game/[id]/index.get.ts: -------------------------------------------------------------------------------- 1 | // get all user screenshots by game 2 | import aclManager from "~/server/internal/acls"; 3 | import screenshotManager from "~/server/internal/screenshots"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const userId = await aclManager.getUserIdACL(h3, ["screenshots:read"]); 7 | if (!userId) throw createError({ statusCode: 403 }); 8 | 9 | const gameId = getRouterParam(h3, "id"); 10 | if (!gameId) 11 | throw createError({ 12 | statusCode: 400, 13 | statusMessage: "Missing game ID", 14 | }); 15 | 16 | const results = await screenshotManager.getUserAllByGame(userId, gameId); 17 | return results; 18 | }); 19 | -------------------------------------------------------------------------------- /server/api/v1/screenshots/game/[id]/index.post.ts: -------------------------------------------------------------------------------- 1 | // create new screenshot 2 | import aclManager from "~/server/internal/acls"; 3 | import prisma from "~/server/internal/db/database"; 4 | import screenshotManager from "~/server/internal/screenshots"; 5 | 6 | // TODO: make defineClientEventHandler instead? 7 | // only clients will be upload screenshots yea?? 8 | export default defineEventHandler(async (h3) => { 9 | const userId = await aclManager.getUserIdACL(h3, ["screenshots:new"]); 10 | if (!userId) throw createError({ statusCode: 403 }); 11 | 12 | const gameId = getRouterParam(h3, "id"); 13 | if (!gameId) 14 | throw createError({ 15 | statusCode: 400, 16 | statusMessage: "Missing game ID", 17 | }); 18 | 19 | const game = await prisma.game.findUnique({ 20 | where: { id: gameId }, 21 | select: { id: true }, 22 | }); 23 | if (!game) 24 | throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); 25 | 26 | await screenshotManager.upload(userId, gameId, h3.node.req); 27 | }); 28 | -------------------------------------------------------------------------------- /server/api/v1/screenshots/index.get.ts: -------------------------------------------------------------------------------- 1 | // get all user screenshots 2 | import aclManager from "~/server/internal/acls"; 3 | import screenshotManager from "~/server/internal/screenshots"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const userId = await aclManager.getUserIdACL(h3, ["screenshots:read"]); 7 | if (!userId) throw createError({ statusCode: 403 }); 8 | 9 | const results = await screenshotManager.getUserAll(userId); 10 | return results; 11 | }); 12 | -------------------------------------------------------------------------------- /server/api/v1/store/recent.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserACL(h3, ["store:read"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const games = await prisma.game.findMany({ 9 | select: { 10 | id: true, 11 | mName: true, 12 | mShortDescription: true, 13 | mCoverObjectId: true, 14 | mBannerObjectId: true, 15 | developers: { 16 | select: { 17 | id: true, 18 | mName: true, 19 | }, 20 | }, 21 | publishers: { 22 | select: { 23 | id: true, 24 | mName: true, 25 | }, 26 | }, 27 | }, 28 | orderBy: { 29 | created: "desc", 30 | }, 31 | take: 8, 32 | }); 33 | 34 | return games; 35 | }); 36 | -------------------------------------------------------------------------------- /server/api/v1/store/released.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserACL(h3, ["store:read"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const games = await prisma.game.findMany({ 9 | orderBy: { 10 | mReleased: "desc", 11 | }, 12 | take: 12, 13 | }); 14 | 15 | return games; 16 | }); 17 | -------------------------------------------------------------------------------- /server/api/v1/store/updated.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserACL(h3, ["store:read"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const versions = await prisma.gameVersion.findMany({ 9 | where: { 10 | versionIndex: { 11 | gte: 1, 12 | }, 13 | }, 14 | select: { 15 | game: true, 16 | }, 17 | orderBy: { 18 | created: "desc", 19 | }, 20 | take: 12, 21 | }); 22 | 23 | const games = versions 24 | .map((e) => e.game) 25 | .filter((v, i, a) => a.findIndex((e) => e.id === v.id) === i); 26 | 27 | return games; 28 | }); 29 | -------------------------------------------------------------------------------- /server/api/v1/task/index.get.ts: -------------------------------------------------------------------------------- 1 | import taskHandler from "~/server/internal/tasks"; 2 | import type { MinimumRequestObject } from "~/server/h3"; 3 | 4 | // TODO add web socket sessions for horizontal scaling 5 | // ID to admin 6 | const socketHeaders = new Map(); 7 | 8 | export default defineWebSocketHandler({ 9 | async open(peer) { 10 | const request = peer.request; 11 | if (!request) { 12 | peer.send("unauthenticated"); 13 | return; 14 | } 15 | 16 | socketHeaders.set(peer.id, { 17 | headers: request.headers ?? new Headers(), 18 | }); 19 | peer.send(`connect`); 20 | }, 21 | message(peer, message) { 22 | if (!peer.id) return; 23 | const headers = socketHeaders.get(peer.id); 24 | if (headers === undefined) return; 25 | const text = message.text(); 26 | if (text.startsWith("connect/")) { 27 | const id = text.substring("connect/".length); 28 | taskHandler.connect(peer.id, id, peer, headers); 29 | return; 30 | } 31 | }, 32 | close(peer, _details) { 33 | if (!peer.id) return; 34 | if (!socketHeaders.has(peer.id)) return; 35 | socketHeaders.delete(peer.id); 36 | 37 | taskHandler.disconnectAll(peer.id); 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /server/api/v1/user/client/[id]/index.delete.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import clientHandler from "~/server/internal/clients/handler"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["clients:revoke"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const clientId = getRouterParam(h3, "id"); 9 | if (!clientId) 10 | throw createError({ 11 | statusCode: 400, 12 | statusMessage: "Client ID missing in route params", 13 | }); 14 | 15 | await clientHandler.removeClient(clientId); 16 | }); 17 | -------------------------------------------------------------------------------- /server/api/v1/user/client/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import prisma from "~/server/internal/db/database"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, ["clients:read"]); 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | const clients = await prisma.client.findMany({ 9 | where: { 10 | userId, 11 | }, 12 | }); 13 | 14 | return clients; 15 | }); 16 | -------------------------------------------------------------------------------- /server/api/v1/user/index.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | 3 | export default defineEventHandler(async (h3) => { 4 | const user = await aclManager.getUserACL(h3, ["read"]); 5 | return user ?? null; // Need to specifically return null 6 | }); 7 | -------------------------------------------------------------------------------- /server/api/v1/user/token/[id]/index.delete.ts: -------------------------------------------------------------------------------- 1 | import { APITokenMode } from "~/prisma/client"; 2 | import aclManager from "~/server/internal/acls"; 3 | import prisma from "~/server/internal/db/database"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication 7 | if (!userId) throw createError({ statusCode: 403 }); 8 | 9 | const id = h3.context.params?.id; 10 | if (!id) 11 | throw createError({ 12 | statusCode: 400, 13 | statusMessage: "No id in router params", 14 | }); 15 | 16 | const deleted = await prisma.aPIToken.delete({ 17 | where: { id: id, userId: userId, mode: APITokenMode.User }, 18 | })!; 19 | if (!deleted) 20 | throw createError({ statusCode: 404, statusMessage: "Token not found" }); 21 | 22 | return; 23 | }); 24 | -------------------------------------------------------------------------------- /server/api/v1/user/token/acls.get.ts: -------------------------------------------------------------------------------- 1 | import aclManager from "~/server/internal/acls"; 2 | import { userACLDescriptions } from "~/server/internal/acls/descriptions"; 3 | 4 | export default defineEventHandler(async (h3) => { 5 | const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication 6 | if (!userId) throw createError({ statusCode: 403 }); 7 | 8 | return userACLDescriptions; 9 | }); 10 | -------------------------------------------------------------------------------- /server/api/v1/user/token/index.get.ts: -------------------------------------------------------------------------------- 1 | import { APITokenMode } from "~/prisma/client"; 2 | import aclManager from "~/server/internal/acls"; 3 | import prisma from "~/server/internal/db/database"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication 7 | if (!userId) throw createError({ statusCode: 403 }); 8 | 9 | const tokens = await prisma.aPIToken.findMany({ 10 | where: { userId: userId, mode: APITokenMode.User }, 11 | omit: { token: true }, 12 | }); 13 | 14 | return tokens; 15 | }); 16 | -------------------------------------------------------------------------------- /server/api/v1/user/token/index.post.ts: -------------------------------------------------------------------------------- 1 | import { APITokenMode } from "~/prisma/client"; 2 | import aclManager, { userACLs } from "~/server/internal/acls"; 3 | import prisma from "~/server/internal/db/database"; 4 | 5 | export default defineEventHandler(async (h3) => { 6 | const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication 7 | if (!userId) throw createError({ statusCode: 403 }); 8 | 9 | const body = await readBody(h3); 10 | const name: string = body.name; 11 | const acls: string[] = body.acls; 12 | 13 | if (!name || typeof name !== "string") 14 | throw createError({ 15 | statusCode: 400, 16 | statusMessage: "Token name required", 17 | }); 18 | if (!acls || !Array.isArray(acls)) 19 | throw createError({ statusCode: 400, statusMessage: "ACLs required" }); 20 | 21 | if (acls.length == 0) 22 | throw createError({ 23 | statusCode: 400, 24 | statusMessage: "Token requires more than zero ACLs", 25 | }); 26 | 27 | const invalidACLs = acls.filter( 28 | (e) => userACLs.findIndex((v) => e == v) == -1, 29 | ); 30 | if (invalidACLs.length > 0) 31 | throw createError({ 32 | statusCode: 400, 33 | statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`, 34 | }); 35 | 36 | const token = await prisma.aPIToken.create({ 37 | data: { 38 | mode: APITokenMode.User, 39 | name: name, 40 | userId: userId, 41 | acls: acls, 42 | }, 43 | }); 44 | 45 | return token; 46 | }); 47 | -------------------------------------------------------------------------------- /server/arktype.ts: -------------------------------------------------------------------------------- 1 | import { configure } from "arktype/config"; 2 | 3 | export const throwingArktype = configure({ 4 | onFail: (errors) => errors.throw(), 5 | actual: () => "", 6 | }); 7 | 8 | // be sure to specify both the runtime and static configs 9 | 10 | declare global { 11 | interface ArkEnv { 12 | onFail: typeof throwingArktype.onFail; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/h3.d.ts: -------------------------------------------------------------------------------- 1 | export type MinimumRequestObject = { headers: Headers }; 2 | 3 | export type TaskReturn = 4 | | { success: true; data: T; error?: never } 5 | | { success: false; data?: never; error: { message: string } }; 6 | -------------------------------------------------------------------------------- /server/internal/cache/cacheHandler.ts: -------------------------------------------------------------------------------- 1 | import { prefixStorage, type StorageValue, type Storage } from "unstorage"; 2 | 3 | /** 4 | * Creates and manages the lifecycles of various caches 5 | */ 6 | export class CacheHandler { 7 | private caches = new Map>(); 8 | 9 | /** 10 | * Create a new cache 11 | * @param name 12 | * @returns 13 | */ 14 | createCache(name: string) { 15 | // will allow us to dynamicing use redis in the future just by changing the storage used 16 | const provider = prefixStorage(useStorage("appCache"), name); 17 | // hack to let ts have us store cache 18 | this.caches.set(name, provider as unknown as Storage); 19 | return provider; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/internal/cache/index.ts: -------------------------------------------------------------------------------- 1 | import { CacheHandler } from "./cacheHandler"; 2 | 3 | export const cacheHandler = new CacheHandler(); 4 | export default cacheHandler; 5 | -------------------------------------------------------------------------------- /server/internal/clients/README.md: -------------------------------------------------------------------------------- 1 | # Client Handshake process 2 | 3 | Drop clients need to complete a handshake in order to connect to a Drop server. It also trades certificates for encrypted P2P connections. 4 | 5 | ## 1. Client requests a handshake 6 | 7 | Client makes request: `POST /api/v1/client/auth/initiate` with information about the client. 8 | 9 | Server responds with a URL to send the user to. It generates a device ID, which has all the metadata attached. 10 | 11 | ## 2. User signs in 12 | 13 | Client sends user to the provided URL (in external browser). User signs in using the existing authentication stack. 14 | 15 | Server sends redirect to `drop://handshake/[id]/[token]`, where the token is an authentication token to generate the necessary certificates, and the ID is the client ID as generated by the server. 16 | 17 | ## 3. Client requests certificates 18 | 19 | Client makes request: `POST /api/v1/client/auth/handshake` with the token recieved in the previous step. 20 | 21 | The server uses it's CA to generate a public-private key pair, the CN of the client ID. It then sends that pair, plus the CA's public key, to the client, which stores it all. 22 | 23 | _The certificate lasts for a year, and is rotated when it has 3 months or less left on it's expiry._ 24 | 25 | ## 4.a Client requests one-time device endpoint 26 | 27 | The client uses a millisecond UNIX timestamp and signs it with their private key. This is then attached to any device-related request. It has 30 seconds to make the request before the nonce becomes invalid (this is to prevent credential stealing & reusing). 28 | 29 | ## 4.b Client wants a long-lived session 30 | 31 | The client does the same as above, but instead makes the request to `POST /api/v1/client/auth/session`, which generates a session token that lasts for a day. This can then be used in the request to provide authentication. 32 | -------------------------------------------------------------------------------- /server/internal/config/sys-conf.ts: -------------------------------------------------------------------------------- 1 | class SystemConfig { 2 | private libraryFolder = process.env.LIBRARY ?? "./.data/library"; 3 | private dataFolder = process.env.DATA ?? "./.data/data"; 4 | 5 | private dropVersion; 6 | private gitRef; 7 | 8 | private checkForUpdates = 9 | process.env.CHECK_FOR_UPDATES !== undefined && 10 | process.env.CHECK_FOR_UPDATES.toLocaleLowerCase() === "true" 11 | ? true 12 | : false; 13 | 14 | constructor() { 15 | // get drop version and git ref from nuxt config 16 | const config = useRuntimeConfig(); 17 | this.dropVersion = config.dropVersion; 18 | this.gitRef = config.gitRef; 19 | } 20 | 21 | getLibraryFolder() { 22 | return this.libraryFolder; 23 | } 24 | 25 | getDataFolder() { 26 | return this.dataFolder; 27 | } 28 | 29 | getDropVersion() { 30 | return this.dropVersion; 31 | } 32 | 33 | getGitRef() { 34 | return this.gitRef; 35 | } 36 | 37 | shouldCheckForUpdates() { 38 | return this.checkForUpdates; 39 | } 40 | } 41 | 42 | export const systemConfig = new SystemConfig(); 43 | -------------------------------------------------------------------------------- /server/internal/consts.ts: -------------------------------------------------------------------------------- 1 | export const DROP_VERSION = "0.3.0"; 2 | -------------------------------------------------------------------------------- /server/internal/db/database.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "~/prisma/client"; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient({}); 5 | }; 6 | 7 | declare const globalThis: { 8 | prismaGlobal: ReturnType; 9 | } & typeof global; 10 | 11 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); 12 | 13 | export default prisma; 14 | 15 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma; 16 | -------------------------------------------------------------------------------- /server/internal/downloads/coordinator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | The download co-ordinator's job is to keep track of all the currently online clients. 3 | 4 | When a client signs on and registers itself as a peer 5 | 6 | */ 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class, @typescript-eslint/no-unused-vars 9 | class DownloadCoordinator {} 10 | -------------------------------------------------------------------------------- /server/internal/library/README.md: -------------------------------------------------------------------------------- 1 | # Library Format 2 | 3 | Drop uses a filesystem-based library format, as it targets homelabs and not enterprise-grade solutions. The format works as follows: 4 | 5 | ## /{game name} 6 | 7 | The game name is only used for initial matching, and doesn't affect actual metadata. Metadata is linked to the game's database entry, which is linked to it's filesystem name (they, however, can be completely different). 8 | 9 | ## /{game name}/{version name} 10 | 11 | The version name can be anything. Versions have to manually imported within the web UI. There, you can change the order of the updates and mark them as deltas. Delta updates apply files over the previous versions. 12 | -------------------------------------------------------------------------------- /server/internal/metadata/manual.ts: -------------------------------------------------------------------------------- 1 | import { MetadataSource } from "~/prisma/client"; 2 | import type { MetadataProvider } from "."; 3 | import type { 4 | _FetchGameMetadataParams, 5 | GameMetadata, 6 | _FetchCompanyMetadataParams, 7 | CompanyMetadata, 8 | } from "./types"; 9 | import * as jdenticon from "jdenticon"; 10 | 11 | export class ManualMetadataProvider implements MetadataProvider { 12 | name() { 13 | return "Manual"; 14 | } 15 | source() { 16 | return MetadataSource.Manual; 17 | } 18 | async search(_query: string) { 19 | return []; 20 | } 21 | async fetchGame({ 22 | name, 23 | createObject, 24 | }: _FetchGameMetadataParams): Promise { 25 | const icon = jdenticon.toPng(name, 512); 26 | const iconId = createObject(icon); 27 | 28 | return { 29 | id: "manual", 30 | name, 31 | shortDescription: "Default description.", 32 | description: "# Default description.", 33 | released: new Date(), 34 | publishers: [], 35 | developers: [], 36 | tags: [], 37 | reviews: [], 38 | 39 | icon: iconId, 40 | coverId: iconId, 41 | bannerId: iconId, 42 | images: [iconId], 43 | }; 44 | } 45 | async fetchCompany( 46 | _params: _FetchCompanyMetadataParams, 47 | ): Promise { 48 | throw new Error("Method not implemented."); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/internal/objects/index.ts: -------------------------------------------------------------------------------- 1 | import { FsObjectBackend } from "./fsBackend"; 2 | import { ObjectHandler } from "./objectHandler"; 3 | 4 | export const objectHandler = new ObjectHandler(new FsObjectBackend()); 5 | export default objectHandler; 6 | -------------------------------------------------------------------------------- /server/internal/security/simple.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcryptjs"; 2 | import * as argon2 from "argon2"; 3 | 4 | export async function checkHashBcrypt(password: string, hash: string) { 5 | return await bcrypt.compare(password, hash); 6 | } 7 | 8 | export async function createHashArgon2(password: string) { 9 | return await argon2.hash(password); 10 | } 11 | 12 | export async function checkHashArgon2(password: string, hash: string) { 13 | return await argon2.verify(hash, password); 14 | } 15 | -------------------------------------------------------------------------------- /server/internal/session/cache.ts: -------------------------------------------------------------------------------- 1 | import cacheHandler from "../cache"; 2 | import type { Session, SessionProvider } from "./types"; 3 | 4 | /** 5 | * DO NOT USE THIS. THE CACHE EVICTS SESSIONS. 6 | * 7 | * This needs work. TODO. 8 | */ 9 | export default function createCacheSessionProvider() { 10 | const sessions = cacheHandler.createCache("cacheSessionProvider"); 11 | 12 | const memoryProvider: SessionProvider = { 13 | async setSession(token, data) { 14 | await sessions.set(token, data); 15 | return true; 16 | }, 17 | async getSession(token: string): Promise { 18 | const session = await sessions.get(token); 19 | return session ? (session as T) : undefined; // Ensure undefined is returned if session is not found 20 | }, 21 | async updateSession(token, data) { 22 | return await this.setSession(token, data); 23 | }, 24 | async removeSession(token) { 25 | await sessions.remove(token); 26 | return true; 27 | }, 28 | async cleanupSessions() { 29 | const now = new Date(); 30 | for (const token of await sessions.getKeys()) { 31 | const session = await sessions.get(token); 32 | if (!session) continue; 33 | // if expires at time is before now, the session is expired 34 | if (session.expiresAt < now) await this.removeSession(token); 35 | } 36 | }, 37 | }; 38 | 39 | return memoryProvider; 40 | } 41 | -------------------------------------------------------------------------------- /server/internal/session/memory.ts: -------------------------------------------------------------------------------- 1 | import type { Session, SessionProvider } from "./types"; 2 | 3 | export default function createMemorySessionHandler() { 4 | const sessions = new Map(); 5 | 6 | const memoryProvider: SessionProvider = { 7 | async setSession(token, data) { 8 | sessions.set(token, data); 9 | return true; 10 | }, 11 | async getSession(token: string): Promise { 12 | const session = sessions.get(token); 13 | return session ? (session as T) : undefined; // Ensure undefined is returned if session is not found 14 | }, 15 | async updateSession(token, data) { 16 | return this.setSession(token, data); 17 | }, 18 | async removeSession(token) { 19 | sessions.delete(token); 20 | return true; 21 | }, 22 | async cleanupSessions() { 23 | const now = new Date(); 24 | for (const [token, session] of sessions) { 25 | // if expires at time is before now, the session is expired 26 | if (session.expiresAt < now) await this.removeSession(token); 27 | } 28 | }, 29 | }; 30 | 31 | return memoryProvider; 32 | } 33 | -------------------------------------------------------------------------------- /server/internal/session/types.d.ts: -------------------------------------------------------------------------------- 1 | export type Session = { 2 | userId: string; 3 | expiresAt: Date; 4 | data: { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | [key: string]: any; 7 | }; 8 | }; 9 | 10 | export interface SessionProvider { 11 | getSession: (token: string) => Promise; 12 | setSession: (token: string, data: Session) => Promise; 13 | updateSession: (token: string, data: Session) => Promise; 14 | removeSession: (token: string) => Promise; 15 | cleanupSessions: () => Promise; 16 | } 17 | -------------------------------------------------------------------------------- /server/internal/utils/handlefileupload.ts: -------------------------------------------------------------------------------- 1 | import type { EventHandlerRequest, H3Event } from "h3"; 2 | import type { Dump, Pull } from "../objects/transactional"; 3 | import { ObjectTransactionalHandler } from "../objects/transactional"; 4 | 5 | export async function handleFileUpload( 6 | h3: H3Event, 7 | metadata: { [key: string]: string }, 8 | permissions: Array, 9 | max = -1, 10 | ): Promise<[string[], { [key: string]: string }, Pull, Dump] | undefined> { 11 | const formData = await readMultipartFormData(h3); 12 | if (!formData) return undefined; 13 | const transactionalHandler = new ObjectTransactionalHandler(); 14 | const [add, pull, dump] = transactionalHandler.new(metadata, permissions); 15 | const options: { [key: string]: string } = {}; 16 | const ids = []; 17 | 18 | for (const entry of formData) { 19 | if (entry.filename) { 20 | if (max > 0 && ids.length >= max) continue; 21 | 22 | // Add file to transaction handler so we can void it later if we error out 23 | ids.push(add(entry.data)); 24 | continue; 25 | } 26 | if (!entry.name) continue; 27 | 28 | options[entry.name] = entry.data.toString("utf-8"); 29 | } 30 | 31 | return [ids, options, pull, dump]; 32 | } 33 | -------------------------------------------------------------------------------- /server/internal/utils/parseplatform.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "~/prisma/client"; 2 | 3 | export function parsePlatform(platform: string) { 4 | switch (platform.toLowerCase()) { 5 | case "linux": 6 | return Platform.Linux; 7 | case "windows": 8 | return Platform.Windows; 9 | case "mac": 10 | case "macos": 11 | return Platform.macOS; 12 | } 13 | 14 | return undefined; 15 | } 16 | -------------------------------------------------------------------------------- /server/internal/utils/recursivedirs.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | export function recursivelyReaddir(dir: string, depth: number = 100) { 5 | if (depth == 0) return []; 6 | const result: Array = []; 7 | const files = fs.readdirSync(dir); 8 | for (const file of files) { 9 | const targetDir = path.join(dir, file); 10 | const stat = fs.lstatSync(targetDir); 11 | if (stat.isDirectory()) { 12 | const subdirs = recursivelyReaddir(targetDir, depth - 1); 13 | result.push(...subdirs); 14 | continue; 15 | } 16 | result.push(targetDir); 17 | } 18 | 19 | return result; 20 | } 21 | -------------------------------------------------------------------------------- /server/internal/utils/types.d.ts: -------------------------------------------------------------------------------- 1 | export type FilterConditionally = Pick< 2 | Source, 3 | { [K in keyof Source]: Source[K] extends Condition ? K : never }[keyof Source] 4 | >; 5 | export type KeyOfType = keyof { 6 | // TODO: should switch to unknown?? 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | [P in keyof T as T[P] extends V ? P : never]: any; 9 | }; 10 | 11 | type EnumDictionary = { 12 | [K in T]: U; 13 | }; 14 | -------------------------------------------------------------------------------- /server/plugins/01.system-init.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/server/internal/db/database"; 2 | 3 | export default defineNitroPlugin(async (_nitro) => { 4 | // Ensure system user exists 5 | // The system user owns any user-based code 6 | // that we want to re-use for the app 7 | // e.g. notifications 8 | await prisma.user.upsert({ 9 | where: { 10 | id: "system", 11 | }, 12 | create: { 13 | id: "system", 14 | admin: true, 15 | 16 | displayName: "System", 17 | username: "system", 18 | email: "system@drop", 19 | profilePictureObjectId: "", 20 | }, 21 | update: { 22 | admin: true, 23 | authMecs: { set: [] }, 24 | clients: { set: [] }, 25 | }, 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /server/plugins/02.setup-admin.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/server/internal/db/database"; 2 | 3 | export default defineNitroPlugin(async (_nitro) => { 4 | const userCount = await prisma.user.count({ 5 | where: { id: { not: "system" } }, 6 | }); 7 | if (userCount != 0) return; 8 | 9 | // This setup runs every time the server sets up, 10 | // but has not been configured 11 | // so it should be in-place 12 | 13 | // Create admin invitation 14 | await prisma.invitation.upsert({ 15 | where: { 16 | id: "admin", 17 | }, 18 | create: { 19 | id: "admin", 20 | isAdmin: true, 21 | expires: new Date("4096-01-01"), 22 | }, 23 | update: { 24 | isAdmin: true, 25 | }, 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /server/plugins/04.auth-init.ts: -------------------------------------------------------------------------------- 1 | import { AuthMec } from "~/prisma/client"; 2 | import { OIDCManager } from "../internal/oidc"; 3 | 4 | export const enabledAuthManagers: { 5 | [AuthMec.Simple]: boolean; 6 | [AuthMec.OpenID]: OIDCManager | undefined; 7 | } = { 8 | [AuthMec.Simple]: false, 9 | [AuthMec.OpenID]: undefined, 10 | }; 11 | 12 | const initFunctions: { 13 | [K in keyof typeof enabledAuthManagers]: () => Promise; 14 | } = { 15 | [AuthMec.OpenID]: OIDCManager.prototype.create, 16 | [AuthMec.Simple]: async () => { 17 | const disabled = process.env.DISABLE_SIMPLE_AUTH as string | undefined; 18 | return !disabled; 19 | }, 20 | }; 21 | 22 | export default defineNitroPlugin(async () => { 23 | for (const [key, init] of Object.entries(initFunctions)) { 24 | try { 25 | const object = await init(); 26 | if (!object) break; 27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 28 | (enabledAuthManagers as any)[key] = object; 29 | console.log(`enabled auth: ${key}`); 30 | } catch (e) { 31 | console.warn(e); 32 | } 33 | } 34 | 35 | // Add every other auth mechanism here, and fall back to simple if none of them are enabled 36 | if (!enabledAuthManagers[AuthMec.OpenID]) { 37 | enabledAuthManagers[AuthMec.Simple] = true; 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /server/plugins/ca.ts: -------------------------------------------------------------------------------- 1 | import { CertificateAuthority } from "../internal/clients/ca"; 2 | import { dbCertificateStore } from "../internal/clients/ca-store"; 3 | 4 | let ca: CertificateAuthority | undefined; 5 | 6 | export const useCertificateAuthority = () => { 7 | if (!ca) throw new Error("CA not initialised"); 8 | return ca; 9 | }; 10 | 11 | export default defineNitroPlugin(async () => { 12 | // const store = fsCertificateStore(); 13 | 14 | ca = await CertificateAuthority.new(dbCertificateStore()); 15 | }); 16 | -------------------------------------------------------------------------------- /server/plugins/redirect.ts: -------------------------------------------------------------------------------- 1 | import { H3Error } from "h3"; 2 | import sessionHandler from "../internal/session"; 3 | 4 | export default defineNitroPlugin((nitro) => { 5 | nitro.hooks.hook("error", async (error, { event }) => { 6 | if (!event) return; 7 | 8 | // Don't handle for API routes 9 | if (event.path.startsWith("/api")) return; 10 | if (event.path.startsWith("/auth")) return; 11 | 12 | // Make sure it's a web error 13 | if (!(error instanceof H3Error)) return; 14 | 15 | switch (error.statusCode) { 16 | case 401: 17 | case 403: { 18 | const user = await sessionHandler.getSession(event); 19 | if (user) break; 20 | return sendRedirect( 21 | event, 22 | `/auth/signin?redirect=${encodeURIComponent(event.path)}`, 23 | ); 24 | } 25 | } 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /server/plugins/tasks.ts: -------------------------------------------------------------------------------- 1 | export default defineNitroPlugin(async (_nitro) => { 2 | // all tasks we should run on server boot 3 | await Promise.all([ 4 | runTask("cleanup:invitations"), 5 | runTask("cleanup:sessions"), 6 | // TODO: maybe implement some sort of rate limit thing to prevent this from calling github api a bunch in the event of crashloop or whatever? 7 | // probably will require custom task scheduler for object cleanup anyway, so something to thing about 8 | runTask("check:update"), 9 | ]); 10 | }); 11 | -------------------------------------------------------------------------------- /server/routes/auth/callback/oidc.get.ts: -------------------------------------------------------------------------------- 1 | import sessionHandler from "~/server/internal/session"; 2 | import { enabledAuthManagers } from "~/server/plugins/04.auth-init"; 3 | 4 | defineRouteMeta({ 5 | openAPI: { 6 | tags: ["Auth"], 7 | description: "OIDC Signin callback", 8 | parameters: [], 9 | }, 10 | }); 11 | 12 | export default defineEventHandler(async (h3) => { 13 | if (!enabledAuthManagers.OpenID) return sendRedirect(h3, "/auth/signin"); 14 | 15 | const manager = enabledAuthManagers.OpenID; 16 | 17 | const query = getQuery(h3); 18 | const code = query.code?.toString(); 19 | if (!code) 20 | throw createError({ 21 | statusCode: 400, 22 | statusMessage: "No code in query params.", 23 | }); 24 | 25 | const state = query.state?.toString(); 26 | if (!state) 27 | throw createError({ 28 | statusCode: 400, 29 | statusMessage: "No state in query params.", 30 | }); 31 | 32 | const result = await manager.authorize(code, state); 33 | 34 | if (typeof result === "string") 35 | throw createError({ 36 | statusCode: 403, 37 | statusMessage: `Failed to sign in: "${result}". Please try again.`, 38 | }); 39 | 40 | await sessionHandler.signin(h3, result.user.id, true); 41 | 42 | if (result.options.redirect) { 43 | return sendRedirect(h3, result.options.redirect); 44 | } 45 | 46 | return sendRedirect(h3, "/"); 47 | }); 48 | -------------------------------------------------------------------------------- /server/routes/auth/oidc.get.ts: -------------------------------------------------------------------------------- 1 | import { enabledAuthManagers } from "~/server/plugins/04.auth-init"; 2 | 3 | defineRouteMeta({ 4 | openAPI: { 5 | tags: ["Auth"], 6 | description: "OIDC Signin redirect", 7 | parameters: [], 8 | }, 9 | }); 10 | 11 | export default defineEventHandler((h3) => { 12 | const redirect = getQuery(h3).redirect?.toString(); 13 | 14 | if (!enabledAuthManagers.OpenID) 15 | return sendRedirect( 16 | h3, 17 | `/auth/signin${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ""}`, 18 | ); 19 | 20 | const manager = enabledAuthManagers.OpenID; 21 | const { redirectUrl } = manager.generateAuthSession({ redirect }); 22 | 23 | return sendRedirect(h3, redirectUrl); 24 | }); 25 | -------------------------------------------------------------------------------- /server/routes/auth/signout.get.ts: -------------------------------------------------------------------------------- 1 | import sessionHandler from "../../internal/session"; 2 | 3 | defineRouteMeta({ 4 | openAPI: { 5 | tags: ["Auth"], 6 | description: "Tells server to deauthorize this session", 7 | parameters: [], 8 | }, 9 | }); 10 | 11 | export default defineEventHandler(async (h3) => { 12 | await sessionHandler.signout(h3); 13 | 14 | return sendRedirect(h3, "/auth/signin"); 15 | }); 16 | -------------------------------------------------------------------------------- /server/tasks/cleanup/invitations.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/server/internal/db/database"; 2 | 3 | export default defineTask({ 4 | meta: { 5 | name: "cleanup:invitations", 6 | }, 7 | async run() { 8 | console.log("[Task cleanup:invitations]: Cleaning invitations"); 9 | 10 | const now = new Date(); 11 | 12 | await prisma.invitation.deleteMany({ 13 | where: { 14 | expires: { 15 | lt: now, 16 | }, 17 | }, 18 | }); 19 | 20 | console.log("[Task cleanup:invitations]: Done"); 21 | return { result: true }; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /server/tasks/cleanup/sessions.ts: -------------------------------------------------------------------------------- 1 | import sessionHandler from "~/server/internal/session"; 2 | 3 | export default defineTask({ 4 | meta: { 5 | name: "cleanup:sessions", 6 | }, 7 | async run() { 8 | console.log("[Task cleanup:sessions]: Cleaning up sessions"); 9 | await sessionHandler.cleanupSessions(); 10 | console.log("[Task cleanup:sessions]: Done"); 11 | return { result: true }; 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json", 3 | "compilerOptions": { 4 | "exactOptionalPropertyTypes": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./components/**/*.{js,vue,ts}", 5 | "./layouts/**/*.vue", 6 | "./pages/**/*.vue", 7 | "./plugins/**/*.{js,ts}", 8 | "./app.vue", 9 | "./error.vue", 10 | ], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ["Inter"], 15 | display: ["Motiva Sans"], 16 | }, 17 | colors: { 18 | zinc: { 19 | 925: "#111112", 20 | }, 21 | }, 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "exactOptionalPropertyTypes": false 6 | } 7 | } 8 | --------------------------------------------------------------------------------