274 Commits

Author SHA1 Message Date
affdadf3aa Merge branch 'feat/ContainerHelper' into v2.1
Some checks failed
Dev Publish: Nuget / Publish Dev Packages (push) Failing after 31s
# Conflicts:
#	Moonlight.Api/Services/ApplicationService.cs
#	Moonlight.Api/Startup/Startup.Base.cs
#	Moonlight.Shared/Http/SerializationContext.cs
2026-02-09 08:18:56 +01:00
8d9a7bb8b3 Added validation to setup dto 2026-02-09 06:51:38 +00:00
1f631be1c7 Updated settings service to support generic types with JSON serialization/deserialization, adjusted setup wizard checks and modified database schema for proper JSONB storage. 2026-02-09 06:51:38 +00:00
5b4959771c Started implementing setup wizard backend for initial instance configuration. Adjusted ui 2026-02-09 06:51:38 +00:00
b8e1bbb28c Added setup wizard component for initial installation flow and integrated it into app routing. 2026-02-09 06:51:38 +00:00
c8fe11bd2b Implemented version fetching from source control git server. Added self version detection and update checks 2026-02-01 14:47:32 +01:00
09b11cc4ad Improved update instance model text design 2026-01-29 14:07:02 +01:00
660319afec Renamed SharedSerializationContext to SerializationContext. Added error handling and no build cache functionality 2026-01-29 13:59:24 +01:00
8181404f0c Moved request and responses dtos to correct namespace 2026-01-29 12:45:09 +01:00
e1207b8d9b Refactored container helper service. Cleaned up event models. Implemented version changing. Added security questions before rebuild 2026-01-29 11:23:07 +01:00
97a676ccd7 Implemented handling of server side issues using the rfc for problem detasils in the frontend 2026-01-29 09:28:50 +01:00
136620f1e6 Updated all forms to use the EnhancedEditForm and blazors native validation. Fixed smaller issues after upgrading to ShadcnBlazor 1.0.11 2026-01-29 08:58:12 +01:00
9b11360a0e Added vesrion to update instance dialog. Added apply functionality to instance page. Replaced dialog launch in overview to link to instance tab 2026-01-28 16:43:29 +01:00
deb69e6014 Upgraded to ShadcnBlazor 1.0.10. Started implementing instance management ui page 2026-01-26 16:49:25 +01:00
4e96905fb2 Implemented container helper status checked. Started implementing container helper ui. Improved update modal 2026-01-25 22:51:51 +01:00
e2f344ab4e Added container rebuild flow with real-time logs and updated UI, backend implementation, config options, and container helper API integration. 2026-01-23 16:38:42 +01:00
76a8a72e83 Moved the applying of selected permissions to the correct place in the role dialogs 2026-01-19 11:06:48 +01:00
3e302198a2 Removed unused launchSettings.json and unnecessary folder reference from project file to clean up configuration.
All checks were successful
Dev Publish: Nuget / Publish Dev Packages (push) Successful in 45s
2026-01-19 10:58:55 +01:00
4187f9da4e Implemented frontend configuration service and dynamic theme reloading integration. Updated sidebar to display dynamic application name.
All checks were successful
Dev Publish: Nuget / Publish Dev Packages (push) Successful in 48s
2026-01-19 10:55:34 +01:00
d85b07bde7 Upgraded ShadcnBlazor and Extras to 1.0.9. Replaced Checkbox with Switch for "Is Enabled" fields and updated CSS editor language to Css. 2026-01-19 10:24:05 +01:00
3cbdd3b203 Implemented theme crud and basic theme loading 2026-01-18 23:31:01 +01:00
56b14f60f1 Implementing api key authentication scheme and validation. Added default value in dtos 2026-01-17 21:05:20 +01:00
01c86406dc Implemented API key management with permission checks, database schema, and frontend integration. Adjusted string lengths for Role and API key attributes. 2026-01-16 15:06:45 +01:00
a28b8aca7a Added permission checks to all controllers. Added role permission loading. Added frontend permission checks. Implemented user logout in admin panel. 2026-01-16 13:07:19 +01:00
bee381702b Added session caching for user validation to reduce db calls and introduced configurable session options. 2026-01-16 09:19:15 +01:00
10cd0f0b09 Implemented member management of roles. Moved users controller 2026-01-15 14:52:29 +01:00
fcaa0dcd07 Migrated user management views to modal-based dialogs, restructured roles, and user pages under /admin/users. Simplified navigation paths and improved tabbed interface. 2026-01-15 12:26:49 +01:00
ea35ddb0dc Added build step for frontend in NuGet publishing workflow to ensure class list files generate properly 2026-01-15 11:45:06 +01:00
b4536ca6e9 Upgraded ShadcnBlazor and Extras to 1.0.8, adjusted styles import paths, and adjusted frontend build process.
Some checks failed
Dev Publish: Nuget / Publish Dev Packages (push) Failing after 23s
2026-01-15 10:59:40 +01:00
7f482fd6c3 Refactored response and request models to dto naming. Adjusted mapper naming 2026-01-14 19:19:45 +01:00
1d1dfc2c1c Adjusted sideoffset of dropdown menu on users page 2026-01-14 19:08:04 +01:00
a197d7d980 Improved diagnose accordium UIs 2026-01-14 19:05:42 +01:00
06063b94b3 Implemented roles and action timestamps. Added oermissions selector and interfaces 2026-01-14 19:03:17 +01:00
43d43a6d7d Adjusted compose file from template to match moonlight 2026-01-14 19:01:05 +01:00
1849cda2f5 Added npm install command to nuget publishing action 2026-01-14 16:19:37 +01:00
f836657603 Implemented Tailwind CSS class list extraction and integrated it into the build process of the frontend package
Some checks failed
Dev Publish: Nuget / Publish Dev Packages (push) Failing after 16s
2026-01-14 16:10:06 +01:00
7e0b427137 Added Gitea workflow for publishing NuGet packages and configured project files for NuGet packaging
All checks were successful
Dev Publish: Nuget / Publish Dev Packages (push) Successful in 41s
2026-01-14 13:36:36 +01:00
b79c8fe476 Added current tab as query parameter to system page 2025-12-30 16:07:08 +01:00
f71bad3da3 Upgraded ShadcnBlazor to 1.0.5 2025-12-30 16:06:33 +01:00
170cac2091 Minor design improvements to user table and diagnose page 2025-12-30 16:06:18 +01:00
ba942b2f8f Made sidebar item collection extendable via interface. Refactored settings to system 2025-12-27 23:54:48 +01:00
05c05f1b72 Adjusted config options for oidc and database to use the correct section 2025-12-27 23:34:00 +01:00
ec6782160c Added update model with progress animations. Backend not implemented 2025-12-27 23:33:33 +01:00
1581276854 Adjusted dockerfile for library-host architecture 2025-12-27 23:32:54 +01:00
e1c0645428 Added diagnose frontend and backend implementation 2025-12-27 23:32:36 +01:00
be3cdb8235 Added settings option model. Recreated migrations 2025-12-25 22:02:12 +01:00
bfc7d9993a Refactored pages to correct locations 2025-12-25 22:00:59 +01:00
ca69d410f2 Added small system overview 2025-12-25 21:55:46 +01:00
a2d4edc0e5 Recreated solution with web app template. Improved theme. Switched to ShadcnBlazor library 2025-12-25 19:16:53 +01:00
0cc35300f1 Updated mooncore styles. Adjusted theme editor and theme loading. Changed versions Upgraded mooncore.blazor.flyonui. Made moonlight flyonui/daisyui compatible 2025-10-27 08:23:02 +00:00
2f21806bea Upgraded mooncore. Regenerated mappings. Updated versions 2025-10-20 20:42:54 +02:00
c5d75a8710 Updated package versions 2025-10-20 17:30:00 +00:00
f3dd37f649 Removed pull request trigger from nuget build action 2025-10-20 17:27:59 +00:00
b035dd6b76 Added filter for csproj files for nuget package publish action 2025-10-20 17:26:46 +00:00
34c4bb8cb7 Updated theme and styles 2025-10-20 19:19:11 +02:00
de5c9f4ea1 Upgraded mooncore deendencies 2025-10-20 19:19:11 +02:00
9ab69ffef5 Upgraded mooncore versions. Cleaned up code, especially startup code. Changed versions 2025-10-05 16:07:27 +00:00
d2ef59d171 Merge pull request #457
Updated dependencies. Refactored to async scheme
2025-09-21 19:24:11 +02:00
8e2b333f47 Updated Moonlight nuget versions. Regenrated mappings 2025-09-21 17:20:33 +00:00
594fb3073f Updated to latest mooncore version for xml docs 2025-09-21 17:17:21 +00:00
3e87d5c140 Switched to database scheme seperation from MoonCores SingleDb. Updated mooncore versions. Updating to correct Async naming 2025-09-21 16:44:01 +00:00
86bec7f2ee Updated to latest mooncore version. Cleaned up some crud controllers and replaced DataTable with the new DataGrid component 2025-09-16 12:09:20 +00:00
8e242dc8da Merge pull request #456
Implemented SignalR (+ Scaling)
2025-09-16 10:04:20 +02:00
efca9cf5d8 Implemented SignalR scaling using redis. Improved diagnose report generator. Added SignalR debug card in Diagnose page 2025-09-16 08:02:53 +00:00
8573fffaa2 Updated dependencies. Changed version. Fixed small file manager archive format issue 2025-09-06 18:42:28 +02:00
51aeb67ad6 Improved paged endpoint rage validation. Fixed smaller request model validation issues 2025-08-26 01:52:43 +02:00
5e371edf2b Updated versions 2025-08-26 01:30:45 +02:00
d46ad72cb6 Merge pull request #454 from Moonlight-Panel/v2.1_FileManager
Extended file manager to support the new interfaces for downloading via url. Improved the handling of compressing and decompressing. Separated file manager controllers. Updated mooncore versions
2025-08-26 01:09:17 +02:00
a6ae2aacfb Extended file manager to support the new interfaces for downloading via url. Improved the handling of compressing and decompressing. Seperated file manager controllers. Updated mooncore versions 2025-08-26 01:07:59 +02:00
dc862e4b3c Removed unused pwa build option from razor class library project 2025-08-24 11:59:00 +02:00
e56c5edfb4 Merge pull request #453
Implemented theme importing and exporting
2025-08-23 23:43:06 +02:00
70b310adef Implemented theme importing and exporting 2025-08-23 23:39:56 +02:00
6748288f3c Merge pull request #452 from Moonlight-Panel/v2.1_OpenTelemetry
Added open telemetry exporter to existing metric system. Improved config section for metrics
2025-08-23 22:57:19 +02:00
2c5d45e9c2 Added open telemetry exporter to existing metric system. Improved config section for metrics 2025-08-23 22:09:00 +02:00
c02c13bf90 Bumped versions 2025-08-23 20:25:08 +02:00
902ca114c1 Merge pull request #451 from Moonlight-Panel/v2.1_ImproveAuth
Improved authentication
2025-08-20 17:20:10 +02:00
17cd039c9b Improved design of login method selection screen 2025-08-20 17:16:19 +02:00
26f955fce2 Added extendability to the sign-in / sync, the session validation and the frontend claims transfer calls 2025-08-20 17:01:42 +02:00
3cc48fb8f7 Updated MoonCore dependencies. Switched to asp.net core native authentication scheme abstractions. Updated claim usage in frontend 2025-08-20 16:16:31 +02:00
60178dc54b Implemented user deletion service and IUserDeleteHandler for plugins to hook into 2025-08-19 21:35:43 +02:00
8a63a3448a Removed old bg-gray-900 from register and login razor pages 2025-08-19 21:18:37 +02:00
f5336f63ce Rewritten build and publish steps because github's nuget registry is not compatible with previous used action 2025-08-10 16:16:57 +02:00
97583349df Added plugins.props files for plugin references. Fixed small tailwind mapping issue. Adjusted actions file to publish packages correctly again. Updated versions 2025-08-10 16:02:10 +02:00
9bec336323 Merge remote-tracking branch 'origin/v2_ChangeArchitecture' into v2_ChangeArchitecture 2025-07-24 09:24:06 +02:00
123b64a666 Cleaned up pagination in user and apikey controller. Extracted login start and start url generation to modular IOAuth2Provider interface. Improved login and register local oauth2 page 2025-07-24 09:24:00 +02:00
5a215231fa Cleaned up pagination in user and apikey controller. Extracted login start and start url generation to modular IOAuth2Provider interface. Improved login and register local oauth2 page 2025-07-24 09:23:36 +02:00
6a29b5386c Merge remote-tracking branch 'origin/v2_ChangeArchitecture' into v2_ChangeArchitecture 2025-07-24 08:33:29 +02:00
7cd138b09f Improved ui/ux of theme pages. Upgraded mooncore.blazor.flyonui 2025-07-23 22:58:16 +02:00
504837fe77 Fixed color inconsistency 2025-07-23 09:49:52 +02:00
7dde1d86f8 Added theme loading. Improved theme editor. Updated app theme model 2025-07-22 21:08:03 +02:00
a480ae9c50 Renamed theme tab to customisation tab. Added basic theme crud 2025-07-21 22:16:34 +02:00
2c9a87bf3e Expanding theme tab to customization tab. Started improving theme selection. 2025-07-20 23:27:51 +02:00
03ea94b858 Merge remote-tracking branch 'origin/v2_ChangeArchitecture' into v2_ChangeArchitecture 2025-07-19 00:20:07 +02:00
9e42985ec8 Added helper message component (not final). Improved stat card design. Started improving theme preview 2025-07-19 00:20:00 +02:00
dbf17aee3b Removed ace editor build from moonlight. Using the one from mooncore now. Bumped up versions 2025-07-18 08:39:54 +02:00
2b91d9a798 Updated mooncore version. Fixed checkbox ui in diagnose page. Bumped nuget version 2025-07-17 22:50:21 +02:00
74d18419a6 Bumped nuget versions to 2.1.4 2025-07-17 21:49:27 +02:00
e3f007b568 Merge pull request #446 from Moonlight-Panel/v2.1_TwClassExport
Fixed oauth2 controller. Improved tw export. Fixed gh action
2025-07-17 21:47:26 +02:00
6690f09a32 Updated mappings import. Fixed github action 2025-07-17 21:44:31 +02:00
7c496b4c7f Fixed oauth2 controller returning invalid data. Adjusted build seps for tailwind class map 2025-07-16 20:48:49 +02:00
77abdd807d Merge pull request #445 from Moonlight-Panel/v2.1_OptimizeNugetBuilding
Optimize nuget building. Refactored startup. Fixed smaller issues
2025-07-14 21:09:21 +02:00
7599a7d60a Merge remote-tracking branch 'origin/v2.1_OptimizeNugetBuilding' into v2.1_OptimizeNugetBuilding 2025-07-14 21:07:34 +02:00
14993b9fe7 Refactored startup. Removed unused usings. Improved nuget package building. Switched to yaml for configuration. Moved asset files. Set correct context type for oauth2 pages. Updated versions 2025-07-14 21:07:13 +02:00
acba3a9f53 Refactored startup. Removed unused usings. Improved nuget package building. Switched to yaml for configuration. Moved asset files. Set correct context type for oauth2 pages. Updated versions 2025-07-14 21:06:54 +02:00
2b62fc141d Refactored startup. Updated usings. Removed dockerignore 2025-07-14 19:25:08 +02:00
4baa0bbe77 Adjusted tailwind style building to work with class name extraction 2025-07-14 14:40:10 +02:00
d88376f2fb Refactored css classes to match flyonui. Switched to postgres arrays for permissions. Migrated file manager. Adjusted everything to work with the latest mooncore version 2025-07-12 23:53:43 +02:00
eaece9e334 Separating runtime from application code to improve building. Upgraded mooncore packages. Started switching to flyonui. Added PluginFramework plugin loading via mooncore 2025-07-11 17:13:37 +02:00
7e158d48c6 version bumped 2025-07-03 21:41:07 +02:00
4ed153ac5b Fixed MainLayout so "h-full" can be used in Pages. 2025-07-03 21:03:42 +02:00
6b17296139 Fixed formatting in workflow. Fixed invalid image link in readme 2025-06-08 16:52:57 +02:00
bb5dffe395 Fixed dotnet tool not being discovered due to missing path env 2025-06-08 00:47:49 +02:00
ole
0fa844f856 hopefully fixed now 2025-06-08 00:42:18 +02:00
ole
4fea77c837 Added new Publish workflow 2025-06-08 00:10:21 +02:00
f76c797a7f Added skip duplicate option to push to prevent pushing the same version 2025-06-07 22:07:30 +02:00
ad81fd9199 Merge remote-tracking branch 'origin/v2_ChangeArchitecture' into v2_ChangeArchitecture 2025-06-07 22:04:39 +02:00
f732c80230 Added missing property to scripts csproj 2025-06-07 22:04:16 +02:00
eb97d15f03 Merge pull request #444 from Moonlight-Panel/v2_ChangeArchitecture_workflows
First iteration of development packages workflow
2025-06-07 21:47:00 +02:00
be590d8e2b First iteration of development packages workflow 2025-06-07 21:40:38 +02:00
a41845e45c Removed outdated workflows. Removed used docker file and compose file. Updated readme 2025-06-01 20:49:19 +02:00
a1cb4f243a Added missing input validation in oauth2 register request 2025-06-01 15:34:17 +02:00
110e055e24 Upgraded Swashbuckle.AspNetCore to 8.1.2 2025-06-01 11:48:27 +02:00
f3a35bd62a Started implementing metrics system 2025-05-23 17:15:19 +02:00
565d9a5a4d Upgraded mooncore.blazor.tailwind 2025-05-23 10:37:01 +02:00
dbc29046f5 Fixed memory usage detection for linux 2025-05-23 10:36:37 +02:00
0152502c1b Updated mooncore version. Fixed tailwind forms plugin layering issues. Added tags input for permissions for api keys and users 2025-05-22 20:36:22 +02:00
0520e3d7e5 Added separate build script to package.json for the docker image to use 2025-05-22 20:16:35 +02:00
dc49e168ab Updated mooncore versions. Updated permission checking. Added client side permission check. Added dotnet tool specifications for scripts project 2025-05-21 10:30:37 +02:00
da55f2b19e Merge pull request #440 from Moonlight-Panel/v2_ChangeArchitecture_AddDiagnose
Added diagnose system
2025-05-17 19:41:32 +02:00
424f2a8779 Merge branch 'v2_ChangeArchitecture' into v2_ChangeArchitecture_AddDiagnose 2025-05-17 19:40:50 +02:00
255bfba9e3 Cleaned up diagnose system. Fixed smaller inconsistencies 2025-05-17 19:38:36 +02:00
593a79c506 Merge pull request #442 from Moonlight-Panel/v2_ChangeArchitecture_FirstUserAdmin
Implemented first user admin feature
2025-05-17 18:07:15 +02:00
d4a7600c14 Cleaned up scripts project 2025-05-17 18:04:59 +02:00
mxritzdev
7ead76fbcc Implemented First User Admin Feature 2025-05-17 17:53:05 +02:00
mxritzdev
f87e4a0800 censored client id config diagnose 2025-05-15 14:15:44 +02:00
mxritzdev
eab03d7f5a cleaned up diagnose feature 2025-05-15 14:09:16 +02:00
9dc77e6dde Workaround for https://github.com/dotnet/aspnetcore/issues/59291 2025-05-15 10:14:44 +02:00
3a804c99ce Upgraded to dotnet 9 2025-05-15 09:45:54 +02:00
mxritzdev
c49b000521 Used CamelCase Formatter in Diagnose ui 2025-05-14 20:15:07 +02:00
mxritzdev
ebc1b9441e changed the diagnose to be easier to use 2025-05-14 20:13:24 +02:00
0e5402c347 Removed unused calls and classes from the old plugin system 2025-05-14 09:15:18 +02:00
6922242b4f Disabled generation of packages during build. Re-enabled trimming in the client 2025-05-13 21:46:29 +02:00
73ca5e57e8 Added nuget directory option to pack command of the script 2025-05-13 21:17:49 +02:00
a579dd4759 Finished compile time plugin loading. Refactored plugin loading. Extended build helper script 2025-05-13 20:48:50 +02:00
mxritzdev
609a0297d5 moved diagnose to own controller, added advanced diagnose building, ui for advanced still missing 2025-05-13 17:22:47 +02:00
mxritzdev
0743bad93c added ui for the diagnose system 2025-05-13 15:28:46 +02:00
mxritzdev
49848db96f added logs to the diagnose 2025-05-13 14:26:29 +02:00
mxritzdev
ba736d2b19 added file logging and log rotation 2025-05-13 14:14:22 +02:00
mxritzdev
753cb04dfe improved the config in the diagnose 2025-05-13 14:06:59 +02:00
mxritzdev
bc25210fe4 added config to diagnose, while censoring sensitive data 2025-05-12 19:36:27 +02:00
mxritzdev
a4e0175173 finished diagnose system 2025-05-12 19:00:09 +02:00
moritz
bd8ea67017 zip extracts now, still some issues with the ressource building 2025-05-12 16:51:29 +02:00
8126250d1a Implemented tag based run helper script 2025-05-11 22:26:05 +02:00
moritz
8ac2d20d8a added base diagnose, is not working, yet, still contains test objects 2025-05-11 13:03:44 +02:00
1b4d32eed3 Prepared tailwind system for plugin builds and exports via nuget. Removed obsolete old css bundling. Added helper scripts for building. Rewritten build scripts 2025-05-11 00:07:41 +02:00
1a67fcffb4 Added nuget export options for development. Added nuget patch script for dotnet-script 2025-05-08 17:10:41 +02:00
bbc6c0fbd3 Upgraded to latest mooncore packages. Upgraded to tailwind v4 2025-05-02 13:06:09 +02:00
6657bae0cd Improved api key ux 2025-04-15 13:42:30 +02:00
65ea5985d3 Implement disabling of local oauth2 controller 2025-04-15 13:08:28 +02:00
7defc9a6a9 Removed legacy and testing options from compose 2025-04-15 13:03:38 +02:00
504cb8e950 Implemented modular oauth2 system 2025-04-15 13:03:13 +02:00
f81b84e4b3 Cleaned up swagger controller 2025-04-15 13:02:45 +02:00
c12e1e38b8 Implemented user permission update 2025-04-15 12:32:29 +02:00
db7ac8d174 Removed unused middleware. Fixed plugin loading issues 2025-04-15 12:19:32 +02:00
0b0c9304b1 Added oauth2 access endpoint override option 2025-04-14 22:36:37 +02:00
f56f592c4c Added distroless dockerfile. Updated docker compose 2025-04-14 22:35:38 +02:00
b0fe27c643 Removed left over rider files 2025-04-14 22:07:08 +02:00
55bc825cb7 Added hangfire. Implemented hangfire statistics. Updated lucide icons 2025-04-09 20:24:31 +02:00
7fa46ef245 Removed use of crud helper. Refactored user and api key response. Removed unused request/response models 2025-04-05 14:56:26 +02:00
e1c0722fce Updated sidebar and header 2025-03-30 17:54:07 +02:00
3f511cefa8 Updated loader style so the table loader is working again 2025-03-24 21:20:21 +01:00
f1adba4fa6 Updated to latest mooncore version 2025-03-24 20:35:42 +01:00
a9d3a30782 Fixed overwriting issue for chunked file upload 2025-03-20 20:50:05 +01:00
55a8cfad46 Implemented chunked uploading. Updated mooncore 2025-03-20 16:23:27 +01:00
420ff46ceb Improved upload progress tracking. Fixed path on frontend export 2025-03-17 10:53:45 +01:00
75f037da02 Implemented frontend hosting file generation helper 2025-03-16 22:03:01 +01:00
1238095f09 Moved resources. Added placeholder pfp 2025-03-14 17:15:43 +01:00
3084bb268b Implemented proper mobile sidebar. Fixed mobile view of api key page. Removed unused exception 2025-03-14 15:12:36 +01:00
f1c0d3b896 Implemented api authentication. Removed old secret system 2025-03-14 12:32:13 +01:00
340cf738dc Updated mooncore dependencies. Improved ux for the system file manager 2025-03-13 12:18:13 +01:00
f23320eb1c Added max file size upload option. Switched from stream upload to multipart form content file upload 2025-03-07 13:31:30 +01:00
9fb1667bf0 Updated mooncore dependencies 2025-03-06 20:55:14 +01:00
1f95577eb7 Updated mooncore.extended. Adjusted authentication config to support multiple schemes 2025-03-01 17:35:21 +01:00
45ccb6fc4c Fixed oauth2 configuration loading 2025-02-28 11:03:54 +01:00
b1092985ff Updated mooncore versions. Removed unused imports and legacy configuration loading 2025-02-28 09:55:47 +01:00
6c5e4c2a1e Fixed bundling. Upgraded mooncore.extended 2025-02-26 22:32:02 +01:00
caa8d47af2 Simplified plugin service and loading 2025-02-26 17:06:25 +01:00
cdc4744f28 Added theme saving. Added interfaces for overview pages. Renamed sidebar interface function 2025-02-26 13:09:31 +01:00
f4a0aabb61 Finished migration to postgresql. Updated mooncore package 2025-02-26 09:23:57 +01:00
64f4a3a58c Started migrating to postgresql 2025-02-25 17:12:31 +01:00
a23c3b0fdd Added comments
<3
2025-02-24 21:02:32 +01:00
3dd5d2958a Implemented plugin loading via di on the api server. Fixed plugin loading in the client 2025-02-24 20:03:37 +01:00
69df761bf4 Implemented plugin interface loading via di
1^
2025-02-24 17:20:16 +01:00
4a571e1944 Updated theme 2025-02-17 06:46:59 +01:00
fa86b26f46 Upgraded npm package. Made update view paths shorter 2025-02-08 18:14:42 +01:00
480d118014 Implemented system files tab 2025-02-06 10:56:49 +01:00
2e5d0dcd73 Added new mooncore js dependencies. Added ace editor build 2025-02-05 16:50:35 +01:00
b6b488edf6 Updated mooncore dependency usage 2025-02-05 14:08:40 +01:00
bf5a744499 Starting updating mooncore dependency usage 2025-02-04 17:09:07 +01:00
1a4864ba00 Added theming support. Added import/export
Missing: API Server save
2025-01-08 00:33:09 +01:00
e299cde6da Removed asset controllers. Started adding design section in settings 2025-01-07 00:08:19 +01:00
8372cfad1b Started implementing fronted configuration. Upgraded mooncore. Made database calls asnyc 2025-01-06 22:36:21 +01:00
d477e803ab Upgraded mooncore packages. Added css variables for theming. Made all db calls use async/await 2025-01-04 10:37:40 +01:00
bf89ef16f7 Upgraded MoonCore 2024-12-20 14:29:54 +01:00
744d977454 Upgraded frontend dependencies 2024-12-20 01:50:41 +01:00
b95f89687f Updated mooncore tailwind dependency 2024-12-17 22:43:43 +01:00
143ba3c138 Fixed app startup issue in frontend for dev server 2024-12-13 20:11:45 +01:00
094af845a0 Added app startup for the frontend 2024-12-13 18:46:10 +01:00
5efeb5fba6 Created powershell variant of the nuget cache helper script 2024-12-11 09:09:42 +01:00
42ab052699 Added helper script 2024-12-10 22:30:06 +01:00
e6c9feed6b Fixed bundle generation service 2024-12-10 22:29:53 +01:00
e63a3db8b9 Added css bundle api. Improved css bundling code
I made the code cleaner as requested @Masu-Baumgartner  :>
2024-12-10 21:25:46 +01:00
75cefea4fa Implemented download service 2024-12-10 19:52:14 +01:00
150a18cc0b Implemented css bundling 2024-12-10 16:28:11 +01:00
64b20e26ac Created a nuget build script for developers using windows 2024-12-05 13:55:58 +01:00
f08c8f013d Upgraded dependencies 2024-12-05 13:11:58 +01:00
721570927f Automated nuget package build
So you don't have to do it manually every time
2024-12-01 22:22:07 +01:00
62fe6089f7 Improved asset service. Removed now unused plugin asset streaming endpoint 2024-12-01 20:04:29 +01:00
0a76e64d2f Added AssetService. Added command line handling for assets. Added asset streaming on the client 2024-12-01 18:34:08 +01:00
2e98d166ec Changed icons to use Lucide icons 2024-12-01 18:00:13 +01:00
Masu-Baumgartner
bc737c830f Started adding asset api/auto import 2024-11-26 17:33:51 +01:00
Masu-Baumgartner
23a74bdfc6 Fixed plugin loader usage. Improved export for nuget. Changed css name 2024-11-21 17:03:38 +01:00
Masu-Baumgartner
f702167d6e Fixed api exception handler 2024-11-21 09:41:53 +01:00
Masu-Baumgartner
fe31c01a06 Upgraded packages. Improved startup. Removed unused components 2024-11-20 17:21:17 +01:00
Masu-Baumgartner
2d0a0e53c0 Implementing plugin loading for api server and client 2024-11-19 16:28:25 +01:00
Masu Baumgartner
072adb5bb1 Working on module/plugin system 2024-11-14 22:30:02 +01:00
Masu-Baumgartner
e5555c815b Cleanud up auth code 2024-11-12 10:29:50 +01:00
Masu-Baumgartner
a074f0c4f0 Testing new oauth2 setup 2024-11-11 16:46:51 +01:00
Masu Baumgartner
d92f996169 Started adding module service
I will probably change the api paths and a lot of other stuff i wrote today tomorrow :|
2024-11-10 22:06:19 +01:00
Masu Baumgartner
96bb3a5c0f Preparations for plugin/module development 2024-11-10 20:36:02 +01:00
Masu Baumgartner
18810766ed DevServer things :> 2024-11-08 23:53:06 +01:00
Masu Baumgartner
764ebe3586 Implemented system overview 2024-11-08 14:49:49 +01:00
Masu Baumgartner
add4c3e99f Fixed smaller issues with refreshing the access token on frontend side
+ Saving the access and refresh token on server side
2024-11-07 00:42:23 +01:00
Masu Baumgartner
f9c4ec1d31 Completed adjustments for the new configuration of oauth2 and token auth 2024-11-06 20:03:18 +01:00
Masu-Baumgartner
f2d653563c Adjusting to use new extension methods to configure and handle token auth and oauth2 2024-11-06 16:46:24 +01:00
Masu Baumgartner
288b0c8d97 Started testing oauth2 handler from mooncore 2024-11-05 22:46:26 +01:00
Masu-Baumgartner
69e5e1c75b Removed unused file. Fixed typo 2024-11-05 16:35:24 +01:00
Masu-Baumgartner
399fbbaab5 Started testing a dx friendlier oauth2 handler 2024-11-04 16:34:29 +01:00
Masu Baumgartner
06f7731011 Implemented register functionality 2024-11-03 20:55:40 +01:00
Masu Baumgartner
81d8bc45ee Improved caching to exclude server side only resources 2024-11-03 19:21:46 +01:00
Masu Baumgartner
17a4e7ec14 Added option to disable the client hosting of the api server 2024-11-03 19:20:51 +01:00
Masu Baumgartner
b73c3ebfb3 Started with docker compose config. Switched to new config system. Upgraded mooncore packages 2024-11-03 01:30:53 +01:00
Masu-Baumgartner
b0a044db97 Implemented apex charts for visualisation
The fill animations is buggy
2024-10-30 15:54:48 +01:00
Masu-Baumgartner
fce44f49b6 Implemented apikey backend 2024-10-30 13:34:19 +01:00
Masu-Baumgartner
6d0c75ceff Smaller ui adjustments 2024-10-29 16:13:53 +01:00
Masu-Baumgartner
324bf6891a Implemented api key crud and started adding system page. Added 404 page 2024-10-29 15:42:20 +01:00
Masu-Baumgartner
e5f29e4725 Added fancy start page greeting 2024-10-29 09:22:18 +01:00
Masu-Baumgartner
54e0675ba9 Improved default log level definition 2024-10-29 09:22:05 +01:00
Masu Baumgartner
e02af774a9 Cleaned up the startup sequence. 2024-10-28 21:30:00 +01:00
Masu-Baumgartner
f6ed12fc7a Upgraded MoonCore packages. Small ui improvement 2024-10-28 15:04:07 +01:00
Masu Baumgartner
c15f18108d Changed auth success ui. Switched to new interface service. Upgraded mooncore versions 2024-10-27 20:49:06 +01:00
Masu Baumgartner
7239182e83 Changed colors......
I am an absolute mess when it comes to frontend. I cannot settle on a design :c
2024-10-25 23:22:34 +02:00
Masu Baumgartner
eba6e00251 Improved oauth2 ui design 2024-10-25 15:49:03 +02:00
Masu Baumgartner
6f3341e6ad Switched to LocalStorage. Upgraded MoonCore. Improved auth flow 2024-10-23 21:37:26 +02:00
Masu Baumgartner
910f190c86 Completed first iteration of access-refresh auth. Upgraded mooncore. Refactored mooncore related stuff 2024-10-21 20:17:59 +02:00
Masu Baumgartner
c4c3d1bd60 Implemented better ux for the oauth2 workflow
Still wip
2024-10-20 01:05:46 +02:00
Masu Baumgartner
f166de1a43 Implemented complete oauth2 flow with modular providers
The local oauth2 provider still needs A LOT of love. But the general oauth2 workflow works. The http api client needs an option to disable the authentication things and we need to switch to the local storage for storing access, refresh and timestamp for the client
2024-10-19 20:09:03 +02:00
Masu Baumgartner
71dc81c4dc Reorganized config. Re implemented auth controller to use token-pair authentication and oauth2 2024-10-19 19:27:22 +02:00
Masu Baumgartner
8883b521e9 Started implementing client and api server auth and the refresh endpoint 2024-10-19 16:37:37 +02:00
Masu Baumgartner
6be3b8338d Improved token handling and used new validate auth request for oauth2 2024-10-18 13:11:02 +02:00
Masu Baumgartner
9d1351527d Started implementing oauth2 based on MoonCore helper services
Its more or less a test how well the helper services improve the implementation. I havent implemented anything fancy here atm. Just testing the oauth2 flow
2024-10-18 00:03:20 +02:00
Masu-Baumgartner
13daa3cbac Started testing tag component 2024-10-09 16:25:33 +02:00
Masu-Baumgartner
19afc5d055 Added permissions to users controller and the client. 2024-10-07 16:37:18 +02:00
Masu-Baumgartner
bb29177e41 Merge branch 'v2_ChangeArchitecture' of https://github.com/Moonlight-Panel/Moonlight into v2_ChangeArchitecture 2024-10-07 12:05:18 +02:00
Masu Baumgartner
f48e5d4b19 Implemented admin crud ui for users page. Fixed some smaller issues 2024-10-06 20:44:18 +02:00
Masu Baumgartner
cf25e4e1e6 Implemented admin users crud api 2024-10-06 01:19:23 +02:00
Masu-Baumgartner
966e67afee Adjusted header to use identity service values 2024-10-04 09:30:20 +02:00
Masu-Baumgartner
a0432eec68 Improved PWA options. Fully implemented auth 2024-10-02 16:31:23 +02:00
Masu Baumgartner
522d0c1471 Added app loaders and screen for the ui. Added identity service. Started auth screens 2024-10-01 21:02:55 +02:00
Masu Baumgartner
ca1b7a84c9 Merge branch 'v2_ChangeArchitecture' of https://github.com/Moonlight-Panel/Moonlight into v2_ChangeArchitecture 2024-10-01 20:17:18 +02:00
Masu-Baumgartner
e32e35d3af Implemented api/check endpoint. Added api error middleware 2024-10-01 14:27:09 +02:00
Masu-Baumgartner
ef2e6c9a20 Added login/register function. Implemented authentication. Started authorization 2024-10-01 11:29:19 +02:00
Masu Baumgartner
fa748494f6 Small changes 2024-09-30 20:50:21 +02:00
Masu-Baumgartner
73bf27d222 Removed old architecture. Added new base project structure 2024-09-30 17:52:14 +02:00
980 changed files with 9981 additions and 56547 deletions

View File

@@ -21,5 +21,10 @@
**/obj **/obj
**/secrets.dev.yaml **/secrets.dev.yaml
**/values.dev.yaml **/values.dev.yaml
**/appsettings.json
**/appsettings.Development.json
**/appsettings*
**/compose.*
**/.env.example
LICENSE LICENSE
README.md README.md

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=user
DATABASE_PASSWORD=user
DATABASE_DATABASE=user
OIDC_AUTHORITY=http://localhost:8092
OIDC_AUTHORITY=http://localhost:8092
OIDC_CLIENT_ID=client_id
OIDC_CLIENT_SECRET=client_secret
OIDC_REQUIRE_HTTPS_METADATA=false

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@@ -0,0 +1,66 @@
name: "Dev Publish: Nuget"
on:
workflow_dispatch:
push:
branches:
- v2.1
paths:
- 'Moonlight.*/*.csproj'
jobs:
publish:
name: Publish Dev Packages
runs-on: linux_amd64
steps:
- name: Check out repository code
uses: actions/checkout@v4
# Publish api server
- name: Build project
run: dotnet build Moonlight.Api --configuration Debug
- name: Publish NuGet package
run: dotnet pack Moonlight.Api --configuration Debug --output ./artifacts
- name: Push nuget package
run: dotnet nuget push ./artifacts/*.nupkg --skip-duplicate --source https://git.battlestati.one/api/packages/Moonlight-Panel/nuget/index.json --api-key ${{ secrets.ACCESS_TOKEN }}
- name: Remove artifacts
run: rm -rf ./artifacts
# Publish frontend
# We need to build it first so the class list files generate
- name: Build project
run: dotnet build Moonlight.Frontend --configuration Debug
- name: Build tailwind styles and extract class list
working-directory: Hosts/Moonlight.Frontend.Host/Styles
run: npm install && npm run build
- name: Build project
run: dotnet build Moonlight.Frontend --configuration Debug
- name: Publish NuGet package
run: dotnet pack Moonlight.Frontend --configuration Debug --output ./artifacts
- name: Push nuget package
run: dotnet nuget push ./artifacts/*.nupkg --skip-duplicate --source https://git.battlestati.one/api/packages/Moonlight-Panel/nuget/index.json --api-key ${{ secrets.ACCESS_TOKEN }}
- name: Remove artifacts
run: rm -rf ./artifacts
# Publish shared
- name: Build project
run: dotnet build Moonlight.Shared --configuration Debug
- name: Publish NuGet package
run: dotnet pack Moonlight.Shared --configuration Debug --output ./artifacts
- name: Push nuget package
run: dotnet nuget push ./artifacts/*.nupkg --skip-duplicate --source https://git.battlestati.one/api/packages/Moonlight-Panel/nuget/index.json --api-key ${{ secrets.ACCESS_TOKEN }}
- name: Remove artifacts
run: rm -rf ./artifacts

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
# These are supported funding model platforms
ko_fi: masuowo

View File

@@ -1,87 +0,0 @@
name: Bug Report
description: Something isn't working quite right in the software.
labels: [not confirmed]
body:
- type: markdown
attributes:
value: |
Bug reports should only be used for reporting issues with how the software works. For assistance installing this software, as well as debugging issues with dependencies, please use our [Discord server](https://discord.gg/TJaspT7A8p).
- type: textarea
attributes:
label: Current Behavior
description: Please provide a clear & concise description of the issue.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: Please describe what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: Please be as detailed as possible when providing steps to reproduce, failure to provide steps will result in this issue being closed.
validations:
required: true
- type: input
id: panel-version
attributes:
label: Panel Version
description: Version number of your Panel (latest is not a version)
placeholder: v2 EA
validations:
required: true
- type: input
id: daemon-version
attributes:
label: Daemon Version
description: Version number of your Daemon (latest is not a version)
placeholder: v2 EA
validations:
required: true
- type: input
id: image-details
attributes:
label: Games and/or Images Affected
description: Please include the specific game(s) or Image(s) you are running into this bug with.
placeholder: Minecraft (Paper), Minecraft (Forge)
- type: input
id: docker-image
attributes:
label: Docker Image
description: The specific Docker image you are using for the game(s) above.
placeholder: ghcr.io/xxx/yolks:java_17
- type: textarea
id: panel-logs
attributes:
label: Error Logs
description: |
Run the following command to collect logs on your system.
Panel: `docker logs moonlight`
Wings: `sudo wings diagnostics`
placeholder: logs here
render: bash
validations:
required: false
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please [search here](https://github.com/Moonlight-Panel/Moonlight/issues) to see if an issue already exists for your problem.
options:
- label: I have searched the existing issues before opening this issue.
required: true
- label: I have provided all relevant details, including the specific game and Docker images I am using if this issue is related to running a server.
required: true
- label: I have checked in the Discord server and believe this is a bug with the software, and not a configuration issue with my specific system.
required: true

View File

@@ -1,8 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Installation Help
url: https://discord.gg/TJaspT7A8p
about: Please visit our Discord for help with your installation.
- name: General Question
url: https://discord.gg/TJaspT7A8p
about: Please visit our Discord for general questions about Moonlight Panel.

View File

@@ -1,32 +0,0 @@
name: Feature Request
description: Suggest a new feature or improvement for the software.
labels: [feature request]
body:
- type: checkboxes
attributes:
label: Is there an existing feature request for this?
description: Please [search here](https://github.com/Moonlight-Panel/Moonlight/issues?q=is%3Aissue) to see if someone else has already suggested this.
options:
- label: I have searched the existing issues before opening this feature request.
required: true
- type: textarea
attributes:
label: Describe the feature you would like to see.
description: "A clear & concise description of the feature you'd like to have added, and what issues it would solve."
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like.
description: "You must explain how you'd like to see this feature implemented. Technical implementation details are not necessary, rather an idea of how you'd like to see this feature used."
validations:
required: true
- type: textarea
attributes:
label: Additional context to this request.
description: "Add any other context or screenshots about the feature request."
validations:
required: false

View File

@@ -1,35 +0,0 @@
name: Development Build
on:
workflow_dispatch:
pull_request:
types:
- closed
branches: [ "v2" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login into docker hub
run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PW }}
- name: Build and Push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Moonlight/Dockerfile
push: true
tags: moonlightpanel/moonlight:dev
platforms: linux/amd64,linux/arm64
build-args: |
"BUILD_CHANNEL=${{ github.ref_name }}"
"BUILD_COMMIT_HASH=${{ github.sha }}"
"BUILD_NAME=devbuild ${{ steps.date.outputs.date }}"
"BUILD_VERSION=unknown"

View File

@@ -1,35 +0,0 @@
name: Release Build
on:
workflow_dispatch:
inputs:
codeName:
description: 'Code Name'
required: true
versionName:
description: 'Version Name'
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login into docker hub
run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PW }}
- name: Build and Push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Moonlight/Dockerfile
push: true
tags: moonlightpanel/moonlight:latest
platforms: linux/amd64,linux/arm64
build-args: |
"BUILD_CHANNEL=${{ github.ref_name }}"
"BUILD_COMMIT_HASH=${{ github.sha }}"
"BUILD_NAME=${{ github.event.inputs.codeName }}"
"BUILD_VERSION=${{ github.event.inputs.versionName }}"

17
.gitignore vendored
View File

@@ -1,4 +1,4 @@
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## files generated by popular Visual Studio add-ons.
## ##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
@@ -395,10 +395,13 @@ FodyWeavers.xsd
*.msp *.msp
# JetBrains Rider # JetBrains Rider
*.sln.iml **/.idea/**
storage/ # Style builds
.idea/.idea.Moonlight/.idea/dataSources.xml **/style.min.css
Moonlight/Assets/Core/css/theme.css **/package-lock.json
Moonlight/Assets/Core/css/theme.css.map
.idea/.idea.Moonlight/.idea/discord.xml # Secrets
**/.env
**/appsettings.json
**/appsettings.Development.json

View File

@@ -1,13 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/contentModel.xml
/projectSettingsUpdater.xml
/.idea.Moonlight.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
<option name="theme" value="material" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
</component>
</project>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="52a374ed:18c1029d858:-8000" />
<option name="version" value="8.13.2" />
</MTProjectMetadataState>
</option>
</component>
</project>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,10 @@
using MoonCore.PluginFramework;
using Moonlight.Api.Startup;
namespace Moonlight.Api.Host;
[PluginLoader]
public partial class AppStartupLoader : IAppStartup
{
}

View File

@@ -0,0 +1,70 @@
# Base image
FROM cgr.dev/chainguard/aspnet-runtime:latest AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# Build dependencies
RUN apt-get update; apt-get install nodejs npm -y; apt-get clean
# Build options
ARG BUILD_CONFIGURATION=Release
# Download npm packages
WORKDIR /src/Hosts/Moonlight.Frontend.Host/Styles
COPY ["Hosts/Moonlight.Frontend.Host/Styles/package.json", "package.json"]
RUN npm install
# Restore nuget packages
WORKDIR /src
COPY ["Moonlight.Api/Moonlight.Api.csproj", "Moonlight.Api/"]
COPY ["Moonlight.Frontend/Moonlight.Frontend.csproj", "Moonlight.Frontend/"]
COPY ["Moonlight.Shared/Moonlight.Shared.csproj", "Moonlight.Shared/"]
COPY ["Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj", "Hosts/Moonlight.Frontend.Host/"]
COPY ["Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj", "Hosts/Moonlight.Api.Host/"]
RUN dotnet restore "Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj"
RUN dotnet restore "Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj"
# Copy over the whole sources
COPY . .
# Build styles
# We need to build it before, so the class lists get generated
WORKDIR "/src/Hosts/Moonlight.Frontend.Host"
RUN dotnet build "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
WORKDIR "/src/Hosts/Moonlight.Frontend.Host/Styles"
RUN npm run build
# Build projects
WORKDIR "/src/Hosts/Moonlight.Api.Host"
RUN dotnet build "./Moonlight.Api.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-api
WORKDIR "/src/Hosts/Moonlight.Frontend.Host"
RUN dotnet build "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
FROM build AS publish
# Publish options
ARG BUILD_CONFIGURATION=Release
# Publish applications
WORKDIR "/src/Hosts/Moonlight.Api.Host"
RUN dotnet publish "./Moonlight.Api.Host.csproj" -c $BUILD_CONFIGURATION -o /app/publish-api /p:UseAppHost=false
WORKDIR "/src/Hosts/Moonlight.Frontend.Host"
RUN dotnet publish "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/publish-frontend /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish-api .
COPY --from=publish /app/publish-frontend/wwwroot ./wwwroot
ENTRYPOINT ["dotnet", "Moonlight.Api.Host.dll"]

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Moonlight.Api\Moonlight.Api.csproj"/>
<ProjectReference Include="..\Moonlight.Frontend.Host\Moonlight.Frontend.Host.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,22 @@
using Moonlight.Api.Host;
var appLoader = new AppStartupLoader();
appLoader.Initialize();
var builder = WebApplication.CreateBuilder(args);
appLoader.PreBuild(builder);
var app = builder.Build();
appLoader.PostBuild(app);
appLoader.PostMiddleware(app);
if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
await app.RunAsync();

View File

@@ -1,10 +1,11 @@
{ {
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": { "profiles": {
"http": { "http": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"applicationUrl": "http://0.0.0.0:5230", "applicationUrl": "http://localhost:5240",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@@ -0,0 +1,10 @@
using MoonCore.PluginFramework;
using Moonlight.Frontend.Startup;
namespace Moonlight.Frontend.Host;
[PluginLoader]
public partial class AppStartupLoader : IAppStartup
{
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<StaticWebAssetsEnabled>True</StaticWebAssetsEnabled>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
<WasmEnableSIMD>true</WasmEnableSIMD>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Moonlight.Frontend.Host;
var appLoader = new AppStartupLoader();
appLoader.Initialize();
var builder = WebAssemblyHostBuilder.CreateDefault(args);
appLoader.PreBuild(builder);
var app = builder.Build();
appLoader.PostBuild(app);
await app.RunAsync();

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5250",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,25 @@
import fs from 'fs';
import selectorParser from 'postcss-selector-parser';
export default function extractTailwindClasses(opts = {}) {
const classSet = new Set();
return {
postcssPlugin: 'extract-tailwind-classes',
Rule(rule) {
selectorParser(selectors => {
selectors.walkClasses(node => {
classSet.add(node.value);
});
}).processSync(rule.selector);
},
OnceExit() {
const classArray = Array.from(classSet).sort();
fs.mkdirSync('../../../Moonlight.Frontend/Styles', { recursive: true });
fs.writeFileSync('../../../Moonlight.Frontend/Styles/Moonlight.Frontend.map', classArray.join('\n'));
console.log(`Extracted classes ${classArray.length}`);
}
};
}
extractTailwindClasses.postcss = true;

View File

@@ -0,0 +1,19 @@
{
"scripts": {
"dev": "npx postcss styles.css -o ../wwwroot/style.min.css --watch",
"dev-build": "npx postcss styles.css -o ../wwwroot/style.min.css",
"build": "cross-env EXTRACT_CLASSES=true npx postcss styles.css -o ../wwwroot/style.min.css"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.18",
"cssnano": "^7.1.2",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"postcss-selector-parser": "^7.1.1",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"cross-env": "^10.1.0"
}
}

View File

@@ -0,0 +1,18 @@
import tailwindcss from '@tailwindcss/postcss';
import cssnano from 'cssnano';
import extractTailwindClasses from './extract-classes.mjs';
const plugins = [
tailwindcss,
cssnano({
preset: 'default'
})
];
if (process.env.EXTRACT_CLASSES === "true") {
plugins.push(extractTailwindClasses());
}
export default {
plugins
};

View File

@@ -0,0 +1,31 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/scrollbar.css";
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/default-theme.css";
@import "./theme.css";
@source "../../../Moonlight.Frontend/bin/ShadcnBlazor/ShadcnBlazor.map";
@source "../../../Moonlight.Api/**/*.razor";
@source "../../../Moonlight.Api/**/*.cs";
@source "../../../Moonlight.Api/**/*.html";
@source "../../../Moonlight.Frontend/**/*.razor";
@source "../../../Moonlight.Frontend/**/*.cs";
@source "../../../Moonlight.Frontend/**/*.html";
@custom-variant dark (&:is(.dark *));
#blazor-error-ui {
display: none;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,132 @@
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
--font-sans: Inter, sans-serif;
--font-serif: Georgia, serif;
}
.dark {
/* Deep blue-slate background with purple undertones */
--background: oklch(0.16 0.028 260);
--foreground: oklch(0.98 0.008 260);
/* Cards with slightly lighter blue-slate */
--card: oklch(0.21 0.032 260);
--card-foreground: oklch(0.98 0.008 260);
/* Popovers with medium depth */
--popover: oklch(0.24 0.035 260);
--popover-foreground: oklch(0.98 0.008 260);
/* Vibrant blue-purple primary */
--primary: oklch(0.62 0.18 270);
--primary-foreground: oklch(0.99 0.005 260);
/* Secondary with blue-slate tone */
--secondary: oklch(0.27 0.038 260);
--secondary-foreground: oklch(0.98 0.008 260);
/* Muted elements */
--muted: oklch(0.25 0.035 260);
--muted-foreground: oklch(0.66 0.025 260);
/* Accent with purple-blue blend */
--accent: oklch(0.36 0.065 268);
--accent-foreground: oklch(0.98 0.008 260);
/* Destructive red with good contrast */
--destructive: oklch(0.62 0.22 25);
--destructive-foreground: oklch(0.99 0.005 260);
/* Subtle borders and inputs */
--border: oklch(0.32 0.025 260);
--input: oklch(0.30 0.030 260);
--ring: oklch(0.62 0.18 270);
/* Chart colors with blue-purple harmony */
--chart-1: oklch(0.58 0.18 270);
--chart-2: oklch(0.62 0.16 245);
--chart-3: oklch(0.68 0.15 290);
--chart-4: oklch(0.60 0.20 260);
--chart-5: oklch(0.65 0.14 280);
/* Sidebar with slightly different depth */
--sidebar: oklch(0.18 0.030 260);
--sidebar-foreground: oklch(0.97 0.008 260);
--sidebar-primary: oklch(0.60 0.17 270);
--sidebar-primary-foreground: oklch(0.99 0.005 260);
--sidebar-accent: oklch(0.26 0.038 260);
--sidebar-accent-foreground: oklch(0.98 0.008 260);
--sidebar-border: oklch(0.30 0.025 260);
--sidebar-ring: oklch(0.58 0.17 270);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Moonlight</title>
<base href="/" />
<link rel="preload" id="webassembly" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback" />
<link rel="stylesheet" href="style.min.css" />
<script type="importmap"></script>
<script>
window.frontendConfig = {
STYLE_TAG_ID: 'theme-variables',
configuration: {},
applyTheme: function(cssContent) {
// Find or create the style tag
let styleTag = document.getElementById(this.STYLE_TAG_ID);
if (!styleTag) {
styleTag = document.createElement('style');
styleTag.id = this.STYLE_TAG_ID;
document.head.appendChild(styleTag);
}
// Update the style tag content
styleTag.textContent = cssContent;
},
reloadConfiguration: function (){
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/frontend/config', false);
xhr.send(null);
if (xhr.status === 200) {
this.configuration = JSON.parse(xhr.responseText);
}
} catch (error) {
console.error('Failed to load initial theme:', error);
}
},
getConfiguration: function (){
return this.configuration;
},
reload: function () {
this.reloadConfiguration();
document.title = this.configuration.name;
this.applyTheme(this.configuration.themeCss);
}
};
window.frontendConfig.reload();
</script>
</head>
<body class="bg-background text-foreground">
<div id="app">
<div class="h-screen w-full flex items-center justify-center">
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-3 rounded-lg border-dashed p-6 text-center text-balance md:p-12">
<div class="flex max-w-sm flex-col items-center gap-2 text-center">
<div class="flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground size-10 rounded-lg [&_svg:not([class*='size-'])]:size-6">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-zap-icon lucide-zap size-6">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</svg>
</div>
</div>
<div class="text-lg font-medium tracking-tight">
Loading application
</div>
<div class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<div class="bg-primary/20 w-full relative h-2 overflow-hidden rounded-full">
<div class="bg-primary h-full w-[var(--blazor-load-percentage,0)] flex-1 transition-all">
</div>
</div>
</div>
</div>
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="/_content/ShadcnBlazor/interop.js" defer></script>
<script src="/_content/ShadcnBlazor.Extras/interop.js" defer></script>
<script src="/_content/ShadcnBlazor.Extras/codemirror-bundle.js" defer></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>

121
LICENSE
View File

@@ -1,121 +0,0 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Api.Configuration;
public class ApiOptions
{
public int LookupCacheMinutes { get; set; } = 3;
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.Api.Configuration;
public class ContainerHelperOptions
{
public bool IsEnabled { get; set; }
public string Url { get; set; } = "http://helper:8080";
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.Api.Configuration;
public class DatabaseOptions
{
public string Host { get; set; }
public int Port { get; set; } = 5432;
public string Username { get; set; }
public string Password { get; set; }
public string Database { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.Api.Configuration;
public class FrontendOptions
{
public bool Enabled { get; set; } = true;
public int CacheMinutes { get; set; } = 3;
}

View File

@@ -0,0 +1,11 @@
namespace Moonlight.Api.Configuration;
public class OidcOptions
{
public string Authority { get; set; }
public bool RequireHttpsMetadata { get; set; } = true;
public string ResponseType { get; set; } = "code";
public string[]? Scopes { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Api.Configuration;
public class SessionOptions
{
public int ValidationCacheMinutes { get; set; } = 3;
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Api.Configuration;
public class SettingsOptions
{
public int CacheMinutes { get; set; } = 3;
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.Api.Configuration;
public class VersionOptions
{
public bool OfflineMode { get; set; }
public string? CurrentOverride { get; set; }
}

View File

@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Database;
public class DataContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<SettingsOption> SettingsOptions { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<RoleMember> RoleMembers { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Theme> Themes { get; set; }
private readonly IOptions<DatabaseOptions> Options;
public DataContext(IOptions<DatabaseOptions> options)
{
Options = options;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
return;
optionsBuilder.UseNpgsql(
$"Host={Options.Value.Host};" +
$"Port={Options.Value.Port};" +
$"Username={Options.Value.Username};" +
$"Password={Options.Value.Password};" +
$"Database={Options.Value.Database}"
);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("core");
base.OnModelCreating(modelBuilder);
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Database;
public class DatabaseRepository<T> where T : class
{
private readonly DataContext DataContext;
private readonly DbSet<T> Set;
public DatabaseRepository(DataContext dataContext)
{
DataContext = dataContext;
Set = DataContext.Set<T>();
}
public IQueryable<T> Query() => Set;
public async Task<T> AddAsync(T entity)
{
if (entity is IActionTimestamps actionTimestamps)
{
actionTimestamps.CreatedAt = DateTimeOffset.UtcNow;
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
}
var final = Set.Add(entity);
await DataContext.SaveChangesAsync();
return final.Entity;
}
public async Task UpdateAsync(T entity)
{
if (entity is IActionTimestamps actionTimestamps)
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
Set.Update(entity);
await DataContext.SaveChangesAsync();
}
public async Task RemoveAsync(T entity)
{
Set.Remove(entity);
await DataContext.SaveChangesAsync();
}
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Database.Entities;
public class ApiKey : IActionTimestamps
{
public int Id { get; set; }
[MaxLength(30)]
public required string Name { get; set; }
[MaxLength(300)]
public required string Description { get; set; }
public string[] Permissions { get; set; } = [];
[MaxLength(32)]
public string Key { get; set; }
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Database.Entities;
public class Role : IActionTimestamps
{
public int Id { get; set; }
[MaxLength(30)]
public required string Name { get; set; }
[MaxLength(300)]
public required string Description { get; set; }
public string[] Permissions { get; set; } = [];
// Relations
public List<RoleMember> Members { get; set; } = [];
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,15 @@
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Database.Entities;
public class RoleMember : IActionTimestamps
{
public int Id { get; set; }
public Role Role { get; set; }
public User User { get; set; }
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Moonlight.Api.Database.Entities;
public class SettingsOption
{
public int Id { get; set; }
[MaxLength(256)]
public required string Key { get; set; }
[MaxLength(4096)]
[Column(TypeName = "jsonb")]
public required string ValueJson { get; set; }
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Api.Database.Entities;
public class Theme
{
public int Id { get; set; }
[MaxLength(30)]
public required string Name { get; set; }
[MaxLength(30)]
public required string Version { get; set; }
[MaxLength(30)]
public required string Author { get; set; }
public bool IsEnabled { get; set; }
[MaxLength(20_000)]
public required string CssContent { get; set; }
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces;
namespace Moonlight.Api.Database.Entities;
public class User : IActionTimestamps
{
public int Id { get; set; }
// Base information
[MaxLength(50)]
public required string Username { get; set; }
[MaxLength(254)]
public required string Email { get; set; }
// Authentication
public DateTimeOffset InvalidateTimestamp { get; set; }
// Relations
public List<RoleMember> RoleMemberships { get; set; } = [];
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.Api.Database.Interfaces;
internal interface IActionTimestamps
{
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,80 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20251225202335_AddedUsersAndSettings")]
partial class AddedUsersAndSettings
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("core")
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasKey("Id");
b.ToTable("SettingsOptions", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(254)
.HasColumnType("character varying(254)");
b.Property<DateTimeOffset>("InvalidateTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Users", "core");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,62 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
/// <inheritdoc />
public partial class AddedUsersAndSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "core");
migrationBuilder.CreateTable(
name: "SettingsOptions",
schema: "core",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Value = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SettingsOptions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
schema: "core",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Username = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Email = table.Column<string>(type: "character varying(254)", maxLength: 254, nullable: false),
InvalidateTimestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SettingsOptions",
schema: "core");
migrationBuilder.DropTable(
name: "Users",
schema: "core");
}
}
}

View File

@@ -0,0 +1,177 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20251230200748_AddedRolesAndActionTimestamps")]
partial class AddedRolesAndActionTimestamps
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("core")
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(15)
.HasColumnType("character varying(15)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Roles", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("RoleId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("UserId");
b.ToTable("RoleMembers", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasKey("Id");
b.ToTable("SettingsOptions", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(254)
.HasColumnType("character varying(254)");
b.Property<DateTimeOffset>("InvalidateTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Users", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.HasOne("Moonlight.Api.Database.Entities.Role", "Role")
.WithMany("Members")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.Api.Database.Entities.User", "User")
.WithMany("RoleMemberships")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Navigation("RoleMemberships");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,115 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
/// <inheritdoc />
public partial class AddedRolesAndActionTimestamps : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "CreatedAt",
schema: "core",
table: "Users",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
migrationBuilder.AddColumn<DateTimeOffset>(
name: "UpdatedAt",
schema: "core",
table: "Users",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
migrationBuilder.CreateTable(
name: "Roles",
schema: "core",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "character varying(15)", maxLength: 15, nullable: false),
Description = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Permissions = table.Column<string[]>(type: "text[]", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Roles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "RoleMembers",
schema: "core",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<int>(type: "integer", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RoleMembers", x => x.Id);
table.ForeignKey(
name: "FK_RoleMembers_Roles_RoleId",
column: x => x.RoleId,
principalSchema: "core",
principalTable: "Roles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RoleMembers_Users_UserId",
column: x => x.UserId,
principalSchema: "core",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_RoleMembers_RoleId",
schema: "core",
table: "RoleMembers",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "IX_RoleMembers_UserId",
schema: "core",
table: "RoleMembers",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "RoleMembers",
schema: "core");
migrationBuilder.DropTable(
name: "Roles",
schema: "core");
migrationBuilder.DropColumn(
name: "CreatedAt",
schema: "core",
table: "Users");
migrationBuilder.DropColumn(
name: "UpdatedAt",
schema: "core",
table: "Users");
}
}
}

View File

@@ -0,0 +1,215 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20260116133404_AddedApiKeys")]
partial class AddedApiKeys
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("core")
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.Api.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(15)
.HasColumnType("character varying(15)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ApiKeys", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(15)
.HasColumnType("character varying(15)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Roles", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("RoleId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("UserId");
b.ToTable("RoleMembers", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasKey("Id");
b.ToTable("SettingsOptions", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(254)
.HasColumnType("character varying(254)");
b.Property<DateTimeOffset>("InvalidateTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Users", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.HasOne("Moonlight.Api.Database.Entities.Role", "Role")
.WithMany("Members")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.Api.Database.Entities.User", "User")
.WithMany("RoleMemberships")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Navigation("RoleMemberships");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
/// <inheritdoc />
public partial class AddedApiKeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ApiKeys",
schema: "core",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "character varying(15)", maxLength: 15, nullable: false),
Description = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Permissions = table.Column<string[]>(type: "text[]", nullable: false),
Key = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiKeys", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApiKeys",
schema: "core");
}
}
}

View File

@@ -0,0 +1,215 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20260116134322_AdjustedLenghtsOfRoleAndApiKeyStrings")]
partial class AdjustedLenghtsOfRoleAndApiKeyStrings
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("core")
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.Api.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ApiKeys", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Roles", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("RoleId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("UserId");
b.ToTable("RoleMembers", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasKey("Id");
b.ToTable("SettingsOptions", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(254)
.HasColumnType("character varying(254)");
b.Property<DateTimeOffset>("InvalidateTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Users", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.HasOne("Moonlight.Api.Database.Entities.Role", "Role")
.WithMany("Members")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.Api.Database.Entities.User", "User")
.WithMany("RoleMemberships")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Navigation("RoleMemberships");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,106 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
/// <inheritdoc />
public partial class AdjustedLenghtsOfRoleAndApiKeyStrings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Name",
schema: "core",
table: "Roles",
type: "character varying(30)",
maxLength: 30,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(15)",
oldMaxLength: 15);
migrationBuilder.AlterColumn<string>(
name: "Description",
schema: "core",
table: "Roles",
type: "character varying(300)",
maxLength: 300,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(100)",
oldMaxLength: 100);
migrationBuilder.AlterColumn<string>(
name: "Name",
schema: "core",
table: "ApiKeys",
type: "character varying(30)",
maxLength: 30,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(15)",
oldMaxLength: 15);
migrationBuilder.AlterColumn<string>(
name: "Description",
schema: "core",
table: "ApiKeys",
type: "character varying(300)",
maxLength: 300,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(100)",
oldMaxLength: 100);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Name",
schema: "core",
table: "Roles",
type: "character varying(15)",
maxLength: 15,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(30)",
oldMaxLength: 30);
migrationBuilder.AlterColumn<string>(
name: "Description",
schema: "core",
table: "Roles",
type: "character varying(100)",
maxLength: 100,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(300)",
oldMaxLength: 300);
migrationBuilder.AlterColumn<string>(
name: "Name",
schema: "core",
table: "ApiKeys",
type: "character varying(15)",
maxLength: 15,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(30)",
oldMaxLength: 30);
migrationBuilder.AlterColumn<string>(
name: "Description",
schema: "core",
table: "ApiKeys",
type: "character varying(100)",
maxLength: 100,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(300)",
oldMaxLength: 300);
}
}
}

View File

@@ -0,0 +1,251 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20260118005634_AddedThemes")]
partial class AddedThemes
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("core")
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.Api.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ApiKeys", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Roles", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("RoleId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("UserId");
b.ToTable("RoleMembers", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasKey("Id");
b.ToTable("SettingsOptions", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Theme", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Author")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("CssContent")
.IsRequired()
.HasMaxLength(20000)
.HasColumnType("character varying(20000)");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.HasKey("Id");
b.ToTable("Themes", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(254)
.HasColumnType("character varying(254)");
b.Property<DateTimeOffset>("InvalidateTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Users", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.HasOne("Moonlight.Api.Database.Entities.Role", "Role")
.WithMany("Members")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.Api.Database.Entities.User", "User")
.WithMany("RoleMemberships")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Navigation("RoleMemberships");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
/// <inheritdoc />
public partial class AddedThemes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Themes",
schema: "core",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
Version = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
Author = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
CssContent = table.Column<string>(type: "character varying(20000)", maxLength: 20000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Themes", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Themes",
schema: "core");
}
}
}

View File

@@ -0,0 +1,251 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20260129134620_SwitchedToJsonForSettingsOption")]
partial class SwitchedToJsonForSettingsOption
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("core")
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.Api.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ApiKeys", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Roles", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("RoleId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("UserId");
b.ToTable("RoleMembers", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ValueJson")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("SettingsOptions", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Theme", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Author")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("CssContent")
.IsRequired()
.HasMaxLength(20000)
.HasColumnType("character varying(20000)");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.HasKey("Id");
b.ToTable("Themes", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(254)
.HasColumnType("character varying(254)");
b.Property<DateTimeOffset>("InvalidateTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Users", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.HasOne("Moonlight.Api.Database.Entities.Role", "Role")
.WithMany("Members")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.Api.Database.Entities.User", "User")
.WithMany("RoleMemberships")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Navigation("RoleMemberships");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
/// <inheritdoc />
public partial class SwitchedToJsonForSettingsOption : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Value",
schema: "core",
table: "SettingsOptions");
migrationBuilder.AddColumn<string>(
name: "ValueJson",
schema: "core",
table: "SettingsOptions",
type: "jsonb",
maxLength: 4096,
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ValueJson",
schema: "core",
table: "SettingsOptions");
migrationBuilder.AddColumn<string>(
name: "Value",
schema: "core",
table: "SettingsOptions",
type: "character varying(4096)",
maxLength: 4096,
nullable: false,
defaultValue: "");
}
}
}

View File

@@ -0,0 +1,248 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
partial class DataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("core")
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.Api.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ApiKeys", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Roles", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("RoleId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("UserId");
b.ToTable("RoleMembers", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ValueJson")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("SettingsOptions", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Theme", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Author")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("CssContent")
.IsRequired()
.HasMaxLength(20000)
.HasColumnType("character varying(20000)");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.HasKey("Id");
b.ToTable("Themes", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(254)
.HasColumnType("character varying(254)");
b.Property<DateTimeOffset>("InvalidateTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Users", "core");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
{
b.HasOne("Moonlight.Api.Database.Entities.Role", "Role")
.WithMany("Members")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.Api.Database.Entities.User", "User")
.WithMany("RoleMemberships")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Navigation("RoleMemberships");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace Moonlight.Api.Helpers;
public class AppConsoleFormatter : ConsoleFormatter
{
private const string TimestampColor = "\e[38;2;148;148;148m";
private const string CategoryColor = "\e[38;2;198;198;198m";
private const string MessageColor = "\e[38;2;255;255;255m";
private const string Bold = "\e[1m";
// Pre-computed ANSI color codes for each log level
private const string CriticalColor = "\e[38;2;255;0;0m";
private const string ErrorColor = "\e[38;2;255;0;0m";
private const string WarningColor = "\e[38;2;215;215;0m";
private const string InfoColor = "\e[38;2;135;215;255m";
private const string DebugColor = "\e[38;2;198;198;198m";
private const string TraceColor = "\e[38;2;68;68;68m";
public AppConsoleFormatter() : base(nameof(AppConsoleFormatter))
{
}
public override void Write<TState>(
in LogEntry<TState> logEntry,
IExternalScopeProvider? scopeProvider,
TextWriter textWriter)
{
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
// Timestamp
textWriter.Write(TimestampColor);
textWriter.Write(DateTime.Now.ToString("dd.MM.yy HH:mm:ss"));
textWriter.Write(' ');
// Log level with color and bold
var (levelText, levelColor) = GetLevelInfo(logEntry.LogLevel);
textWriter.Write(levelColor);
textWriter.Write(Bold);
textWriter.Write(levelText);
textWriter.Write(' ');
// Category
textWriter.Write(CategoryColor);
textWriter.Write(logEntry.Category);
// Message
textWriter.Write(MessageColor);
textWriter.Write(": ");
textWriter.Write(message);
// Exception
if (logEntry.Exception != null)
{
textWriter.Write(MessageColor);
textWriter.WriteLine(logEntry.Exception.ToString());
}
else
textWriter.WriteLine();
}
private static (string text, string color) GetLevelInfo(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Critical => ("CRIT", CriticalColor),
LogLevel.Error => ("ERRO", ErrorColor),
LogLevel.Warning => ("WARN", WarningColor),
LogLevel.Information => ("INFO", InfoColor),
LogLevel.Debug => ("DEBG", DebugColor),
LogLevel.Trace => ("TRCE", TraceColor),
_ => ("NONE", "")
};
}
}

View File

@@ -0,0 +1,98 @@
using System.Runtime.InteropServices;
namespace Moonlight.Api.Helpers;
public class OsHelper
{
public static string GetName()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return GetWindowsVersion();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return GetLinuxVersion();
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return "Goofy OS";
return "Unknown OS";
}
private static string GetWindowsVersion()
{
var version = Environment.OSVersion.Version;
// Windows 11 is version 10.0 build 22000+
if (version.Major == 10 && version.Build >= 22000)
return $"Windows 11 ({version.Build})";
if (version.Major == 10)
return $"Windows 10 ({version.Build})";
if (version.Major == 6 && version.Minor == 3)
return "Windows 8.1";
if (version.Major == 6 && version.Minor == 2)
return "Windows 8";
if (version.Major == 6 && version.Minor == 1)
return "Windows 7";
return $"Windows {version.Major}.{version.Minor}";
}
private static string GetLinuxVersion()
{
try
{
// Read /etc/os-release, should work everywhere
if (File.Exists("/etc/os-release"))
{
var lines = File.ReadAllLines("/etc/os-release");
string? name = null;
string? version = null;
foreach (var line in lines)
{
if (line.StartsWith("NAME="))
name = line.Substring(5).Trim('"');
else if (line.StartsWith("VERSION_ID="))
version = line.Substring(11).Trim('"');
}
if (!string.IsNullOrEmpty(name))
{
return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
}
}
//If for some weird reason it still uses lsb release
if (File.Exists("/etc/lsb-release"))
{
var lines = File.ReadAllLines("/etc/lsb-release");
string? name = null;
string? version = null;
foreach (var line in lines)
{
if (line.StartsWith("DISTRIB_ID="))
name = line.Substring(11);
else if (line.StartsWith("DISTRIB_RELEASE="))
version = line.Substring(16);
}
if (!string.IsNullOrEmpty(name))
{
return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
}
}
}
catch
{
// Ignore
}
// Fallback
return $"Linux {Environment.OSVersion.Version}";
}
}

View File

@@ -0,0 +1,134 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
namespace Moonlight.Api.Http.Controllers.Admin;
[Authorize]
[ApiController]
[Route("api/admin/apiKeys")]
public class ApiKeyController : Controller
{
private readonly DatabaseRepository<ApiKey> KeyRepository;
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository)
{
KeyRepository = keyRepository;
}
[HttpGet]
[Authorize(Policy = Permissions.ApiKeys.View)]
public async Task<ActionResult<PagedData<ApiKeyDto>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int length,
[FromQuery] FilterOptions? filterOptions
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
// Query building
var query = KeyRepository.Query();
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(ApiKey.Name) =>
query.Where(k => EF.Functions.ILike(k.Name, $"%{filterOption.Value}%")),
nameof(ApiKey.Description) =>
query.Where(k => EF.Functions.ILike(k.Description, $"%{filterOption.Value}%")),
_ => query
};
}
}
// Pagination
var data = await query
.OrderBy(k => k.Id)
.ProjectToDto()
.Skip(startIndex)
.Take(length)
.ToArrayAsync();
var total = await query.CountAsync();
return new PagedData<ApiKeyDto>(data, total);
}
[HttpGet("{id:int}")]
[Authorize(Policy = Permissions.ApiKeys.View)]
public async Task<ActionResult<ApiKeyDto>> GetAsync([FromRoute] int id)
{
var key = await KeyRepository
.Query()
.FirstOrDefaultAsync(k => k.Id == id);
if (key == null)
return Problem("No API key with this id found", statusCode: 404);
return ApiKeyMapper.ToDto(key);
}
[HttpPost]
[Authorize(Policy = Permissions.ApiKeys.Create)]
public async Task<ActionResult<ApiKeyDto>> CreateAsync([FromBody] CreateApiKeyDto request)
{
var apiKey = ApiKeyMapper.ToEntity(request);
apiKey.Key = Guid.NewGuid().ToString("N").Substring(0, 32);
var finalKey = await KeyRepository.AddAsync(apiKey);
return ApiKeyMapper.ToDto(finalKey);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = Permissions.ApiKeys.Edit)]
public async Task<ActionResult<ApiKeyDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateApiKeyDto request)
{
var apiKey = await KeyRepository
.Query()
.FirstOrDefaultAsync(k => k.Id == id);
if (apiKey == null)
return Problem("No API key with this id found", statusCode: 404);
ApiKeyMapper.Merge(apiKey, request);
await KeyRepository.UpdateAsync(apiKey);
return ApiKeyMapper.ToDto(apiKey);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = Permissions.ApiKeys.Delete)]
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
var apiKey = await KeyRepository
.Query()
.FirstOrDefaultAsync(k => k.Id == id);
if (apiKey == null)
return Problem("No API key with this id found", statusCode: 404);
await KeyRepository.RemoveAsync(apiKey);
return NoContent();
}
}

View File

@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared.Http.Events;
using Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/ch")]
public class ContainerHelperController : Controller
{
private readonly ContainerHelperService ContainerHelperService;
private readonly IOptions<ContainerHelperOptions> Options;
public ContainerHelperController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
{
ContainerHelperService = containerHelperService;
Options = options;
}
[HttpGet("status")]
public async Task<ActionResult<ContainerHelperStatusDto>> GetStatusAsync()
{
if (!Options.Value.IsEnabled)
return new ContainerHelperStatusDto(false, false);
var status = await ContainerHelperService.CheckConnectionAsync();
return new ContainerHelperStatusDto(true, status);
}
[HttpPost("rebuild")]
public Task<IResult> RebuildAsync([FromBody] RequestRebuildDto request)
{
var result = ContainerHelperService.RebuildAsync(request.NoBuildCache);
var mappedResult = result.Select(ContainerHelperMapper.ToDto);
return Task.FromResult<IResult>(
TypedResults.ServerSentEvents(mappedResult)
);
}
[HttpPost("version")]
public async Task<ActionResult> SetVersionAsync([FromBody] SetVersionDto request)
{
await ContainerHelperService.SetVersionAsync(request.Version);
return NoContent();
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Authorize(Policy = Permissions.System.Diagnose)]
[Route("api/admin/system/diagnose")]
public class DiagnoseController : Controller
{
private readonly DiagnoseService DiagnoseService;
public DiagnoseController(DiagnoseService diagnoseService)
{
DiagnoseService = diagnoseService;
}
[HttpGet]
public async Task<ActionResult<DiagnoseResultDto[]>> GetAsync()
{
var results = await DiagnoseService.DiagnoseAsync();
return results
.OrderBy(x => x.Level)
.ToDto()
.ToArray();
}
}

View File

@@ -0,0 +1,171 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Authorize(Policy = Permissions.Roles.Members)]
[Route("api/admin/roles/{roleId:int}/members")]
public class RoleMembersController : Controller
{
private readonly DatabaseRepository<User> UsersRepository;
private readonly DatabaseRepository<Role> RolesRepository;
private readonly DatabaseRepository<RoleMember> RoleMembersRepository;
public RoleMembersController(
DatabaseRepository<User> usersRepository,
DatabaseRepository<Role> rolesRepository,
DatabaseRepository<RoleMember> roleMembersRepository
)
{
UsersRepository = usersRepository;
RolesRepository = rolesRepository;
RoleMembersRepository = roleMembersRepository;
}
[HttpGet]
public async Task<ActionResult<PagedData<UserDto>>> GetAsync(
[FromRoute] int roleId,
[FromQuery] int startIndex, [FromQuery] int length,
[FromQuery] string? searchTerm
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
// Query building
var query = RoleMembersRepository
.Query()
.Where(x => x.Role.Id == roleId)
.Select(x => x.User);
// Filtering
if (!string.IsNullOrWhiteSpace(searchTerm))
{
query = query.Where(x =>
EF.Functions.ILike(x.Username, $"%{searchTerm}%") ||
EF.Functions.ILike(x.Email, $"%{searchTerm}%")
);
}
// Pagination
var items = query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length)
.ProjectToDto()
.ToArray();
var totalCount = await query.CountAsync();
return new PagedData<UserDto>(items, totalCount);
}
[HttpGet("available")]
public async Task<ActionResult<PagedData<UserDto>>> GetAvailableAsync(
[FromRoute] int roleId,
[FromQuery] int startIndex, [FromQuery] int length,
[FromQuery] string? searchTerm
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
// Query building
var query = UsersRepository
.Query()
.Where(x => x.RoleMemberships.All(y => y.Role.Id != roleId));
// Filtering
if (!string.IsNullOrWhiteSpace(searchTerm))
{
query = query.Where(x =>
EF.Functions.ILike(x.Username, $"%{searchTerm}%") ||
EF.Functions.ILike(x.Email, $"%{searchTerm}%")
);
}
// Pagination
var items = query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(length)
.ProjectToDto()
.ToArray();
var totalCount = await query.CountAsync();
return new PagedData<UserDto>(items, totalCount);
}
[HttpPut("{userId:int}")]
public async Task<ActionResult> AddMemberAsync([FromRoute] int roleId, [FromRoute] int userId)
{
// Check and load role
var role = await RolesRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == roleId);
if (role == null)
return Problem("Role not found", statusCode: 404);
// Check and load user
var user = await UsersRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == userId);
if (user == null)
return Problem("User not found", statusCode: 404);
// Check if a role member already exists with these details
var isUserInRole = await RoleMembersRepository
.Query()
.AnyAsync(x => x.Role.Id == roleId && x.User.Id == userId);
if (isUserInRole)
return Problem("User is already a member of this role", statusCode: 400);
var roleMember = new RoleMember
{
Role = role,
User = user
};
await RoleMembersRepository.AddAsync(roleMember);
return NoContent();
}
[HttpDelete("{userId:int}")]
public async Task<ActionResult> RemoveMemberAsync([FromRoute] int roleId, [FromRoute] int userId)
{
var roleMember = await RoleMembersRepository
.Query()
.FirstOrDefaultAsync(x => x.User.Id == userId && x.Role.Id == roleId);
if (roleMember == null)
return Problem("User is not a member of this role, the role does not exist or the user does not exist",
statusCode: 404);
await RoleMembersRepository.RemoveAsync(roleMember);
return NoContent();
}
}

View File

@@ -0,0 +1,131 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/roles")]
public class RolesController : Controller
{
private readonly DatabaseRepository<Role> RoleRepository;
public RolesController(DatabaseRepository<Role> roleRepository)
{
RoleRepository = roleRepository;
}
[HttpGet]
[Authorize(Policy = Permissions.Roles.View)]
public async Task<ActionResult<PagedData<RoleDto>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int length,
[FromQuery] FilterOptions? filterOptions
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
// Query building
var query = RoleRepository
.Query();
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(Role.Name) =>
query.Where(role => EF.Functions.ILike(role.Name, $"%{filterOption.Value}%")),
_ => query
};
}
}
// Pagination
var data = await query
.OrderBy(x => x.Id)
.ProjectToDto()
.Skip(startIndex)
.Take(length)
.ToArrayAsync();
var total = await query.CountAsync();
return new PagedData<RoleDto>(data, total);
}
[HttpGet("{id:int}")]
[Authorize(Policy = Permissions.Roles.View)]
public async Task<ActionResult<RoleDto>> GetAsync([FromRoute] int id)
{
var role = await RoleRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (role == null)
return Problem("No role with this id found", statusCode: 404);
return RoleMapper.ToDto(role);
}
[HttpPost]
[Authorize(Policy = Permissions.Roles.Create)]
public async Task<ActionResult<RoleDto>> CreateAsync([FromBody] CreateRoleDto request)
{
var role = RoleMapper.ToEntity(request);
var finalRole = await RoleRepository.AddAsync(role);
return RoleMapper.ToDto(finalRole);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = Permissions.Roles.Edit)]
public async Task<ActionResult<RoleDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateRoleDto request)
{
var role = await RoleRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (role == null)
return Problem("No role with this id found", statusCode: 404);
RoleMapper.Merge(role, request);
await RoleRepository.UpdateAsync(role);
return RoleMapper.ToDto(role);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = Permissions.Roles.Delete)]
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
var role = await RoleRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (role == null)
return Problem("No role with this id found", statusCode: 404);
await RoleRepository.RemoveAsync(role);
return NoContent();
}
}

View File

@@ -0,0 +1,127 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests.Seup;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/setup")]
public class SetupController : Controller
{
private readonly SettingsService SettingsService;
private readonly DatabaseRepository<User> UsersRepository;
private readonly DatabaseRepository<Role> RolesRepository;
private const string StateSettingsKey = "Moonlight.Api.Setup.State";
public SetupController(
SettingsService settingsService,
DatabaseRepository<User> usersRepository,
DatabaseRepository<Role> rolesRepository
)
{
SettingsService = settingsService;
UsersRepository = usersRepository;
RolesRepository = rolesRepository;
}
[HttpGet]
[AllowAnonymous]
public async Task<ActionResult> GetSetupAsync()
{
var hasBeenSetup = await SettingsService.GetValueAsync<bool>(StateSettingsKey);
if (hasBeenSetup)
return Problem("This instance is already configured", statusCode: 405);
return NoContent();
}
[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> ApplySetupAsync([FromBody] ApplySetupDto dto)
{
var adminRole = await RolesRepository
.Query()
.FirstOrDefaultAsync(x => x.Name == "Administrators");
if (adminRole == null)
{
adminRole = await RolesRepository.AddAsync(new Role()
{
Name = "Administrators",
Description = "Automatically generated group for full administrator permissions",
Permissions = [
Permissions.ApiKeys.View,
Permissions.ApiKeys.Create,
Permissions.ApiKeys.Edit,
Permissions.ApiKeys.Delete,
Permissions.Roles.View,
Permissions.Roles.Create,
Permissions.Roles.Edit,
Permissions.Roles.Delete,
Permissions.Roles.Members,
Permissions.Users.View,
Permissions.Users.Create,
Permissions.Users.Edit,
Permissions.Users.Delete,
Permissions.Users.Logout,
Permissions.Themes.View,
Permissions.Themes.Create,
Permissions.Themes.Edit,
Permissions.Themes.Delete,
Permissions.System.Info,
Permissions.System.Diagnose,
]
});
}
var user = await UsersRepository
.Query()
.FirstOrDefaultAsync(u => u.Email == dto.AdminEmail);
if (user == null)
{
await UsersRepository.AddAsync(new User()
{
Email = dto.AdminEmail,
Username = dto.AdminUsername,
RoleMemberships = [
new RoleMember()
{
Role = adminRole,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
}
],
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
}
else
{
user.RoleMemberships.Add(new RoleMember()
{
Role = adminRole,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
await UsersRepository.UpdateAsync(user);
}
await SettingsService.SetValueAsync(StateSettingsKey, true);
return NoContent();
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/system")]
public class SystemController : Controller
{
private readonly ApplicationService ApplicationService;
public SystemController(ApplicationService applicationService)
{
ApplicationService = applicationService;
}
[HttpGet("info")]
[Authorize(Policy = Permissions.System.Info)]
public async Task<ActionResult<SystemInfoDto>> GetInfoAsync()
{
var cpuUsage = await ApplicationService.GetCpuUsageAsync();
var memoryUsage = await ApplicationService.GetMemoryUsageAsync();
return new SystemInfoDto(
cpuUsage,
memoryUsage,
ApplicationService.OperatingSystem,
DateTimeOffset.UtcNow - ApplicationService.StartedAt,
ApplicationService.VersionName,
ApplicationService.IsUpToDate
);
}
}

View File

@@ -0,0 +1,145 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Themes;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/themes")]
public class ThemesController : Controller
{
private readonly DatabaseRepository<Theme> ThemeRepository;
private readonly FrontendService FrontendService;
public ThemesController(DatabaseRepository<Theme> themeRepository, FrontendService frontendService)
{
ThemeRepository = themeRepository;
FrontendService = frontendService;
}
[HttpGet]
[Authorize(Policy = Permissions.Themes.View)]
public async Task<ActionResult<PagedData<ThemeDto>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int length,
[FromQuery] FilterOptions? filterOptions
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
// Query building
var query = ThemeRepository
.Query();
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(Theme.Name) =>
query.Where(user => EF.Functions.ILike(user.Name, $"%{filterOption.Value}%")),
nameof(Theme.Version) =>
query.Where(user => EF.Functions.ILike(user.Version, $"%{filterOption.Value}%")),
nameof(Theme.Author) =>
query.Where(user => EF.Functions.ILike(user.Author, $"%{filterOption.Value}%")),
_ => query
};
}
}
// Pagination
var data = await query
.OrderBy(x => x.Id)
.ProjectToDto()
.Skip(startIndex)
.Take(length)
.ToArrayAsync();
var total = await query.CountAsync();
return new PagedData<ThemeDto>(data, total);
}
[HttpGet("{id:int}")]
[Authorize(Policy = Permissions.Themes.View)]
public async Task<ActionResult<ThemeDto>> GetAsync([FromRoute] int id)
{
var item = await ThemeRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (item == null)
return Problem("No theme with this id", statusCode: 404);
return ThemeMapper.ToDto(item);
}
[HttpPost]
[Authorize(Policy = Permissions.Themes.Create)]
public async Task<ActionResult<ThemeDto>> CreateAsync([FromBody] CreateThemeDto request)
{
var theme = ThemeMapper.ToEntity(request);
var finalTheme = await ThemeRepository.AddAsync(theme);
await FrontendService.ResetCacheAsync();
return ThemeMapper.ToDto(finalTheme);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = Permissions.Themes.Edit)]
public async Task<ActionResult<ThemeDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateThemeDto request)
{
var theme = await ThemeRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (theme == null)
return Problem("No theme with this id found", statusCode: 404);
ThemeMapper.Merge(theme, request);
await ThemeRepository.UpdateAsync(theme);
await FrontendService.ResetCacheAsync();
return ThemeMapper.ToDto(theme);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = Permissions.Themes.Delete)]
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
var theme = await ThemeRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (theme == null)
return Problem("No theme with this id found", statusCode: 404);
await ThemeRepository.RemoveAsync(theme);
await FrontendService.ResetCacheAsync();
return NoContent();
}
}

View File

@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Services;
using Moonlight.Shared;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/users/{id:int}")]
public class UserActionsController : Controller
{
// Consider building a service for deletion and logout or actions in general
private readonly DatabaseRepository<User> UsersRepository;
private readonly IMemoryCache Cache;
public UserActionsController(DatabaseRepository<User> usersRepository, IMemoryCache cache)
{
UsersRepository = usersRepository;
Cache = cache;
}
[HttpPost("logout")]
[Authorize(Policy = Permissions.Users.Logout)]
public async Task<ActionResult> LogoutAsync([FromRoute] int id)
{
var user = await UsersRepository
.Query()
.FirstOrDefaultAsync(u => u.Id == id);
if(user == null)
return Problem("User not found", statusCode: 404);
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
await UsersRepository.UpdateAsync(user);
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, id));
return NoContent();
}
}

View File

@@ -0,0 +1,135 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Api.Http.Controllers.Admin;
[Authorize]
[ApiController]
[Route("api/admin/users")]
public class UsersController : Controller
{
private readonly DatabaseRepository<User> UserRepository;
public UsersController(DatabaseRepository<User> userRepository)
{
UserRepository = userRepository;
}
[HttpGet]
[Authorize(Policy = Permissions.Users.View)]
public async Task<ActionResult<PagedData<UserDto>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int length,
[FromQuery] FilterOptions? filterOptions
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
// Query building
var query = UserRepository
.Query();
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(Database.Entities.User.Email) =>
query.Where(user => EF.Functions.ILike(user.Email, $"%{filterOption.Value}%")),
nameof(Database.Entities.User.Username) =>
query.Where(user => EF.Functions.ILike(user.Username, $"%{filterOption.Value}%")),
_ => query
};
}
}
// Pagination
var data = await query
.OrderBy(x => x.Id)
.ProjectToDto()
.Skip(startIndex)
.Take(length)
.ToArrayAsync();
var total = await query.CountAsync();
return new PagedData<UserDto>(data, total);
}
[HttpGet("{id:int}")]
[Authorize(Policy = Permissions.Users.View)]
public async Task<ActionResult<UserDto>> GetAsync([FromRoute] int id)
{
var user = await UserRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
return Problem("No user with this id found", statusCode: 404);
return UserMapper.ToDto(user);
}
[HttpPost]
[Authorize(Policy = Permissions.Users.Create)]
public async Task<ActionResult<UserDto>> CreateAsync([FromBody] CreateUserDto request)
{
var user = UserMapper.ToEntity(request);
user.InvalidateTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1);
var finalUser = await UserRepository.AddAsync(user);
return UserMapper.ToDto(finalUser);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = Permissions.Users.Edit)]
public async Task<ActionResult<UserDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateUserDto request)
{
var user = await UserRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
return Problem("No user with this id found", statusCode: 404);
UserMapper.Merge(user, request);
await UserRepository.UpdateAsync(user);
return UserMapper.ToDto(user);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = Permissions.Users.Delete)]
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
var user = await UserRepository
.Query()
.FirstOrDefaultAsync(user => user.Id == id);
if (user == null)
return Problem("No user with this id found", statusCode: 404);
await UserRepository.RemoveAsync(user);
return NoContent();
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/versions")]
[Authorize(Policy = Permissions.System.Versions)]
public class VersionsController : Controller
{
private readonly VersionService VersionService;
public VersionsController(VersionService versionService)
{
VersionService = versionService;
}
[HttpGet]
public async Task<ActionResult<VersionDto[]>> GetAsync()
{
var versions = await VersionService.GetVersionsAsync();
return VersionMapper.ToDtos(versions).ToArray();
}
[HttpGet("instance")]
public async Task<ActionResult<VersionDto>> GetInstanceAsync()
{
var version = await VersionService.GetInstanceVersionAsync();
return VersionMapper.ToDto(version);
}
[HttpGet("latest")]
public async Task<ActionResult<VersionDto>> GetLatestAsync()
{
var version = await VersionService.GetLatestVersionAsync();
if(version == null)
return Problem("Unable to retrieve latest version", statusCode: 404);
return VersionMapper.ToDto(version);
}
}

View File

@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Shared.Http.Responses.Admin.Auth;
namespace Moonlight.Api.Http.Controllers;
[ApiController]
[Route("api/auth")]
public class AuthController : Controller
{
private readonly IAuthenticationSchemeProvider SchemeProvider;
public AuthController(IAuthenticationSchemeProvider schemeProvider)
{
SchemeProvider = schemeProvider;
}
[HttpGet]
public async Task<ActionResult<SchemeDto[]>> GetSchemesAsync()
{
var schemes = await SchemeProvider.GetAllSchemesAsync();
return schemes
.Where(scheme => !string.IsNullOrWhiteSpace(scheme.DisplayName))
.Select(scheme => new SchemeDto(scheme.Name, scheme.DisplayName!))
.ToArray();
}
[HttpGet("{schemeName:alpha}")]
public async Task<ActionResult> StartAsync([FromRoute] string schemeName)
{
var scheme = await SchemeProvider.GetSchemeAsync(schemeName);
if (scheme == null || string.IsNullOrWhiteSpace(scheme.DisplayName))
return Problem("Invalid authentication scheme name", statusCode: 400);
return Challenge(new AuthenticationProperties()
{
RedirectUri = "/"
}, scheme.Name);
}
[Authorize]
[HttpGet("claims")]
public Task<ActionResult<ClaimDto[]>> GetClaimsAsync()
{
var result = User.Claims
.Select(claim => new ClaimDto(claim.Type, claim.Value))
.ToArray();
return Task.FromResult<ActionResult<ClaimDto[]>>(result);
}
[HttpGet("logout")]
public Task<ActionResult> LogoutAsync()
{
return Task.FromResult<ActionResult>(
SignOut(new AuthenticationProperties()
{
RedirectUri = "/"
})
);
}
}

View File

@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared.Http.Responses.Admin.Frontend;
namespace Moonlight.Api.Http.Controllers;
[ApiController]
[Route("api/frontend")]
public class FrontendController : Controller
{
private readonly FrontendService FrontendService;
public FrontendController(FrontendService frontendService)
{
FrontendService = frontendService;
}
[HttpGet("config")]
public async Task<ActionResult<FrontendConfigDto>> GetConfigAsync()
{
var configuration = await FrontendService.GetConfigurationAsync();
return FrontendConfigMapper.ToDto(configuration);
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Moonlight.Api.Http.Controllers;
[ApiController]
[Route("api/ping")]
public class PingController : Controller
{
[HttpGet]
[AllowAnonymous]
public IActionResult Get() => Ok("Pong");
}

View File

@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace Moonlight.Api.Http.Services.ContainerHelper.Events;
public struct RebuildEventDto
{
[JsonPropertyName("type")]
public RebuildEventType Type { get; set; }
[JsonPropertyName("data")]
public string Data { get; set; }
}
public enum RebuildEventType
{
Log = 0,
Failed = 1,
Succeeded = 2,
Step = 3
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.Api.Http.Services.ContainerHelper;
public class ProblemDetails
{
public string Type { get; set; }
public string Title { get; set; }
public int Status { get; set; }
public string? Detail { get; set; }
public Dictionary<string, string[]>? Errors { get; set; }
}

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
public record RequestRebuildDto(bool NoBuildCache);

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
public record SetVersionDto(string Version);

View File

@@ -0,0 +1,29 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Moonlight.Api.Http.Services.ContainerHelper.Events;
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
namespace Moonlight.Api.Http.Services.ContainerHelper;
[JsonSerializable(typeof(SetVersionDto))]
[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(RebuildEventDto))]
[JsonSerializable(typeof(RequestRebuildDto))]
public partial class SerializationContext : JsonSerializerContext
{
private static JsonSerializerOptions? InternalTunedOptions;
public static JsonSerializerOptions TunedOptions
{
get
{
if (InternalTunedOptions != null)
return InternalTunedOptions;
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
return InternalTunedOptions;
}
}
}

View File

@@ -0,0 +1,71 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared;
namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
{
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
private readonly IMemoryCache MemoryCache;
public ApiKeySchemeHandler(
IOptionsMonitor<ApiKeySchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
DatabaseRepository<ApiKey> apiKeyRepository,
IMemoryCache memoryCache
) : base(options, logger, encoder)
{
ApiKeyRepository = apiKeyRepository;
MemoryCache = memoryCache;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authHeaderValue = Request.Headers.Authorization.FirstOrDefault() ?? null;
if (string.IsNullOrWhiteSpace(authHeaderValue))
return AuthenticateResult.NoResult();
if (authHeaderValue.Length > 32)
return AuthenticateResult.Fail("Invalid api key specified");
if (!MemoryCache.TryGetValue<ApiKeySession>(authHeaderValue, out var apiKey))
{
apiKey = await ApiKeyRepository
.Query()
.Where(x => x.Key == authHeaderValue)
.Select(x => new ApiKeySession(x.Permissions))
.FirstOrDefaultAsync();
if (apiKey == null)
return AuthenticateResult.Fail("Invalid api key specified");
MemoryCache.Set(authHeaderValue, apiKey, Options.LookupCacheTime);
}
else
{
if (apiKey == null)
return AuthenticateResult.Fail("Invalid api key specified");
}
return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(
new ClaimsIdentity(
apiKey.Permissions.Select(x => new Claim(Permissions.ClaimType, x)).ToArray()
)
),
Scheme.Name
));
}
private record ApiKeySession(string[] Permissions);
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authentication;
namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
{
public TimeSpan LookupCacheTime { get; set; }
}

View File

@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Authorization;
using Moonlight.Shared;
namespace Moonlight.Api.Implementations;
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
var permissionClaim = context.User.FindFirst(x =>
x.Type.Equals(Permissions.ClaimType, StringComparison.OrdinalIgnoreCase) &&
x.Value.Equals(requirement.Identifier, StringComparison.OrdinalIgnoreCase)
);
if (permissionClaim == null)
{
context.Fail(new AuthorizationFailureReason(
this,
$"User does not have the requested permission '{requirement.Identifier}'"
));
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Moonlight.Shared;
namespace Moonlight.Api.Implementations;
public class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
private readonly DefaultAuthorizationPolicyProvider FallbackProvider;
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options)
{
FallbackProvider = new DefaultAuthorizationPolicyProvider(options);
}
public async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (!policyName.StartsWith(Permissions.Prefix, StringComparison.OrdinalIgnoreCase))
return await FallbackProvider.GetPolicyAsync(policyName);
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionRequirement(policyName));
return policy.Build();
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
=> FallbackProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
=> FallbackProvider.GetFallbackPolicyAsync();
}
public class PermissionRequirement : IAuthorizationRequirement
{
public string Identifier { get; }
public PermissionRequirement(string identifier)
{
Identifier = identifier;
}
}

View File

@@ -0,0 +1,33 @@
using Moonlight.Api.Interfaces;
using Moonlight.Api.Models;
using Moonlight.Api.Services;
namespace Moonlight.Api.Implementations;
public sealed class UpdateDiagnoseProvider : IDiagnoseProvider
{
private readonly ApplicationService ApplicationService;
public UpdateDiagnoseProvider(ApplicationService applicationService)
{
ApplicationService = applicationService;
}
public Task<DiagnoseResult[]> DiagnoseAsync()
{
if (ApplicationService.IsUpToDate)
return Task.FromResult<DiagnoseResult[]>([]);
return Task.FromResult<DiagnoseResult[]>([
new DiagnoseResult(
DiagnoseLevel.Warning,
"Instance is not up-to-date",
["Moonlight", "Update Check"],
"Update your moonlight instance to receive bug fixes, new features and security patches. Update button can be found in the overview",
null,
"/admin",
null
)
]);
}
}

View File

@@ -0,0 +1,8 @@
using Moonlight.Api.Models;
namespace Moonlight.Api.Interfaces;
public interface IDiagnoseProvider
{
public Task<DiagnoseResult[]> DiagnoseAsync();
}

View File

@@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class ApiKeyMapper
{
public static partial IQueryable<ApiKeyDto> ProjectToDto(this IQueryable<ApiKey> apiKeys);
public static partial ApiKeyDto ToDto(ApiKey apiKey);
public static partial void Merge([MappingTarget] ApiKey apiKey, UpdateApiKeyDto request);
public static partial ApiKey ToEntity(CreateApiKeyDto request);
}

View File

@@ -0,0 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Events;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class ContainerHelperMapper
{
public static partial RebuildEventDto ToDto(Http.Services.ContainerHelper.Events.RebuildEventDto rebuildEventDto);
}

View File

@@ -0,0 +1,14 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class DiagnoseResultMapper
{
public static partial IEnumerable<DiagnoseResultDto> ToDto(this IEnumerable<DiagnoseResult> results);
}

View File

@@ -0,0 +1,14 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Admin.Frontend;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class FrontendConfigMapper
{
public static partial FrontendConfigDto ToDto(FrontendConfiguration configuration);
}

View File

@@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
[SuppressMessage("Mapper", "RMG012:Source member was not found for target member")]
public static partial class RoleMapper
{
[MapProperty([nameof(Role.Members), nameof(Role.Members.Count)], nameof(RoleDto.MemberCount))]
public static partial RoleDto ToDto(Role role);
public static partial Role ToEntity(CreateRoleDto request);
public static partial void Merge([MappingTarget] Role role, UpdateRoleDto request);
public static partial IQueryable<RoleDto> ProjectToDto(this IQueryable<Role> roles);
}

View File

@@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Responses.Admin.Themes;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class ThemeMapper
{
public static partial IQueryable<ThemeDto> ProjectToDto(this IQueryable<Theme> themes);
public static partial ThemeDto ToDto(Theme theme);
public static partial void Merge([MappingTarget] Theme theme, UpdateThemeDto request);
public static partial Theme ToEntity(CreateThemeDto request);
}

View File

@@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class UserMapper
{
public static partial IQueryable<UserDto> ProjectToDto(this IQueryable<User> users);
public static partial UserDto ToDto(User user);
public static partial void Merge([MappingTarget] User user, UpdateUserDto request);
public static partial User ToEntity(CreateUserDto request);
}

View File

@@ -0,0 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
[SuppressMessage("Mapper", "RMG012:Source member was not found for target member")]
public static partial class VersionMapper
{
public static partial IEnumerable<VersionDto> ToDtos(IEnumerable<MoonlightVersion> versions);
public static partial VersionDto ToDto(MoonlightVersion version);
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.Api.Models;
public record DiagnoseResult(DiagnoseLevel Level, string Title, string[] Tags, string? Message, string? StackStrace, string? SolutionUrl, string? ReportUrl);
public enum DiagnoseLevel
{
Error = 0,
Warning = 1,
Healthy = 2
}

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Api.Models;
public record FrontendConfiguration(string Name, string? ThemeCss);

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