JJ Colocate

This commit is contained in:
2025-06-30 14:23:20 -04:00
parent 360fa53439
commit 24abf5607f
77 changed files with 5700 additions and 5700 deletions

View File

@@ -1,30 +1,30 @@
**/.classpath **/.classpath
**/.dockerignore **/.dockerignore
**/.env **/.env
**/.git **/.git
**/.gitignore **/.gitignore
**/.project **/.project
**/.settings **/.settings
**/.toolstarget **/.toolstarget
**/.vs **/.vs
**/.vscode **/.vscode
**/*.*proj.user **/*.*proj.user
**/*.dbmdl **/*.dbmdl
**/*.jfm **/*.jfm
**/azds.yaml **/azds.yaml
**/bin **/bin
**/charts **/charts
**/docker-compose* **/docker-compose*
**/Dockerfile* **/Dockerfile*
**/node_modules **/node_modules
**/npm-debug.log **/npm-debug.log
**/obj **/obj
**/secrets.dev.yaml **/secrets.dev.yaml
**/values.dev.yaml **/values.dev.yaml
LICENSE LICENSE
README.md README.md
!**/.gitignore !**/.gitignore
!.git/HEAD !.git/HEAD
!.git/config !.git/config
!.git/packed-refs !.git/packed-refs
!.git/refs/heads/** !.git/refs/heads/**

126
.gitattributes vendored
View File

@@ -1,63 +1,63 @@
############################################################################### ###############################################################################
# Set default behavior to automatically normalize line endings. # Set default behavior to automatically normalize line endings.
############################################################################### ###############################################################################
* text=auto * text=auto
############################################################################### ###############################################################################
# Set default behavior for command prompt diff. # Set default behavior for command prompt diff.
# #
# This is need for earlier builds of msysgit that does not have it on by # This is need for earlier builds of msysgit that does not have it on by
# default for csharp files. # default for csharp files.
# Note: This is only used by command line # Note: This is only used by command line
############################################################################### ###############################################################################
#*.cs diff=csharp #*.cs diff=csharp
############################################################################### ###############################################################################
# Set the merge driver for project and solution files # Set the merge driver for project and solution files
# #
# Merging from the command prompt will add diff markers to the files if there # Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS # are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following # the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat # file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user # these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below # intervention with every merge. To do so, just uncomment the entries below
############################################################################### ###############################################################################
#*.sln merge=binary #*.sln merge=binary
#*.csproj merge=binary #*.csproj merge=binary
#*.vbproj merge=binary #*.vbproj merge=binary
#*.vcxproj merge=binary #*.vcxproj merge=binary
#*.vcproj merge=binary #*.vcproj merge=binary
#*.dbproj merge=binary #*.dbproj merge=binary
#*.fsproj merge=binary #*.fsproj merge=binary
#*.lsproj merge=binary #*.lsproj merge=binary
#*.wixproj merge=binary #*.wixproj merge=binary
#*.modelproj merge=binary #*.modelproj merge=binary
#*.sqlproj merge=binary #*.sqlproj merge=binary
#*.wwaproj merge=binary #*.wwaproj merge=binary
############################################################################### ###############################################################################
# behavior for image files # behavior for image files
# #
# image files are treated as binary by default. # image files are treated as binary by default.
############################################################################### ###############################################################################
#*.jpg binary #*.jpg binary
#*.png binary #*.png binary
#*.gif binary #*.gif binary
############################################################################### ###############################################################################
# diff behavior for common document formats # diff behavior for common document formats
# #
# Convert binary document formats to text before diffing them. This feature # Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the # is only available from the command line. Turn it on by uncommenting the
# entries below. # entries below.
############################################################################### ###############################################################################
#*.doc diff=astextplain #*.doc diff=astextplain
#*.DOC diff=astextplain #*.DOC diff=astextplain
#*.docx diff=astextplain #*.docx diff=astextplain
#*.DOCX diff=astextplain #*.DOCX diff=astextplain
#*.dot diff=astextplain #*.dot diff=astextplain
#*.DOT diff=astextplain #*.DOT diff=astextplain
#*.pdf diff=astextplain #*.pdf diff=astextplain
#*.PDF diff=astextplain #*.PDF diff=astextplain
#*.rtf diff=astextplain #*.rtf diff=astextplain
#*.RTF diff=astextplain #*.RTF diff=astextplain

724
.gitignore vendored
View File

@@ -1,363 +1,363 @@
## 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/master/VisualStudio.gitignore ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files # User-specific files
*.rsuser *.rsuser
*.suo *.suo
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs
# Mono auto generated files # Mono auto generated files
mono_crash.* mono_crash.*
# Build results # Build results
[Dd]ebug/ [Dd]ebug/
[Dd]ebugPublic/ [Dd]ebugPublic/
[Rr]elease/ [Rr]elease/
[Rr]eleases/ [Rr]eleases/
x64/ x64/
x86/ x86/
[Ww][Ii][Nn]32/ [Ww][Ii][Nn]32/
[Aa][Rr][Mm]/ [Aa][Rr][Mm]/
[Aa][Rr][Mm]64/ [Aa][Rr][Mm]64/
bld/ bld/
[Bb]in/ [Bb]in/
[Oo]bj/ [Oo]bj/
[Oo]ut/ [Oo]ut/
[Ll]og/ [Ll]og/
[Ll]ogs/ [Ll]ogs/
# Visual Studio 2015/2017 cache/options directory # Visual Studio 2015/2017 cache/options directory
.vs/ .vs/
# Uncomment if you have tasks that create the project's static files in wwwroot # Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/ #wwwroot/
# Visual Studio 2017 auto generated files # Visual Studio 2017 auto generated files
Generated\ Files/ Generated\ Files/
# MSTest test Results # MSTest test Results
[Tt]est[Rr]esult*/ [Tt]est[Rr]esult*/
[Bb]uild[Ll]og.* [Bb]uild[Ll]og.*
# NUnit # NUnit
*.VisualState.xml *.VisualState.xml
TestResult.xml TestResult.xml
nunit-*.xml nunit-*.xml
# Build Results of an ATL Project # Build Results of an ATL Project
[Dd]ebugPS/ [Dd]ebugPS/
[Rr]eleasePS/ [Rr]eleasePS/
dlldata.c dlldata.c
# Benchmark Results # Benchmark Results
BenchmarkDotNet.Artifacts/ BenchmarkDotNet.Artifacts/
# .NET Core # .NET Core
project.lock.json project.lock.json
project.fragment.lock.json project.fragment.lock.json
artifacts/ artifacts/
# ASP.NET Scaffolding # ASP.NET Scaffolding
ScaffoldingReadMe.txt ScaffoldingReadMe.txt
# StyleCop # StyleCop
StyleCopReport.xml StyleCopReport.xml
# Files built by Visual Studio # Files built by Visual Studio
*_i.c *_i.c
*_p.c *_p.c
*_h.h *_h.h
*.ilk *.ilk
*.meta *.meta
*.obj *.obj
*.iobj *.iobj
*.pch *.pch
*.pdb *.pdb
*.ipdb *.ipdb
*.pgc *.pgc
*.pgd *.pgd
*.rsp *.rsp
*.sbr *.sbr
*.tlb *.tlb
*.tli *.tli
*.tlh *.tlh
*.tmp *.tmp
*.tmp_proj *.tmp_proj
*_wpftmp.csproj *_wpftmp.csproj
*.log *.log
*.vspscc *.vspscc
*.vssscc *.vssscc
.builds .builds
*.pidb *.pidb
*.svclog *.svclog
*.scc *.scc
# Chutzpah Test files # Chutzpah Test files
_Chutzpah* _Chutzpah*
# Visual C++ cache files # Visual C++ cache files
ipch/ ipch/
*.aps *.aps
*.ncb *.ncb
*.opendb *.opendb
*.opensdf *.opensdf
*.sdf *.sdf
*.cachefile *.cachefile
*.VC.db *.VC.db
*.VC.VC.opendb *.VC.VC.opendb
# Visual Studio profiler # Visual Studio profiler
*.psess *.psess
*.vsp *.vsp
*.vspx *.vspx
*.sap *.sap
# Visual Studio Trace Files # Visual Studio Trace Files
*.e2e *.e2e
# TFS 2012 Local Workspace # TFS 2012 Local Workspace
$tf/ $tf/
# Guidance Automation Toolkit # Guidance Automation Toolkit
*.gpState *.gpState
# ReSharper is a .NET coding add-in # ReSharper is a .NET coding add-in
_ReSharper*/ _ReSharper*/
*.[Rr]e[Ss]harper *.[Rr]e[Ss]harper
*.DotSettings.user *.DotSettings.user
# TeamCity is a build add-in # TeamCity is a build add-in
_TeamCity* _TeamCity*
# DotCover is a Code Coverage Tool # DotCover is a Code Coverage Tool
*.dotCover *.dotCover
# AxoCover is a Code Coverage Tool # AxoCover is a Code Coverage Tool
.axoCover/* .axoCover/*
!.axoCover/settings.json !.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool # Coverlet is a free, cross platform Code Coverage Tool
coverage*.json coverage*.json
coverage*.xml coverage*.xml
coverage*.info coverage*.info
# Visual Studio code coverage results # Visual Studio code coverage results
*.coverage *.coverage
*.coveragexml *.coveragexml
# NCrunch # NCrunch
_NCrunch_* _NCrunch_*
.*crunch*.local.xml .*crunch*.local.xml
nCrunchTemp_* nCrunchTemp_*
# MightyMoose # MightyMoose
*.mm.* *.mm.*
AutoTest.Net/ AutoTest.Net/
# Web workbench (sass) # Web workbench (sass)
.sass-cache/ .sass-cache/
# Installshield output folder # Installshield output folder
[Ee]xpress/ [Ee]xpress/
# DocProject is a documentation generator add-in # DocProject is a documentation generator add-in
DocProject/buildhelp/ DocProject/buildhelp/
DocProject/Help/*.HxT DocProject/Help/*.HxT
DocProject/Help/*.HxC DocProject/Help/*.HxC
DocProject/Help/*.hhc DocProject/Help/*.hhc
DocProject/Help/*.hhk DocProject/Help/*.hhk
DocProject/Help/*.hhp DocProject/Help/*.hhp
DocProject/Help/Html2 DocProject/Help/Html2
DocProject/Help/html DocProject/Help/html
# Click-Once directory # Click-Once directory
publish/ publish/
# Publish Web Output # Publish Web Output
*.[Pp]ublish.xml *.[Pp]ublish.xml
*.azurePubxml *.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings, # Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted # but database connection strings (with potential passwords) will be unencrypted
*.pubxml *.pubxml
*.publishproj *.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to # Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained # checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted # in these scripts will be unencrypted
PublishScripts/ PublishScripts/
# NuGet Packages # NuGet Packages
*.nupkg *.nupkg
# NuGet Symbol Packages # NuGet Symbol Packages
*.snupkg *.snupkg
# The packages folder can be ignored because of Package Restore # The packages folder can be ignored because of Package Restore
**/[Pp]ackages/* **/[Pp]ackages/*
# except build/, which is used as an MSBuild target. # except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/ !**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed # Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config #!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files # NuGet v3's project.json files produces more ignorable files
*.nuget.props *.nuget.props
*.nuget.targets *.nuget.targets
# Microsoft Azure Build Output # Microsoft Azure Build Output
csx/ csx/
*.build.csdef *.build.csdef
# Microsoft Azure Emulator # Microsoft Azure Emulator
ecf/ ecf/
rcf/ rcf/
# Windows Store app package directories and files # Windows Store app package directories and files
AppPackages/ AppPackages/
BundleArtifacts/ BundleArtifacts/
Package.StoreAssociation.xml Package.StoreAssociation.xml
_pkginfo.txt _pkginfo.txt
*.appx *.appx
*.appxbundle *.appxbundle
*.appxupload *.appxupload
# Visual Studio cache files # Visual Studio cache files
# files ending in .cache can be ignored # files ending in .cache can be ignored
*.[Cc]ache *.[Cc]ache
# but keep track of directories ending in .cache # but keep track of directories ending in .cache
!?*.[Cc]ache/ !?*.[Cc]ache/
# Others # Others
ClientBin/ ClientBin/
~$* ~$*
*~ *~
*.dbmdl *.dbmdl
*.dbproj.schemaview *.dbproj.schemaview
*.jfm *.jfm
*.pfx *.pfx
*.publishsettings *.publishsettings
orleans.codegen.cs orleans.codegen.cs
# Including strong name files can present a security risk # Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424) # (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk #*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components # Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/ #bower_components/
# RIA/Silverlight projects # RIA/Silverlight projects
Generated_Code/ Generated_Code/
# Backup & report files from converting an old project file # Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed, # to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-) # because we have git ;-)
_UpgradeReport_Files/ _UpgradeReport_Files/
Backup*/ Backup*/
UpgradeLog*.XML UpgradeLog*.XML
UpgradeLog*.htm UpgradeLog*.htm
ServiceFabricBackup/ ServiceFabricBackup/
*.rptproj.bak *.rptproj.bak
# SQL Server files # SQL Server files
*.mdf *.mdf
*.ldf *.ldf
*.ndf *.ndf
# Business Intelligence projects # Business Intelligence projects
*.rdl.data *.rdl.data
*.bim.layout *.bim.layout
*.bim_*.settings *.bim_*.settings
*.rptproj.rsuser *.rptproj.rsuser
*- [Bb]ackup.rdl *- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes # Microsoft Fakes
FakesAssemblies/ FakesAssemblies/
# GhostDoc plugin setting file # GhostDoc plugin setting file
*.GhostDoc.xml *.GhostDoc.xml
# Node.js Tools for Visual Studio # Node.js Tools for Visual Studio
.ntvs_analysis.dat .ntvs_analysis.dat
node_modules/ node_modules/
# Visual Studio 6 build log # Visual Studio 6 build log
*.plg *.plg
# Visual Studio 6 workspace options file # Visual Studio 6 workspace options file
*.opt *.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw *.vbw
# Visual Studio LightSwitch build output # Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts **/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml **/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts **/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml **/*.Server/ModelManifest.xml
_Pvt_Extensions _Pvt_Extensions
# Paket dependency manager # Paket dependency manager
.paket/paket.exe .paket/paket.exe
paket-files/ paket-files/
# FAKE - F# Make # FAKE - F# Make
.fake/ .fake/
# CodeRush personal settings # CodeRush personal settings
.cr/personal .cr/personal
# Python Tools for Visual Studio (PTVS) # Python Tools for Visual Studio (PTVS)
__pycache__/ __pycache__/
*.pyc *.pyc
# Cake - Uncomment if you are using it # Cake - Uncomment if you are using it
# tools/** # tools/**
# !tools/packages.config # !tools/packages.config
# Tabs Studio # Tabs Studio
*.tss *.tss
# Telerik's JustMock configuration file # Telerik's JustMock configuration file
*.jmconfig *.jmconfig
# BizTalk build output # BizTalk build output
*.btp.cs *.btp.cs
*.btm.cs *.btm.cs
*.odx.cs *.odx.cs
*.xsd.cs *.xsd.cs
# OpenCover UI analysis results # OpenCover UI analysis results
OpenCover/ OpenCover/
# Azure Stream Analytics local run output # Azure Stream Analytics local run output
ASALocalRun/ ASALocalRun/
# MSBuild Binary and Structured Log # MSBuild Binary and Structured Log
*.binlog *.binlog
# NVidia Nsight GPU debugger configuration file # NVidia Nsight GPU debugger configuration file
*.nvuser *.nvuser
# MFractors (Xamarin productivity tool) working folder # MFractors (Xamarin productivity tool) working folder
.mfractor/ .mfractor/
# Local History for Visual Studio # Local History for Visual Studio
.localhistory/ .localhistory/
# BeatPulse healthcheck temp database # BeatPulse healthcheck temp database
healthchecksdb healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017 # Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder # Ionide (cross platform F# VS Code tools) working folder
.ionide/ .ionide/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd

View File

@@ -1,2 +1,2 @@
[build] [build]
target = "wasm32-unknown-unknown" target = "wasm32-unknown-unknown"

View File

@@ -1,6 +1,6 @@
**/target **/target
**/dist **/dist
LICENSES LICENSES
LICENSE LICENSE
temp temp
README.md README.md

14
AobaClient/.gitignore vendored
View File

@@ -1,7 +1,7 @@
# Generated by Cargo # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables
/target /target
.DS_Store .DS_Store
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk

4916
AobaClient/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,37 @@
[package] [package]
name = "aoba-client" name = "aoba-client"
version = "0.1.0" version = "0.1.0"
authors = ["Amatsugu <khamraj@kaisei.app>"] authors = ["Amatsugu <khamraj@kaisei.app>"]
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
dioxus = { version = "0.6.0", features = ["router"] } dioxus = { version = "0.6.0", features = ["router"] }
serde = "1.0.219" serde = "1.0.219"
serde_repr = "0.1.20" serde_repr = "0.1.20"
tonic = { version = "*", default-features = false, features = [ tonic = { version = "*", default-features = false, features = [
"codegen", "codegen",
"prost", "prost",
] } ] }
prost = "0.13" prost = "0.13"
tonic-web-wasm-client = "0.7" tonic-web-wasm-client = "0.7"
web-sys = { version = "0.3.77", features = ["Storage", "Window"] } web-sys = { version = "0.3.77", features = ["Storage", "Window"] }
[build-dependencies] [build-dependencies]
tonic-build = { version = "*", default-features = false, features = ["prost"] } tonic-build = { version = "*", default-features = false, features = ["prost"] }
[features] [features]
default = ["web"] default = ["web"]
web = ["dioxus/web"] web = ["dioxus/web"]
[profile] [profile]
[profile.wasm-dev] [profile.wasm-dev]
inherits = "dev" inherits = "dev"
opt-level = 1 opt-level = 1
[profile.server-dev] [profile.server-dev]
inherits = "dev" inherits = "dev"
[profile.android-dev] [profile.android-dev]
inherits = "dev" inherits = "dev"

View File

@@ -1,21 +1,21 @@
[application] [application]
[web.app] [web.app]
# HTML title tag content # HTML title tag content
title = "aoba-client" title = "aoba-client"
# include `assets` in web platform # include `assets` in web platform
[web.resource] [web.resource]
# Additional CSS style files # Additional CSS style files
style = [] style = []
# Additional JavaScript files # Additional JavaScript files
script = [] script = []
[web.resource.dev] [web.resource.dev]
# Javascript code file # Javascript code file
# serve: [dev-server] only # serve: [dev-server] only
script = [] script = []

View File

@@ -1,25 +1,25 @@
# Development # Development
Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets. Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets.
``` ```
project/ project/
├─ assets/ # Any assets that are used by the app should be placed here ├─ assets/ # Any assets that are used by the app should be placed here
├─ src/ ├─ src/
│ ├─ main.rs # main.rs is the entry point to your application and currently contains all components for the app │ ├─ main.rs # main.rs is the entry point to your application and currently contains all components for the app
├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project ├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project
``` ```
### Serving Your App ### Serving Your App
Run the following command in the root of your project to start developing with the default platform: Run the following command in the root of your project to start developing with the default platform:
```bash ```bash
dx serve dx serve
``` ```
To run for a different platform, use the `--platform platform` flag. E.g. To run for a different platform, use the `--platform platform` flag. E.g.
```bash ```bash
dx serve --platform desktop dx serve --platform desktop
``` ```

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,8 +1,8 @@
$mainBGColor: #584577; $mainBGColor: #584577;
$featureColor: #ce2d4f; $featureColor: #ce2d4f;
$accentColor: #f0eaf8; $accentColor: #f0eaf8;
$mainTextColor: #eee; $mainTextColor: #eee;
$brightTextColor: #fff; $brightTextColor: #fff;
$invertTextColor: #222; $invertTextColor: #222;
$invertBrightTextColor: #000; $invertBrightTextColor: #000;

View File

@@ -1,28 +1,28 @@
label { label {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
textarea, textarea,
input[type="url"], input[type="url"],
input[type="email"], input[type="email"],
input[type="number"], input[type="number"],
input[type="tel"], input[type="tel"],
input[type="text"] { input[type="text"] {
} }
.searchBar { .searchBar {
display: grid; display: grid;
width: 100%; width: 100%;
input { input {
padding: 10px; padding: 10px;
font-size: 1.5rem; font-size: 1.5rem;
border-radius: 20px; border-radius: 20px;
} }
} }
textarea { textarea {
min-height: 200px; min-height: 200px;
min-width: 500px; min-width: 500px;
} }

View File

@@ -1,151 +1,151 @@
@import "mixins"; @import "mixins";
@import "colors"; @import "colors";
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
:root { :root {
background-color: $mainBGColor; background-color: $mainBGColor;
color: $mainTextColor; color: $mainTextColor;
box-sizing: border-box; box-sizing: border-box;
font-family: "Noto Sans", sans-serif; font-family: "Noto Sans", sans-serif;
font-optical-sizing: auto; font-optical-sizing: auto;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
font-variation-settings: "wdth" 100; font-variation-settings: "wdth" 100;
} }
.stickyTop { .stickyTop {
top: 0; top: 0;
position: sticky; position: sticky;
} }
body { body {
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
#main:has(#content) { #main:has(#content) {
display: grid; display: grid;
grid-template-columns: $navBarSize 1fr; grid-template-columns: $navBarSize 1fr;
grid-template-areas: "Nav Content"; grid-template-areas: "Nav Content";
} }
#content { #content {
grid-area: Content; grid-area: Content;
overflow-x: hidden; overflow-x: hidden;
padding: 10px; padding: 10px;
/* margin-left: $navBarSize; */ /* margin-left: $navBarSize; */
} }
$mediaItemSize: 300px; $mediaItemSize: 300px;
.mediaGrid { .mediaGrid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 10px; gap: 10px;
margin: 10px 0; margin: 10px 0;
.mediaItem { .mediaItem {
width: $mediaItemSize; width: $mediaItemSize;
height: $mediaItemSize; height: $mediaItemSize;
overflow: hidden; overflow: hidden;
display: grid; display: grid;
grid-template-columns: $mediaItemSize; grid-template-columns: $mediaItemSize;
grid-template-areas: "A"; grid-template-areas: "A";
box-shadow: 0 0 2px #000; box-shadow: 0 0 2px #000;
color: $mainTextColor; color: $mainTextColor;
text-decoration: none; text-decoration: none;
transition: transition:
transform 0.25s ease-out, transform 0.25s ease-out,
box-shadow 0.25s ease-out; box-shadow 0.25s ease-out;
> * { > * {
grid-area: A; grid-area: A;
} }
img { img {
aspect-ratio: 1; aspect-ratio: 1;
object-fit: cover; object-fit: cover;
width: 100%; width: 100%;
object-position: center; object-position: center;
background-color: $invertTextColor; background-color: $invertTextColor;
border: 0; border: 0;
outline: none; outline: none;
} }
.info { .info {
align-self: end; align-self: end;
backdrop-filter: blur(20px) brightness(0.5); backdrop-filter: blur(20px) brightness(0.5);
transition: transform 0.25s ease-out; transition: transform 0.25s ease-out;
transform: translateY(100%); transform: translateY(100%);
padding: 2px; padding: 2px;
.name { .name {
text-align: center; text-align: center;
width: 100%; width: 100%;
display: block; display: block;
overflow: hidden; overflow: hidden;
} }
.details { .details {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
} }
&:hover { &:hover {
transform: scale(110%) translateZ(2px); transform: scale(110%) translateZ(2px);
box-shadow: 0 0 8px #000; box-shadow: 0 0 8px #000;
.info { .info {
transform: translateY(0%); transform: translateY(0%);
} }
} }
} }
} }
#main:has(#centralModal) { #main:has(#centralModal) {
display: grid; display: grid;
place-items: center; place-items: center;
height: 100dvh; height: 100dvh;
width: 100dvw; width: 100dvw;
} }
#centralModal { #centralModal {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
form { form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
} }
.notif { .notif {
background-color: red; background-color: red;
display: grid; display: grid;
grid-template-columns: 50px 1fr; grid-template-columns: 50px 1fr;
height: 50px; height: 50px;
border-radius: 20px; border-radius: 20px;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
.icon { .icon {
padding: 10px; padding: 10px;
} }
.message { .message {
padding: 10px; padding: 10px;
align-self: center; align-self: center;
} }
} }
.codeSelect { .codeSelect {
line-break: anywhere; line-break: anywhere;
white-space: pre-wrap; white-space: pre-wrap;
background-color: $featureColor; background-color: $featureColor;
padding: 5px; padding: 5px;
user-select: all; user-select: all;
} }

View File

@@ -1,43 +1,43 @@
$navBarSize: 64px; $navBarSize: 64px;
@mixin mobile { @mixin mobile {
@media (max-width: 700px) { @media (max-width: 700px) {
@content; @content;
} }
} }
@mixin max-screen($size) { @mixin max-screen($size) {
@media (max-width: #{$size}) { @media (max-width: #{$size}) {
@content; @content;
} }
} }
@mixin max-container($size) { @mixin max-container($size) {
@container (max-width: #{$size}) { @container (max-width: #{$size}) {
@content; @content;
} }
} }
@mixin small-container { @mixin small-container {
@container (max-width: 500px) { @container (max-width: 500px) {
@content; @content;
} }
} }
@mixin medium-container { @mixin medium-container {
@container (max-width: 800px) { @container (max-width: 800px) {
@content; @content;
} }
} }
@mixin large-container { @mixin large-container {
@container (max-width: 1000px) { @container (max-width: 1000px) {
@content; @content;
} }
} }
@mixin xlarge-container { @mixin xlarge-container {
@container (max-width: 1200px) { @container (max-width: 1200px) {
@content; @content;
} }
} }

View File

@@ -1,43 +1,43 @@
@import "mixins"; @import "mixins";
@import "colors"; @import "colors";
nav { nav {
display: grid; display: grid;
grid-template-areas: "Branding" "Nav" "Widgets" "Utils"; grid-template-areas: "Branding" "Nav" "Widgets" "Utils";
grid-template-rows: auto 1fr auto auto; grid-template-rows: auto 1fr auto auto;
background-color: $featureColor; background-color: $featureColor;
height: 100dvh; height: 100dvh;
position: fixed; position: fixed;
width: $navBarSize; width: $navBarSize;
box-shadow: 0 0 3px #000; box-shadow: 0 0 3px #000;
gap: 20px; gap: 20px;
padding: 20px 0; padding: 20px 0;
> * { > * {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
.branding { .branding {
grid-area: Branding; grid-area: Branding;
img { img {
width: $navBarSize; width: $navBarSize;
object-fit: contain; object-fit: contain;
} }
} }
.mainNav { .mainNav {
grid-area: Nav; grid-area: Nav;
} }
.widgets { .widgets {
grid-area: Widgets; grid-area: Widgets;
} }
.utils { .utils {
grid-area: Utils; grid-area: Utils;
} }
} }

View File

@@ -1,11 +1,11 @@
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure() tonic_build::configure()
.build_server(false) .build_server(false)
.build_client(true) .build_client(true)
.compile_protos( .compile_protos(
&["../AobaServer/Proto/Aoba.proto", "../AobaServer/Proto/Auth.proto"], &["../AobaServer/Proto/Aoba.proto", "../AobaServer/Proto/Auth.proto"],
&["../AobaServer/Proto/"], &["../AobaServer/Proto/"],
)?; )?;
Ok(()) Ok(())
} }

View File

@@ -1,30 +1,30 @@
use dioxus::prelude::*; use dioxus::prelude::*;
#[derive(PartialEq, Clone, Props)] #[derive(PartialEq, Clone, Props)]
pub struct ButtonProps { pub struct ButtonProps {
pub variant: Option<ButtonVariant>, pub variant: Option<ButtonVariant>,
pub text: String, pub text: String,
pub onclick: Option<EventHandler<Event<MouseData>>>, pub onclick: Option<EventHandler<Event<MouseData>>>,
} }
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
pub enum ButtonVariant { pub enum ButtonVariant {
Base, Base,
Muted, Muted,
Accented, Accented,
} }
#[component] #[component]
pub fn Button(props: ButtonProps) -> Element { pub fn Button(props: ButtonProps) -> Element {
rsx! { rsx! {
button { button {
onclick: move |event| { onclick: move |event| {
event.prevent_default(); event.prevent_default();
if let Some(h) = props.onclick { if let Some(h) = props.onclick {
h.call(event); h.call(event);
} }
}, },
"{props.text}" "{props.text}"
} }
} }
} }

View File

@@ -1,35 +1,35 @@
use dioxus::prelude::*; use dioxus::prelude::*;
#[derive(PartialEq, Clone, Props)] #[derive(PartialEq, Clone, Props)]
pub struct InputProps { pub struct InputProps {
pub r#type: Option<String>, pub r#type: Option<String>,
pub value: Option<Signal<String>>, pub value: Option<Signal<String>>,
pub label: Option<String>, pub label: Option<String>,
pub placeholder: Option<String>, pub placeholder: Option<String>,
pub name: String, pub name: String,
pub oninput: Option<EventHandler<FormEvent>>, pub oninput: Option<EventHandler<FormEvent>>,
pub required: Option<bool>, pub required: Option<bool>,
} }
#[component] #[component]
pub fn Input(props: InputProps) -> Element { pub fn Input(props: InputProps) -> Element {
let label = props.label.unwrap_or("".into()); let label = props.label.unwrap_or("".into());
let ph = props.placeholder.unwrap_or(label.clone()); let ph = props.placeholder.unwrap_or(label.clone());
rsx! { rsx! {
label { label {
"{label}" "{label}"
input { input {
r#type: props.r#type.unwrap_or("text".into()), r#type: props.r#type.unwrap_or("text".into()),
value: props.value, value: props.value,
oninput: move |e| { oninput: move |e| {
if let Some(mut s) = props.value { if let Some(mut s) = props.value {
s.set(e.value()); s.set(e.value());
} }
}, },
name: props.name, name: props.name,
placeholder: ph, placeholder: ph,
required: props.required, required: props.required,
} }
} }
} }
} }

View File

@@ -1,4 +1,4 @@
mod button; mod button;
mod input; mod input;
pub use button::*; pub use button::*;
pub use input::*; pub use input::*;

View File

@@ -1,52 +1,52 @@
use dioxus::prelude::*; use dioxus::prelude::*;
#[component] #[component]
pub fn Info() -> Element { pub fn Info() -> Element {
rsx! { rsx! {
svg { svg {
class: "size-6", class: "size-6",
fill: "currentColor", fill: "currentColor",
view_box: "0 0 24 24", view_box: "0 0 24 24",
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",
path { path {
clip_rule: "evenodd", clip_rule: "evenodd",
d: "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z", d: "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z",
fill_rule: "evenodd", fill_rule: "evenodd",
} }
} }
} }
} }
#[component] #[component]
pub fn Warn() -> Element { pub fn Warn() -> Element {
rsx! { rsx! {
svg { svg {
class: "size-6", class: "size-6",
fill: "currentColor", fill: "currentColor",
view_box: "0 0 24 24", view_box: "0 0 24 24",
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",
path { path {
clip_rule: "evenodd", clip_rule: "evenodd",
d: "M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z", d: "M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z",
fill_rule: "evenodd", fill_rule: "evenodd",
} }
} }
} }
} }
#[component] #[component]
pub fn Error() -> Element { pub fn Error() -> Element {
rsx! { rsx! {
svg { svg {
class: "size-6", class: "size-6",
fill: "currentColor", fill: "currentColor",
view_box: "0 0 24 24", view_box: "0 0 24 24",
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",
path { path {
clip_rule: "evenodd", clip_rule: "evenodd",
d: "M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z", d: "M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z",
fill_rule: "evenodd", fill_rule: "evenodd",
} }
} }
} }
} }

View File

@@ -1,77 +1,77 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use tonic::IntoRequest; use tonic::IntoRequest;
use crate::{ use crate::{
components::MediaItem, components::MediaItem,
rpc::{aoba::PageFilter, get_rpc_client}, rpc::{aoba::PageFilter, get_rpc_client},
}; };
#[derive(PartialEq, Clone, Props)] #[derive(PartialEq, Clone, Props)]
pub struct MediaGridProps { pub struct MediaGridProps {
pub query: Option<String>, pub query: Option<String>,
#[props(default = Some(1))] #[props(default = Some(1))]
pub page: Option<i32>, pub page: Option<i32>,
#[props(default = Some(100))] #[props(default = Some(100))]
pub page_size: Option<i32>, pub page_size: Option<i32>,
} }
impl IntoRequest<PageFilter> for MediaGridProps { impl IntoRequest<PageFilter> for MediaGridProps {
fn into_request(self) -> tonic::Request<PageFilter> { fn into_request(self) -> tonic::Request<PageFilter> {
let f: PageFilter = self.into(); let f: PageFilter = self.into();
f.into_request() f.into_request()
} }
} }
impl Into<PageFilter> for MediaGridProps { impl Into<PageFilter> for MediaGridProps {
fn into(self) -> PageFilter { fn into(self) -> PageFilter {
PageFilter { PageFilter {
page: self.page, page: self.page,
page_size: self.page_size, page_size: self.page_size,
query: self.query, query: self.query,
} }
} }
} }
#[component] #[component]
pub fn MediaGrid(props: MediaGridProps) -> Element { pub fn MediaGrid(props: MediaGridProps) -> Element {
let media_result = use_resource(use_reactive!(|(props)| async move { let media_result = use_resource(use_reactive!(|(props)| async move {
let mut client = get_rpc_client(); let mut client = get_rpc_client();
let result = client.list_media(props.into_request()).await; let result = client.list_media(props.into_request()).await;
if let Ok(items) = result { if let Ok(items) = result {
return Ok(items.into_inner()); return Ok(items.into_inner());
} else { } else {
let err = result.err().unwrap(); let err = result.err().unwrap();
let message = err.message(); let message = err.message();
return Err(format!("Failed to load results: {message}")); return Err(format!("Failed to load results: {message}"));
} }
})); }));
match media_result.cloned() { match media_result.cloned() {
Some(value) => match value { Some(value) => match value {
Ok(result) => rsx! { Ok(result) => rsx! {
div { div {
class: "mediaGrid", class: "mediaGrid",
{result.items.iter().map(|itm| rsx!{ {result.items.iter().map(|itm| rsx!{
MediaItem { item: itm.clone() } MediaItem { item: itm.clone() }
})}, })},
} }
}, },
Err(msg) => rsx! { Err(msg) => rsx! {
div { div {
class: "mediaGrid", class: "mediaGrid",
div { div {
"Failed to load results: {msg}" "Failed to load results: {msg}"
} }
} }
}, },
}, },
None => rsx! { None => rsx! {
div{ div{
class: "mediaGrid", class: "mediaGrid",
div { div {
"Loading..." "Loading..."
} }
} }
}, },
} }
} }

View File

@@ -1,29 +1,29 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::{HOST, rpc::aoba::MediaModel}; use crate::{HOST, rpc::aoba::MediaModel};
#[derive(PartialEq, Clone, Props)] #[derive(PartialEq, Clone, Props)]
pub struct MediaItemProps { pub struct MediaItemProps {
pub item: MediaModel, pub item: MediaModel,
} }
#[component] #[component]
pub fn MediaItem(props: MediaItemProps) -> Element { pub fn MediaItem(props: MediaItemProps) -> Element {
let mtype = props.item.media_type().as_str_name(); let mtype = props.item.media_type().as_str_name();
let filename = props.item.file_name; let filename = props.item.file_name;
let id = props.item.media_id.unwrap().value; let id = props.item.media_id.unwrap().value;
let src = format!("{HOST}/m/thumb/{id}"); let src = format!("{HOST}/m/thumb/{id}");
rsx! { rsx! {
a { class: "mediaItem", href: "{HOST}/m/{id}", target: "_blank", a { class: "mediaItem", href: "{HOST}/m/{id}", target: "_blank",
img { src } img { src }
span { class: "info", span { class: "info",
span { class: "name", "{filename}" } span { class: "name", "{filename}" }
span { class: "details", span { class: "details",
span { "{mtype}" } span { "{mtype}" }
span { "{props.item.view_count}" } span { "{props.item.view_count}" }
} }
} }
} }
} }
} }

View File

@@ -1,12 +1,12 @@
pub mod basic; pub mod basic;
mod media_grid; mod media_grid;
mod media_item; mod media_item;
mod navbar; mod navbar;
mod notif; mod notif;
mod search; mod search;
pub use media_grid::*; pub use media_grid::*;
pub use media_item::*; pub use media_item::*;
pub use navbar::*; pub use navbar::*;
pub use notif::*; pub use notif::*;
pub use search::*; pub use search::*;
mod icons; mod icons;

View File

@@ -1,56 +1,56 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::{Route, contexts::AuthContext}; use crate::{Route, contexts::AuthContext};
const NAV_CSS: Asset = asset!("/assets/style/nav.scss"); const NAV_CSS: Asset = asset!("/assets/style/nav.scss");
const NAV_ICON: Asset = asset!("/assets/favicon.ico"); const NAV_ICON: Asset = asset!("/assets/favicon.ico");
#[component] #[component]
pub fn Navbar() -> Element { pub fn Navbar() -> Element {
rsx! { rsx! {
document::Link { rel: "stylesheet", href: NAV_CSS } document::Link { rel: "stylesheet", href: NAV_CSS }
nav { nav {
Branding {} Branding {}
MainNaviagation {} MainNaviagation {}
Widgets {} Widgets {}
Utils {} Utils {}
} }
} }
} }
#[component] #[component]
pub fn MainNaviagation() -> Element { pub fn MainNaviagation() -> Element {
rsx! { rsx! {
div { class: "mainNav", div { class: "mainNav",
Link { class: "navItem", to: Route::Home {}, "Home" } Link { class: "navItem", to: Route::Home {}, "Home" }
Link { class: "navItem", to: Route::Settings {}, "Settings" } Link { class: "navItem", to: Route::Settings {}, "Settings" }
} }
} }
} }
#[component] #[component]
pub fn Branding() -> Element { pub fn Branding() -> Element {
rsx! { rsx! {
div { class: "branding", div { class: "branding",
img { src: NAV_ICON, alt: "Aoba" } img { src: NAV_ICON, alt: "Aoba" }
} }
} }
} }
#[component] #[component]
pub fn Widgets() -> Element { pub fn Widgets() -> Element {
rsx! { rsx! {
div { class: "widgets" } div { class: "widgets" }
} }
} }
#[component] #[component]
pub fn Utils() -> Element { pub fn Utils() -> Element {
let mut auth_context = use_context::<AuthContext>(); let mut auth_context = use_context::<AuthContext>();
rsx! { rsx! {
div { class: "utils", div { class: "utils",
div { onclick: move |_| auth_context.logout(), "Logout" } div { onclick: move |_| auth_context.logout(), "Logout" }
} }
} }
} }

View File

@@ -1,39 +1,39 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::icons; use crate::components::icons;
#[derive(PartialEq, Clone, Props)] #[derive(PartialEq, Clone, Props)]
pub struct NotifProps { pub struct NotifProps {
r#type: Option<NotifType>, r#type: Option<NotifType>,
message: String, message: String,
} }
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
pub enum NotifType { pub enum NotifType {
Notice, Notice,
Error, Error,
Warning, Warning,
} }
#[component] #[component]
pub fn Notif(props: NotifProps) -> Element { pub fn Notif(props: NotifProps) -> Element {
let t = props.r#type.unwrap_or(NotifType::Notice); let t = props.r#type.unwrap_or(NotifType::Notice);
let type_class = match t { let type_class = match t {
NotifType::Notice => "notice", NotifType::Notice => "notice",
NotifType::Error => "error", NotifType::Error => "error",
NotifType::Warning => "warning", NotifType::Warning => "warning",
}; };
let m = props.message; let m = props.message;
rsx! { rsx! {
div { class: "notif {type_class}", div { class: "notif {type_class}",
div { class: "icon", div { class: "icon",
match t { match t {
NotifType::Notice => icons::Error(), NotifType::Notice => icons::Error(),
NotifType::Error => icons::Error(), NotifType::Error => icons::Error(),
NotifType::Warning => icons::Warn(), NotifType::Warning => icons::Warn(),
} }
} }
div { class: "message", "{m}" } div { class: "message", "{m}" }
} }
} }
} }

View File

@@ -1,15 +1,15 @@
use dioxus::prelude::*; use dioxus::prelude::*;
#[component] #[component]
pub fn Search(query: Signal<String>) -> Element { pub fn Search(query: Signal<String>) -> Element {
rsx! { rsx! {
div { class: "searchBar stickyTop", div { class: "searchBar stickyTop",
input { input {
r#type: "search", r#type: "search",
placeholder: "Search Files", placeholder: "Search Files",
value: query, value: query,
oninput: move |event| query.set(event.value()), oninput: move |event| query.set(event.value()),
} }
} }
} }
} }

View File

@@ -1,42 +1,42 @@
use dioxus::signals::{Signal, Writable}; use dioxus::signals::{Signal, Writable};
use web_sys::window; use web_sys::window;
use crate::rpc::{login, logout}; use crate::rpc::{login, logout};
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
pub struct AuthContext { pub struct AuthContext {
pub jwt: Signal<Option<String>>, pub jwt: Signal<Option<String>>,
} }
impl AuthContext { impl AuthContext {
pub fn login(&mut self, token: String) { pub fn login(&mut self, token: String) {
self.jwt.set(Some(token.clone())); self.jwt.set(Some(token.clone()));
let local_storage = window().unwrap().local_storage().unwrap().unwrap(); let local_storage = window().unwrap().local_storage().unwrap().unwrap();
_ = local_storage.set_item("token", token.as_str()); _ = local_storage.set_item("token", token.as_str());
login(token.clone()); login(token.clone());
} }
pub fn logout(&mut self) { pub fn logout(&mut self) {
self.jwt.set(None); self.jwt.set(None);
let local_storage = window().unwrap().local_storage().unwrap().unwrap(); let local_storage = window().unwrap().local_storage().unwrap().unwrap();
_ = local_storage.remove_item("token"); _ = local_storage.remove_item("token");
logout(); logout();
} }
pub fn new() -> Self { pub fn new() -> Self {
println!("new"); println!("new");
let local_storage = window().unwrap().local_storage().unwrap().unwrap(); let local_storage = window().unwrap().local_storage().unwrap().unwrap();
match local_storage.get_item("token") { match local_storage.get_item("token") {
Ok(value) => { Ok(value) => {
if let Some(jwt) = value { if let Some(jwt) = value {
login(jwt.clone()); login(jwt.clone());
return AuthContext { return AuthContext {
jwt: Signal::new(Some(jwt)), jwt: Signal::new(Some(jwt)),
}; };
} }
return AuthContext::default(); return AuthContext::default();
} }
Err(_) => AuthContext::default(), Err(_) => AuthContext::default(),
} }
} }
} }

View File

@@ -1,2 +1,2 @@
mod auth_context; mod auth_context;
pub use auth_context::*; pub use auth_context::*;

View File

@@ -1,19 +1,19 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::{Route, components::Navbar, contexts::AuthContext, views::Login}; use crate::{Route, components::Navbar, contexts::AuthContext, views::Login};
#[component] #[component]
pub fn MainLayout() -> Element { pub fn MainLayout() -> Element {
let auth_context = use_context::<AuthContext>(); let auth_context = use_context::<AuthContext>();
if auth_context.jwt.cloned().is_none() { if auth_context.jwt.cloned().is_none() {
return rsx! { return rsx! {
Login {} Login {}
}; };
} }
return rsx! { return rsx! {
Navbar {} Navbar {}
div { id: "content", Outlet::<Route> {} } div { id: "content", Outlet::<Route> {} }
}; };
} }

View File

@@ -1,2 +1,2 @@
mod main_layout; mod main_layout;
pub use main_layout::*; pub use main_layout::*;

View File

@@ -1,45 +1,45 @@
pub mod components; pub mod components;
pub mod contexts; pub mod contexts;
mod layouts; mod layouts;
pub mod models; pub mod models;
pub mod route; pub mod route;
pub mod rpc; pub mod rpc;
pub mod views; pub mod views;
use contexts::AuthContext; use contexts::AuthContext;
use dioxus::prelude::*; use dioxus::prelude::*;
use route::Route; use route::Route;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub const HOST: &'static str = "http://localhost:8081"; pub const HOST: &'static str = "http://localhost:8081";
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub const RPC_HOST: &'static str = "http://localhost:8081"; pub const RPC_HOST: &'static str = "http://localhost:8081";
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
pub const RPC_HOST: &'static str = "https://grpc.aoba.app:8443"; pub const RPC_HOST: &'static str = "https://grpc.aoba.app:8443";
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
pub const HOST: &'static str = "https://aoba.app"; pub const HOST: &'static str = "https://aoba.app";
const FAVICON: Asset = asset!("/assets/favicon.ico"); const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/style/main.scss"); const MAIN_CSS: Asset = asset!("/assets/style/main.scss");
const INPUT_CSS: Asset = asset!("/assets/style/inputs.scss"); const INPUT_CSS: Asset = asset!("/assets/style/inputs.scss");
fn main() { fn main() {
dioxus::launch(App); dioxus::launch(App);
} }
#[component] #[component]
fn App() -> Element { fn App() -> Element {
let _auth_state = use_context_provider(|| AuthContext::new()); let _auth_state = use_context_provider(|| AuthContext::new());
rsx! { rsx! {
document::Link { rel: "icon", href: FAVICON } document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" } document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" } document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" }
document::Link { rel: "stylesheet", href: MAIN_CSS } document::Link { rel: "stylesheet", href: MAIN_CSS }
document::Link { rel: "stylesheet", href: INPUT_CSS } document::Link { rel: "stylesheet", href: INPUT_CSS }
document::Link { document::Link {
rel: "stylesheet", rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap", href: "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap",
} }
Router::<Route> {} Router::<Route> {}
} }
} }

View File

@@ -1,23 +1,23 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
#[derive(Serialize, Deserialize, Clone, PartialEq)] #[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct Media { pub struct Media {
pub id: String, pub id: String,
pub media_id: String, pub media_id: String,
pub filename: String, pub filename: String,
pub media_type: MediaType, pub media_type: MediaType,
pub ext: String, pub ext: String,
pub view_count: i32, pub view_count: i32,
pub owner: String, pub owner: String,
} }
#[derive(Serialize_repr, Deserialize_repr, Clone, PartialEq)] #[derive(Serialize_repr, Deserialize_repr, Clone, PartialEq)]
#[repr(i32)] #[repr(i32)]
pub enum MediaType { pub enum MediaType {
Image, Image,
Audio, Audio,
Video, Video,
Text, Text,
Code, Code,
Raw, Raw,
} }

View File

@@ -1 +1 @@
pub mod media; pub mod media;

View File

@@ -1,16 +1,16 @@
use crate::{ use crate::{
layouts::MainLayout, layouts::MainLayout,
views::{Home, Settings}, views::{Home, Settings},
}; };
use dioxus::prelude::*; use dioxus::prelude::*;
#[derive(Debug, Clone, Routable, PartialEq)] #[derive(Debug, Clone, Routable, PartialEq)]
#[rustfmt::skip] #[rustfmt::skip]
pub enum Route { pub enum Route {
#[layout(MainLayout)] #[layout(MainLayout)]
#[route("/")] #[route("/")]
Home {}, Home {},
#[route("/settings")] #[route("/settings")]
Settings {}, Settings {},
// #[end_layout] // #[end_layout]
} }

View File

@@ -1,75 +1,75 @@
use std::sync::RwLock; use std::sync::RwLock;
use aoba::{aoba_rpc_client::AobaRpcClient, auth_rpc_client::AuthRpcClient}; use aoba::{aoba_rpc_client::AobaRpcClient, auth_rpc_client::AuthRpcClient};
use tonic::service::{Interceptor, interceptor::InterceptedService}; use tonic::service::{Interceptor, interceptor::InterceptedService};
use tonic_web_wasm_client::Client; use tonic_web_wasm_client::Client;
use crate::RPC_HOST; use crate::RPC_HOST;
pub mod aoba { pub mod aoba {
tonic::include_proto!("aoba"); tonic::include_proto!("aoba");
tonic::include_proto!("aoba.auth"); tonic::include_proto!("aoba.auth");
} }
static RPC_CLIENT: RpcConnection = RpcConnection { static RPC_CLIENT: RpcConnection = RpcConnection {
aoba: RwLock::new(None), aoba: RwLock::new(None),
auth: RwLock::new(None), auth: RwLock::new(None),
jwt: RwLock::new(None), jwt: RwLock::new(None),
}; };
#[derive(Default)] #[derive(Default)]
pub struct RpcConnection { pub struct RpcConnection {
aoba: RwLock<Option<AobaRpcClient<InterceptedService<Client, AuthInterceptor>>>>, aoba: RwLock<Option<AobaRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
auth: RwLock<Option<AuthRpcClient<Client>>>, auth: RwLock<Option<AuthRpcClient<Client>>>,
jwt: RwLock<Option<String>>, jwt: RwLock<Option<String>>,
} }
impl RpcConnection { impl RpcConnection {
pub fn get_client(&self) -> AobaRpcClient<InterceptedService<Client, AuthInterceptor>> { pub fn get_client(&self) -> AobaRpcClient<InterceptedService<Client, AuthInterceptor>> {
self.ensure_client(); self.ensure_client();
return self.aoba.read().unwrap().clone().unwrap(); return self.aoba.read().unwrap().clone().unwrap();
} }
pub fn get_auth_client(&self) -> AuthRpcClient<Client> { pub fn get_auth_client(&self) -> AuthRpcClient<Client> {
self.ensure_client(); self.ensure_client();
return self.auth.read().unwrap().clone().unwrap(); return self.auth.read().unwrap().clone().unwrap();
} }
fn ensure_client(&self) { fn ensure_client(&self) {
if self.aoba.read().unwrap().is_none() { if self.aoba.read().unwrap().is_none() {
let wasm_client = Client::new(RPC_HOST.into()); let wasm_client = Client::new(RPC_HOST.into());
let aoba_client = AobaRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor); let aoba_client = AobaRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor);
*self.aoba.write().unwrap() = Some(aoba_client); *self.aoba.write().unwrap() = Some(aoba_client);
*self.auth.write().unwrap() = Some(AuthRpcClient::new(wasm_client.clone())); *self.auth.write().unwrap() = Some(AuthRpcClient::new(wasm_client.clone()));
} }
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct AuthInterceptor; pub struct AuthInterceptor;
impl Interceptor for AuthInterceptor { impl Interceptor for AuthInterceptor {
fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> { fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
if let Some(jwt) = RPC_CLIENT.jwt.read().unwrap().clone() { if let Some(jwt) = RPC_CLIENT.jwt.read().unwrap().clone() {
request request
.metadata_mut() .metadata_mut()
.insert("authorization", format!("Bearer {jwt}").parse().unwrap()); .insert("authorization", format!("Bearer {jwt}").parse().unwrap());
} }
return Ok(request); return Ok(request);
} }
} }
pub fn get_rpc_client() -> AobaRpcClient<InterceptedService<Client, AuthInterceptor>> { pub fn get_rpc_client() -> AobaRpcClient<InterceptedService<Client, AuthInterceptor>> {
return RPC_CLIENT.get_client(); return RPC_CLIENT.get_client();
} }
pub fn get_auth_rpc_client() -> AuthRpcClient<Client> { pub fn get_auth_rpc_client() -> AuthRpcClient<Client> {
return RPC_CLIENT.get_auth_client(); return RPC_CLIENT.get_auth_client();
} }
pub fn login(jwt: String) { pub fn login(jwt: String) {
*RPC_CLIENT.jwt.write().unwrap() = Some(jwt); *RPC_CLIENT.jwt.write().unwrap() = Some(jwt);
} }
pub fn logout() { pub fn logout() {
*RPC_CLIENT.jwt.write().unwrap() = None; *RPC_CLIENT.jwt.write().unwrap() = None;
} }

View File

@@ -1,12 +1,12 @@
use crate::components::{MediaGrid, Search}; use crate::components::{MediaGrid, Search};
use dioxus::prelude::*; use dioxus::prelude::*;
#[component] #[component]
pub fn Home() -> Element { pub fn Home() -> Element {
let query = use_signal(|| "".to_string()); let query = use_signal(|| "".to_string());
rsx! { rsx! {
Search { query } Search { query }
MediaGrid { query: query.cloned() } MediaGrid { query: query.cloned() }
} }
} }

View File

@@ -1,78 +1,78 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use tonic::IntoRequest; use tonic::IntoRequest;
use crate::{ use crate::{
components::{basic::Input, Notif, NotifType}, components::{basic::Input, Notif, NotifType},
contexts::AuthContext, contexts::AuthContext,
rpc::{aoba::Credentials, get_auth_rpc_client}, rpc::{aoba::Credentials, get_auth_rpc_client},
}; };
#[component] #[component]
pub fn Login() -> Element { pub fn Login() -> Element {
let username = use_signal(|| "".to_string()); let username = use_signal(|| "".to_string());
let password = use_signal(|| "".to_string()); let password = use_signal(|| "".to_string());
let mut error: Signal<Option<String>> = use_signal(|| None); let mut error: Signal<Option<String>> = use_signal(|| None);
let mut auth_context = use_context::<AuthContext>(); let mut auth_context = use_context::<AuthContext>();
let login = move |e: Event<MouseData>| { let login = move |e: Event<MouseData>| {
e.prevent_default(); e.prevent_default();
if username.cloned().is_empty() || password.cloned().is_empty() { if username.cloned().is_empty() || password.cloned().is_empty() {
error.set(Some("Username and Password are required".into())); error.set(Some("Username and Password are required".into()));
return; return;
} }
spawn(async move { spawn(async move {
let mut auth = get_auth_rpc_client(); let mut auth = get_auth_rpc_client();
let result = auth let result = auth
.login( .login(
Credentials { Credentials {
user: username.cloned(), user: username.cloned(),
password: password.cloned(), password: password.cloned(),
} }
.into_request(), .into_request(),
) )
.await; .await;
match result { match result {
Ok(res) => { Ok(res) => {
match res.into_inner().result.unwrap() { match res.into_inner().result.unwrap() {
crate::rpc::aoba::login_response::Result::Jwt(jwt) => { crate::rpc::aoba::login_response::Result::Jwt(jwt) => {
auth_context.login(jwt.token); auth_context.login(jwt.token);
} }
crate::rpc::aoba::login_response::Result::Error(login_error) => { crate::rpc::aoba::login_response::Result::Error(login_error) => {
auth_context.logout(); auth_context.logout();
error.set(Some(login_error.message)); error.set(Some(login_error.message));
} }
}; };
} }
Err(_err) => { Err(_err) => {
auth_context.logout(); auth_context.logout();
} }
} }
}); });
}; };
rsx! { rsx! {
div { id: "centralModal", div { id: "centralModal",
if let Some(err) = error.cloned() { if let Some(err) = error.cloned() {
Notif { r#type: NotifType::Error, message: err } Notif { r#type: NotifType::Error, message: err }
} }
form { form {
Input { Input {
r#type: "text", r#type: "text",
name: "username", name: "username",
label: "Username", label: "Username",
value: username, value: username,
required: true, required: true,
} }
Input { Input {
r#type: "password", r#type: "password",
name: "password", name: "password",
label: "Password", label: "Password",
value: password, value: password,
required: true, required: true,
} }
button { onclick: login, "Login!" } button { onclick: login, "Login!" }
} }
} }
} }
} }

View File

@@ -1,7 +1,7 @@
mod home; mod home;
mod login; mod login;
pub use home::*; pub use home::*;
pub use login::*; pub use login::*;
mod settings; mod settings;
pub use settings::Settings; pub use settings::Settings;

View File

@@ -1,31 +1,31 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::rpc::get_rpc_client; use crate::rpc::get_rpc_client;
#[component] #[component]
pub fn Settings() -> Element { pub fn Settings() -> Element {
let dst = use_resource(async move || { let dst = use_resource(async move || {
let result = get_rpc_client().get_share_x_destination(()).await; let result = get_rpc_client().get_share_x_destination(()).await;
if let Ok(d) = result { if let Ok(d) = result {
if let Some(r) = d.into_inner().dst_result { if let Some(r) = d.into_inner().dst_result {
return match r { return match r {
crate::rpc::aoba::share_x_response::DstResult::Destination(json) => json, crate::rpc::aoba::share_x_response::DstResult::Destination(json) => json,
crate::rpc::aoba::share_x_response::DstResult::Error(err) => err, crate::rpc::aoba::share_x_response::DstResult::Error(err) => err,
}; };
} }
return "No Result".to_string(); return "No Result".to_string();
} }
let err = result.err().unwrap(); let err = result.err().unwrap();
let status = err.message(); let status = err.message();
return format!("Failed to load config: {status}").to_string(); return format!("Failed to load config: {status}").to_string();
}); });
let d = dst.cloned().unwrap_or("".to_string()); let d = dst.cloned().unwrap_or("".to_string());
rsx! { rsx! {
"this is settings" "this is settings"
div { div {
pre { class: "codeSelect", "{d}" } pre { class: "codeSelect", "{d}" }
} }
} }
} }

View File

@@ -1,21 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FFMpegCore" Version="5.2.0" /> <PackageReference Include="FFMpegCore" Version="5.2.0" />
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" /> <PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
<PackageReference Include="MaybeError" Version="1.1.0" /> <PackageReference Include="MaybeError" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
<PackageReference Include="MongoDB.Driver" Version="3.4.0" /> <PackageReference Include="MongoDB.Driver" Version="3.4.0" />
<PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.1.0" /> <PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.1.0" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.11.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.11.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,47 +1,47 @@
global using MaybeError; global using MaybeError;
using AobaCore.Services; using AobaCore.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver; using MongoDB.Driver;
using MongoDB.Driver.Core.Extensions.DiagnosticSources; using MongoDB.Driver.Core.Extensions.DiagnosticSources;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace AobaCore; namespace AobaCore;
public static class Extensions public static class Extensions
{ {
public static IServiceCollection AddAoba(this IServiceCollection services, string dbString) public static IServiceCollection AddAoba(this IServiceCollection services, string dbString)
{ {
var settings = MongoClientSettings.FromConnectionString(dbString); var settings = MongoClientSettings.FromConnectionString(dbString);
settings.ClusterConfigurator = cb => cb.Subscribe(new DiagnosticsActivityEventSubscriber()); settings.ClusterConfigurator = cb => cb.Subscribe(new DiagnosticsActivityEventSubscriber());
var dbClient = new MongoClient(settings); var dbClient = new MongoClient(settings);
var db = dbClient.GetDatabase("Aoba"); var db = dbClient.GetDatabase("Aoba");
services.AddSingleton(dbClient); services.AddSingleton(dbClient);
services.AddSingleton<IMongoDatabase>(db); services.AddSingleton<IMongoDatabase>(db);
services.AddSingleton<AobaService>(); services.AddSingleton<AobaService>();
services.AddSingleton<ThumbnailService>(); services.AddSingleton<ThumbnailService>();
services.AddSingleton<AccountsService>(); services.AddSingleton<AccountsService>();
services.AddHostedService<AobaIndexCreationService>(); services.AddHostedService<AobaIndexCreationService>();
return services; return services;
} }
public static async Task EnsureIndexAsync<T>(this IMongoCollection<T> collection, CreateIndexModel<T> indexModel) public static async Task EnsureIndexAsync<T>(this IMongoCollection<T> collection, CreateIndexModel<T> indexModel)
{ {
try try
{ {
await collection.Indexes.CreateOneAsync(indexModel); await collection.Indexes.CreateOneAsync(indexModel);
} }
catch (MongoCommandException e) when (e.Code == 85 || e.Code == 86) //CodeName "IndexOptionsConflict" or "NameConflict" catch (MongoCommandException e) when (e.Code == 85 || e.Code == 86) //CodeName "IndexOptionsConflict" or "NameConflict"
{ {
await collection.Indexes.DropOneAsync(indexModel.Options.Name); await collection.Indexes.DropOneAsync(indexModel.Options.Name);
await collection.Indexes.CreateOneAsync(indexModel); await collection.Indexes.CreateOneAsync(indexModel);
} }
} }
} }

View File

@@ -1,98 +1,98 @@
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
namespace AobaCore.Models; namespace AobaCore.Models;
[BsonIgnoreExtraElements] [BsonIgnoreExtraElements]
public class Media public class Media
{ {
[BsonId] [BsonId]
public ObjectId Id { get; set; } public ObjectId Id { get; set; }
public ObjectId MediaId { get; set; } public ObjectId MediaId { get; set; }
public string Filename { get; set; } public string Filename { get; set; }
public MediaType MediaType { get; set; } public MediaType MediaType { get; set; }
public string Ext { get; set; } public string Ext { get; set; }
public int ViewCount { get; set; } public int ViewCount { get; set; }
public ObjectId Owner { get; set; } public ObjectId Owner { get; set; }
public DateTime UploadDate { get; set; } public DateTime UploadDate { get; set; }
public static readonly Dictionary<string, MediaType> KnownTypes = new() public static readonly Dictionary<string, MediaType> KnownTypes = new()
{ {
{ ".jpg", MediaType.Image }, { ".jpg", MediaType.Image },
{ ".avif", MediaType.Image }, { ".avif", MediaType.Image },
{ ".jpeg", MediaType.Image }, { ".jpeg", MediaType.Image },
{ ".png", MediaType.Image }, { ".png", MediaType.Image },
{ ".apng", MediaType.Image }, { ".apng", MediaType.Image },
{ ".webp", MediaType.Image }, { ".webp", MediaType.Image },
{ ".ico", MediaType.Image }, { ".ico", MediaType.Image },
{ ".gif", MediaType.Image }, { ".gif", MediaType.Image },
{ ".mp3", MediaType.Audio }, { ".mp3", MediaType.Audio },
{ ".flac", MediaType.Audio }, { ".flac", MediaType.Audio },
{ ".alac", MediaType.Audio }, { ".alac", MediaType.Audio },
{ ".mp4", MediaType.Video }, { ".mp4", MediaType.Video },
{ ".webm", MediaType.Video }, { ".webm", MediaType.Video },
{ ".mov", MediaType.Video }, { ".mov", MediaType.Video },
{ ".avi", MediaType.Video }, { ".avi", MediaType.Video },
{ ".mkv", MediaType.Video }, { ".mkv", MediaType.Video },
{ ".txt", MediaType.Text }, { ".txt", MediaType.Text },
{ ".log", MediaType.Text }, { ".log", MediaType.Text },
{ ".css", MediaType.Code }, { ".css", MediaType.Code },
{ ".cs", MediaType.Code }, { ".cs", MediaType.Code },
{ ".cpp", MediaType.Code }, { ".cpp", MediaType.Code },
{ ".lua", MediaType.Code }, { ".lua", MediaType.Code },
{ ".js", MediaType.Code }, { ".js", MediaType.Code },
{ ".htm", MediaType.Code }, { ".htm", MediaType.Code },
{ ".html", MediaType.Code }, { ".html", MediaType.Code },
{ ".cshtml", MediaType.Code }, { ".cshtml", MediaType.Code },
{ ".xml", MediaType.Code }, { ".xml", MediaType.Code },
{ ".json", MediaType.Code }, { ".json", MediaType.Code },
{ ".py", MediaType.Code }, { ".py", MediaType.Code },
}; };
[BsonConstructor] [BsonConstructor]
private Media() private Media()
{ {
Filename = string.Empty; Filename = string.Empty;
Ext = string.Empty; Ext = string.Empty;
} }
public Media(ObjectId fileId, string filename, ObjectId owner) public Media(ObjectId fileId, string filename, ObjectId owner)
{ {
MediaType = GetMediaType(filename); MediaType = GetMediaType(filename);
Ext = Path.GetExtension(filename); Ext = Path.GetExtension(filename);
Filename = filename; Filename = filename;
MediaId = fileId; MediaId = fileId;
Owner = owner; Owner = owner;
Id = ObjectId.GenerateNewId(); Id = ObjectId.GenerateNewId();
UploadDate = DateTime.UtcNow; UploadDate = DateTime.UtcNow;
} }
public string GetMediaUrl() public string GetMediaUrl()
{ {
return this switch return this switch
{ {
//Media { MediaType: MediaType.Raw or MediaType.Text or MediaType.Code} => $"/i/dl/{MediaId}/{Filename}", //Media { MediaType: MediaType.Raw or MediaType.Text or MediaType.Code} => $"/i/dl/{MediaId}/{Filename}",
_ => $"/m/{MediaId}" _ => $"/m/{MediaId}"
}; };
} }
public static MediaType GetMediaType(string filename) public static MediaType GetMediaType(string filename)
{ {
string ext = Path.GetExtension(filename); string ext = Path.GetExtension(filename);
if (KnownTypes.TryGetValue(ext, out MediaType mType)) if (KnownTypes.TryGetValue(ext, out MediaType mType))
return mType; return mType;
else else
return MediaType.Raw; return MediaType.Raw;
} }
} }
public enum MediaType public enum MediaType
{ {
Image, Image,
Audio, Audio,
Video, Video,
Text, Text,
Code, Code,
Raw Raw
} }

View File

@@ -1,19 +1,19 @@
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
namespace AobaCore.Models; namespace AobaCore.Models;
public record MediaThumbnail public record MediaThumbnail
{ {
[BsonId] [BsonId]
public required ObjectId Id { get; init; } public required ObjectId Id { get; init; }
public Dictionary<ThumbnailSize, ObjectId> Sizes { get; set; } = []; public Dictionary<ThumbnailSize, ObjectId> Sizes { get; set; } = [];
} }
public enum ThumbnailSize public enum ThumbnailSize
{ {
Small = 128, Small = 128,
Medium = 256, Medium = 256,
Large = 512, Large = 512,
ExtraLarge = 1024 ExtraLarge = 1024
} }

View File

@@ -1,18 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace AobaCore.Models; namespace AobaCore.Models;
public class PagedResult<T>(List<T> items, int page, int pageSize, long totalItems) public class PagedResult<T>(List<T> items, int page, int pageSize, long totalItems)
{ {
public List<T> Items { get; set; } = items; public List<T> Items { get; set; } = items;
public int Page { get; set; } = page; public int Page { get; set; } = page;
public int PageSize { get; set; } = pageSize; public int PageSize { get; set; } = pageSize;
public long TotalItems { get; set; } = totalItems; public long TotalItems { get; set; } = totalItems;
public long TotalPages { get; set; } = totalItems / pageSize; public long TotalPages { get; set; } = totalItems / pageSize;
public string? Query { get; set; } public string? Query { get; set; }
} }

View File

@@ -1,36 +1,36 @@
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using System; using System;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace AobaCore.Models; namespace AobaCore.Models;
public class User public class User
{ {
[BsonId] [BsonId]
public ObjectId Id { get; set; } public ObjectId Id { get; set; }
public required string Username { get; set; } public required string Username { get; set; }
public required string PasswordHash { get; set; } public required string PasswordHash { get; set; }
public required string Role { get; set; } public required string Role { get; set; }
public bool IsArgon { get; set; } public bool IsArgon { get; set; }
public ObjectId[] ApiKeys { get; set; } = []; public ObjectId[] ApiKeys { get; set; } = [];
public List<ObjectId> RegTokens { get; set; } = []; public List<ObjectId> RegTokens { get; set; } = [];
public ClaimsIdentity GetIdentity() public ClaimsIdentity GetIdentity()
{ {
var id = new ClaimsIdentity(new[] var id = new ClaimsIdentity(new[]
{ {
new Claim(ClaimTypes.NameIdentifier, Id.ToString()), new Claim(ClaimTypes.NameIdentifier, Id.ToString()),
new Claim(ClaimTypes.Name, Username), new Claim(ClaimTypes.Name, Username),
}); });
if (Role != null) if (Role != null)
id.AddClaim(new Claim(ClaimTypes.Role, Role)); id.AddClaim(new Claim(ClaimTypes.Role, Role));
return id; return id;
} }
} }

View File

@@ -1,66 +1,66 @@
using AobaCore.Models; using AobaCore.Models;
using Isopoh.Cryptography.Argon2; using Isopoh.Cryptography.Argon2;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace AobaCore.Services; namespace AobaCore.Services;
public class AccountsService(IMongoDatabase db) public class AccountsService(IMongoDatabase db)
{ {
public readonly IMongoCollection<User> _users = db.GetCollection<User>("users"); public readonly IMongoCollection<User> _users = db.GetCollection<User>("users");
public async Task<User?> GetUserAsync(ObjectId id, CancellationToken cancellationToken = default) public async Task<User?> GetUserAsync(ObjectId id, CancellationToken cancellationToken = default)
{ {
return await _users.Find(u => u.Id == id).FirstOrDefaultAsync(cancellationToken); return await _users.Find(u => u.Id == id).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<User?> VerifyLoginAsync(string username, string password, CancellationToken cancellationToken = default) public async Task<User?> VerifyLoginAsync(string username, string password, CancellationToken cancellationToken = default)
{ {
var user = await _users.Find(u => u.Username == username).FirstOrDefaultAsync(cancellationToken); var user = await _users.Find(u => u.Username == username).FirstOrDefaultAsync(cancellationToken);
if(user == null) if(user == null)
return null; return null;
if(user.IsArgon && Argon2.Verify(user.PasswordHash, password)) if(user.IsArgon && Argon2.Verify(user.PasswordHash, password))
return user; return user;
if(LegacyVerifyPassword( password, user.PasswordHash)) if(LegacyVerifyPassword( password, user.PasswordHash))
{ {
#if !DEBUG #if !DEBUG
var argon2Hash = Argon2.Hash(password); var argon2Hash = Argon2.Hash(password);
var update = Builders<User>.Update.Set(u => u.PasswordHash, argon2Hash).Set(u => u.IsArgon, true); var update = Builders<User>.Update.Set(u => u.PasswordHash, argon2Hash).Set(u => u.IsArgon, true);
await _users.UpdateOneAsync(u => u.Id == user.Id, update, cancellationToken: cancellationToken); await _users.UpdateOneAsync(u => u.Id == user.Id, update, cancellationToken: cancellationToken);
#endif #endif
return user; return user;
} }
return null; return null;
} }
public static bool LegacyVerifyPassword(string password, string passwordHash) public static bool LegacyVerifyPassword(string password, string passwordHash)
{ {
if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(passwordHash)) if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(passwordHash))
return false; return false;
/* Extract the bytes */ /* Extract the bytes */
byte[] hashBytes = Convert.FromBase64String(passwordHash); byte[] hashBytes = Convert.FromBase64String(passwordHash);
/* Get the salt */ /* Get the salt */
byte[] salt = new byte[16]; byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16); Array.Copy(hashBytes, 0, salt, 0, 16);
/* Compute the hash on the password the user entered */ /* Compute the hash on the password the user entered */
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA1); var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA1);
byte[] hash = pbkdf2.GetBytes(20); byte[] hash = pbkdf2.GetBytes(20);
/* Compare the results */ /* Compare the results */
for (int i = 0; i < 20; i++) for (int i = 0; i < 20; i++)
if (hashBytes[i + 16] != hash[i]) if (hashBytes[i + 16] != hash[i])
return false; return false;
return true; return true;
} }
} }

View File

@@ -1,30 +1,30 @@
using AobaCore.Models; using AobaCore.Models;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers; using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver; using MongoDB.Driver;
namespace AobaCore.Services; namespace AobaCore.Services;
public class AobaIndexCreationService(IMongoDatabase db): BackgroundService public class AobaIndexCreationService(IMongoDatabase db): BackgroundService
{ {
private readonly IMongoCollection<Media> _media = db.GetCollection<Media>("media"); private readonly IMongoCollection<Media> _media = db.GetCollection<Media>("media");
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
BsonSerializer.RegisterSerializer(new EnumSerializer<ThumbnailSize>(BsonType.String)); BsonSerializer.RegisterSerializer(new EnumSerializer<ThumbnailSize>(BsonType.String));
var textKeys = Builders<Media>.IndexKeys var textKeys = Builders<Media>.IndexKeys
.Text(m => m.Filename); .Text(m => m.Filename);
var textModel = new CreateIndexModel<Media>(textKeys, new CreateIndexOptions var textModel = new CreateIndexModel<Media>(textKeys, new CreateIndexOptions
{ {
Name = "Text", Name = "Text",
Background = true Background = true
}); });
await _media.EnsureIndexAsync(textModel); await _media.EnsureIndexAsync(textModel);
} }
} }

View File

@@ -1,98 +1,98 @@
using AobaCore.Models; using AobaCore.Models;
using MaybeError.Errors; using MaybeError.Errors;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using MongoDB.Driver.GridFS; using MongoDB.Driver.GridFS;
namespace AobaCore.Services; namespace AobaCore.Services;
public class AobaService(IMongoDatabase db) public class AobaService(IMongoDatabase db)
{ {
private readonly IMongoCollection<Media> _media = db.GetCollection<Media>("media"); private readonly IMongoCollection<Media> _media = db.GetCollection<Media>("media");
private readonly GridFSBucket _gridFs = new(db); private readonly GridFSBucket _gridFs = new(db);
public async Task<Media?> GetMediaAsync(ObjectId id, CancellationToken cancellationToken = default) public async Task<Media?> GetMediaAsync(ObjectId id, CancellationToken cancellationToken = default)
{ {
return await _media.Find(m => m.Id == id).FirstOrDefaultAsync(cancellationToken); return await _media.Find(m => m.Id == id).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<Media?> GetMediaFromFileAsync(ObjectId id, CancellationToken cancellationToken = default) public async Task<Media?> GetMediaFromFileAsync(ObjectId id, CancellationToken cancellationToken = default)
{ {
return await _media.Find(m => m.MediaId == id).FirstOrDefaultAsync(cancellationToken); return await _media.Find(m => m.MediaId == id).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<PagedResult<Media>> FindMediaAsync(string? query, ObjectId userId, int page = 1, int pageSize = 100) public async Task<PagedResult<Media>> FindMediaAsync(string? query, ObjectId userId, int page = 1, int pageSize = 100)
{ {
var filter = Builders<Media>.Filter.And([ var filter = Builders<Media>.Filter.And([
string.IsNullOrWhiteSpace(query) ? "{}" : Builders<Media>.Filter.Text(query), string.IsNullOrWhiteSpace(query) ? "{}" : Builders<Media>.Filter.Text(query),
Builders<Media>.Filter.Eq(m => m.Owner, userId) Builders<Media>.Filter.Eq(m => m.Owner, userId)
]); ]);
var sort = Builders<Media>.Sort.Descending(m => m.UploadDate); var sort = Builders<Media>.Sort.Descending(m => m.UploadDate);
var find = _media.Find(filter); var find = _media.Find(filter);
var total = await find.CountDocumentsAsync(); var total = await find.CountDocumentsAsync();
page -= 1; page -= 1;
var items = await find.Sort(sort).Skip(page * pageSize).Limit(pageSize).ToListAsync(); var items = await find.Sort(sort).Skip(page * pageSize).Limit(pageSize).ToListAsync();
return new PagedResult<Media>(items, page, pageSize, total); return new PagedResult<Media>(items, page, pageSize, total);
} }
public Task AddMediaAsync(Media media, CancellationToken cancellationToken = default) public Task AddMediaAsync(Media media, CancellationToken cancellationToken = default)
{ {
return _media.InsertOneAsync(media, null, cancellationToken); return _media.InsertOneAsync(media, null, cancellationToken);
} }
public Task IncrementViewCountAsync(ObjectId id, CancellationToken cancellationToken = default) public Task IncrementViewCountAsync(ObjectId id, CancellationToken cancellationToken = default)
{ {
return _media.UpdateOneAsync(m => m.Id == id, Builders<Media>.Update.Inc(m => m.ViewCount, 1), cancellationToken: cancellationToken); return _media.UpdateOneAsync(m => m.Id == id, Builders<Media>.Update.Inc(m => m.ViewCount, 1), cancellationToken: cancellationToken);
} }
public Task IncrementFileViewCountAsync(ObjectId fileId, CancellationToken cancellationToken = default) public Task IncrementFileViewCountAsync(ObjectId fileId, CancellationToken cancellationToken = default)
{ {
return _media.UpdateOneAsync(m => m.MediaId == fileId, Builders<Media>.Update.Inc(m => m.ViewCount, 1), cancellationToken: cancellationToken); return _media.UpdateOneAsync(m => m.MediaId == fileId, Builders<Media>.Update.Inc(m => m.ViewCount, 1), cancellationToken: cancellationToken);
} }
public async Task<Maybe<Media>> UploadFileAsync(Stream data, string filename, ObjectId owner, CancellationToken cancellationToken = default) public async Task<Maybe<Media>> UploadFileAsync(Stream data, string filename, ObjectId owner, CancellationToken cancellationToken = default)
{ {
try try
{ {
var fileId = await _gridFs.UploadFromStreamAsync(filename, data, cancellationToken: cancellationToken); var fileId = await _gridFs.UploadFromStreamAsync(filename, data, cancellationToken: cancellationToken);
var media = new Media(fileId, filename, owner); var media = new Media(fileId, filename, owner);
await AddMediaAsync(media, cancellationToken); await AddMediaAsync(media, cancellationToken);
return media; return media;
} }
catch (Exception ex) catch (Exception ex)
{ {
return ex; return ex;
} }
} }
public async Task<MaybeEx<GridFSDownloadStream, GridFSException>> GetFileStreamAsync(ObjectId id, bool seekable = false, CancellationToken cancellationToken = default) public async Task<MaybeEx<GridFSDownloadStream, GridFSException>> GetFileStreamAsync(ObjectId id, bool seekable = false, CancellationToken cancellationToken = default)
{ {
try try
{ {
return await _gridFs.OpenDownloadStreamAsync(id, new GridFSDownloadOptions { Seekable = seekable }, cancellationToken); return await _gridFs.OpenDownloadStreamAsync(id, new GridFSDownloadOptions { Seekable = seekable }, cancellationToken);
} }
catch (GridFSException ex) catch (GridFSException ex)
{ {
return ex; return ex;
} }
} }
public async Task DeleteFileAsync(ObjectId fileId, CancellationToken cancellationToken = default) public async Task DeleteFileAsync(ObjectId fileId, CancellationToken cancellationToken = default)
{ {
try try
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
await _gridFs.DeleteAsync(fileId, CancellationToken.None); await _gridFs.DeleteAsync(fileId, CancellationToken.None);
await _media.DeleteOneAsync(m => m.MediaId == fileId, CancellationToken.None); await _media.DeleteOneAsync(m => m.MediaId == fileId, CancellationToken.None);
} }
catch (GridFSFileNotFoundException) catch (GridFSFileNotFoundException)
{ {
//ignore if file was not found //ignore if file was not found
} }
} }
} }

View File

@@ -1,145 +1,145 @@
using AobaCore.Models; using AobaCore.Models;
using FFMpegCore; using FFMpegCore;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
using MaybeError.Errors; using MaybeError.Errors;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using MongoDB.Driver.GridFS; using MongoDB.Driver.GridFS;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace AobaCore.Services; namespace AobaCore.Services;
public class ThumbnailService(IMongoDatabase db, AobaService aobaService) public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
{ {
private readonly GridFSBucket _gridfs = new GridFSBucket(db); private readonly GridFSBucket _gridfs = new GridFSBucket(db);
private readonly IMongoCollection<MediaThumbnail> _thumbnails = db.GetCollection<MediaThumbnail>("thumbs"); private readonly IMongoCollection<MediaThumbnail> _thumbnails = db.GetCollection<MediaThumbnail>("thumbs");
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="id">File id</param> /// <param name="id">File id</param>
/// <param name="size"></param> /// <param name="size"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public async Task<Maybe<Stream>> GetOrCreateThumbnailAsync(ObjectId id, ThumbnailSize size, CancellationToken cancellationToken = default) public async Task<Maybe<Stream>> GetOrCreateThumbnailAsync(ObjectId id, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
var existingThumb = await GetThumbnailAsync(id, size, cancellationToken); var existingThumb = await GetThumbnailAsync(id, size, cancellationToken);
if (existingThumb != null) if (existingThumb != null)
return existingThumb; return existingThumb;
var media = await aobaService.GetMediaFromFileAsync(id, cancellationToken); var media = await aobaService.GetMediaFromFileAsync(id, cancellationToken);
if (media == null) if (media == null)
return new Error("Media does not exist"); return new Error("Media does not exist");
try try
{ {
using var mediaData = await _gridfs.OpenDownloadStreamAsync(media.MediaId, new GridFSDownloadOptions { Seekable = true }, cancellationToken); using var mediaData = await _gridfs.OpenDownloadStreamAsync(media.MediaId, new GridFSDownloadOptions { Seekable = true }, cancellationToken);
var thumb = await GenerateThumbnailAsync(mediaData, size, media.MediaType, media.Ext, cancellationToken); var thumb = await GenerateThumbnailAsync(mediaData, size, media.MediaType, media.Ext, cancellationToken);
if (thumb.HasError) if (thumb.HasError)
return thumb.Error; return thumb.Error;
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
#if !DEBUG #if !DEBUG
var thumbId = await _gridfs.UploadFromStreamAsync($"{media.Filename}.webp", thumb, cancellationToken: CancellationToken.None); var thumbId = await _gridfs.UploadFromStreamAsync($"{media.Filename}.webp", thumb, cancellationToken: CancellationToken.None);
var update = Builders<MediaThumbnail>.Update.Set(t => t.Sizes[size], thumbId); var update = Builders<MediaThumbnail>.Update.Set(t => t.Sizes[size], thumbId);
await _thumbnails.UpdateOneAsync(t => t.Id == id, update, cancellationToken: CancellationToken.None); await _thumbnails.UpdateOneAsync(t => t.Id == id, update, cancellationToken: CancellationToken.None);
#endif #endif
thumb.Value.Position = 0; thumb.Value.Position = 0;
return thumb; return thumb;
} catch (Exception ex) { } catch (Exception ex) {
return ex; return ex;
} }
} }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="id">File Id</param> /// <param name="id">File Id</param>
/// <param name="size"></param> /// <param name="size"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public async Task<Stream?> GetThumbnailAsync(ObjectId id, ThumbnailSize size, CancellationToken cancellationToken = default) public async Task<Stream?> GetThumbnailAsync(ObjectId id, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
var thumb = await _thumbnails.Find(t => t.Id == id).FirstOrDefaultAsync(cancellationToken); var thumb = await _thumbnails.Find(t => t.Id == id).FirstOrDefaultAsync(cancellationToken);
if (thumb == null) if (thumb == null)
return null; return null;
if (!thumb.Sizes.TryGetValue(size, out var tid)) if (!thumb.Sizes.TryGetValue(size, out var tid))
return null; return null;
var thumbData = await _gridfs.OpenDownloadStreamAsync(tid, cancellationToken: cancellationToken); var thumbData = await _gridfs.OpenDownloadStreamAsync(tid, cancellationToken: cancellationToken);
return thumbData; return thumbData;
} }
public async Task<Maybe<Stream>> GenerateThumbnailAsync(Stream stream, ThumbnailSize size, MediaType type, string ext, CancellationToken cancellationToken = default) public async Task<Maybe<Stream>> GenerateThumbnailAsync(Stream stream, ThumbnailSize size, MediaType type, string ext, CancellationToken cancellationToken = default)
{ {
return type switch return type switch
{ {
MediaType.Image => await GenerateImageThumbnailAsync(stream, size, cancellationToken), MediaType.Image => await GenerateImageThumbnailAsync(stream, size, cancellationToken),
MediaType.Video => await GenerateVideoThumbnailAsync(stream, size, cancellationToken), MediaType.Video => await GenerateVideoThumbnailAsync(stream, size, cancellationToken),
MediaType.Text or MediaType.Code => await GenerateDocumentThumbnailAsync(stream, size, cancellationToken), MediaType.Text or MediaType.Code => await GenerateDocumentThumbnailAsync(stream, size, cancellationToken),
_ => new Error($"No Thumbnail for {type}"), _ => new Error($"No Thumbnail for {type}"),
}; };
} }
public async Task<Stream> GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, CancellationToken cancellationToken = default) public async Task<Stream> GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
var img = Image.Load(stream); var img = Image.Load(stream);
img.Mutate(o => img.Mutate(o =>
{ {
var size = var size =
o.Resize(new ResizeOptions o.Resize(new ResizeOptions
{ {
Position = AnchorPositionMode.Center, Position = AnchorPositionMode.Center,
Mode = ResizeMode.Crop, Mode = ResizeMode.Crop,
Size = new Size(300, 300) Size = new Size(300, 300)
}); });
}); });
var result = new MemoryStream(); var result = new MemoryStream();
await img.SaveAsWebpAsync(result, cancellationToken); await img.SaveAsWebpAsync(result, cancellationToken);
result.Position = 0; result.Position = 0;
return result; return result;
} }
public async Task<Maybe<Stream>> GenerateVideoThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) public async Task<Maybe<Stream>> GenerateVideoThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
var w = (int)size; var w = (int)size;
var source = new MemoryStream(); var source = new MemoryStream();
data.CopyTo(source); data.CopyTo(source);
source.Position = 0; source.Position = 0;
var output = new MemoryStream(); var output = new MemoryStream();
await FFMpegArguments.FromPipeInput(new StreamPipeSource(source)) await FFMpegArguments.FromPipeInput(new StreamPipeSource(source))
.OutputToPipe(new StreamPipeSink(output), opt => .OutputToPipe(new StreamPipeSink(output), opt =>
{ {
opt.WithCustomArgument($"-t 5 -vf \"crop='min(in_w,in_h)':'min(in_w,in_h)',scale={w}:{w}\" -loop 0") opt.WithCustomArgument($"-t 5 -vf \"crop='min(in_w,in_h)':'min(in_w,in_h)',scale={w}:{w}\" -loop 0")
.ForceFormat("webp"); .ForceFormat("webp");
}).ProcessAsynchronously(); }).ProcessAsynchronously();
output.Position = 0; output.Position = 0;
return output; return output;
} }
public async Task<Maybe<Stream>> GenerateDocumentThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) public async Task<Maybe<Stream>> GenerateDocumentThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
return new NotImplementedException(); return new NotImplementedException();
} }
} }

View File

@@ -1 +1 @@
wwwroot wwwroot

View File

@@ -1 +1 @@
wwwroot/* wwwroot/*

View File

@@ -1,39 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>9ffcc706-7f1b-48e3-bf30-eab69a90fded</UserSecretsId> <UserSecretsId>9ffcc706-7f1b-48e3-bf30-eab69a90fded</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" /> <PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.71.0" /> <PackageReference Include="Grpc.AspNetCore.Web" Version="2.71.0" />
<PackageReference Include="Grpc.Tools" Version="2.72.0"> <PackageReference Include="Grpc.Tools" Version="2.72.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" /> <PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.12.1" /> <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.12.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
<PackageReference Include="MimeTypesMap" Version="1.0.9" /> <PackageReference Include="MimeTypesMap" Version="1.0.9" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" /> <PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\AobaCore\AobaCore.csproj" /> <ProjectReference Include="..\AobaCore\AobaCore.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Protobuf Include="Proto\Aoba.proto"></Protobuf> <Protobuf Include="Proto\Aoba.proto"></Protobuf>
<Protobuf Include="Proto\Auth.proto"></Protobuf> <Protobuf Include="Proto\Auth.proto"></Protobuf>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,28 +1,28 @@
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
namespace AobaServer.Auth; namespace AobaServer.Auth;
internal class AobaAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder) internal class AobaAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{ {
protected override Task<AuthenticateResult> HandleAuthenticateAsync() protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
protected override Task HandleChallengeAsync(AuthenticationProperties properties) protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{ {
Response.StatusCode = StatusCodes.Status401Unauthorized; Response.StatusCode = StatusCodes.Status401Unauthorized;
Response.BodyWriter.Complete(); Response.BodyWriter.Complete();
return Task.CompletedTask; return Task.CompletedTask;
} }
protected override Task HandleForbiddenAsync(AuthenticationProperties properties) protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
{ {
Response.StatusCode = StatusCodes.Status403Forbidden; Response.StatusCode = StatusCodes.Status403Forbidden;
Response.BodyWriter.Complete(); Response.BodyWriter.Complete();
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@@ -1,44 +1,44 @@
using AobaServer.Models; using AobaServer.Models;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
namespace AobaServer.Auth; namespace AobaServer.Auth;
public class MetricsTokenValidator(AuthInfo authInfo) : JwtSecurityTokenHandler public class MetricsTokenValidator(AuthInfo authInfo) : JwtSecurityTokenHandler
{ {
private readonly JwtSecurityTokenHandler _handler = new(); private readonly JwtSecurityTokenHandler _handler = new();
public override Task<TokenValidationResult> ValidateTokenAsync(string token, TokenValidationParameters validationParameters) public override Task<TokenValidationResult> ValidateTokenAsync(string token, TokenValidationParameters validationParameters)
{ {
try try
{ {
var principal = _handler.ValidateToken(token, new TokenValidationParameters var principal = _handler.ValidateToken(token, new TokenValidationParameters
{ {
ValidateIssuerSigningKey = true, ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(authInfo.SecureKey), IssuerSigningKey = new SymmetricSecurityKey(authInfo.SecureKey),
ValidateIssuer = true, ValidateIssuer = true,
ValidIssuer = authInfo.Issuer, ValidIssuer = authInfo.Issuer,
ValidateAudience = true, ValidateAudience = true,
ValidAudience = "metrics", ValidAudience = "metrics",
ValidateLifetime = false, ValidateLifetime = false,
ClockSkew = TimeSpan.FromMinutes(1) ClockSkew = TimeSpan.FromMinutes(1)
}, out var validatedToken); }, out var validatedToken);
return Task.FromResult(new TokenValidationResult return Task.FromResult(new TokenValidationResult
{ {
IsValid = true, IsValid = true,
SecurityToken = validatedToken, SecurityToken = validatedToken,
ClaimsIdentity = new ClaimsIdentity(principal.Identity), ClaimsIdentity = new ClaimsIdentity(principal.Identity),
}); });
} }
catch (Exception e) catch (Exception e)
{ {
return Task.FromResult(new TokenValidationResult return Task.FromResult(new TokenValidationResult
{ {
IsValid = false, IsValid = false,
Exception = e Exception = e
}); });
} }
} }
} }

View File

@@ -1,19 +1,19 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace AobaServer.Controllers.Api; namespace AobaServer.Controllers.Api;
[Route("/api/auth")] [Route("/api/auth")]
public class AuthApi : ControllerBase public class AuthApi : ControllerBase
{ {
[HttpGet("login")] [HttpGet("login")]
public Task<IActionResult> LoginAsync() public Task<IActionResult> LoginAsync()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
[HttpGet("register")] [HttpGet("register")]
public Task<IActionResult> RegisterAsync() public Task<IActionResult> RegisterAsync()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
} }

View File

@@ -1,38 +1,38 @@
using AobaCore.Models; using AobaCore.Models;
using AobaCore.Services; using AobaCore.Services;
using AobaServer.Utils; using AobaServer.Utils;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MongoDB.Bson; using MongoDB.Bson;
namespace AobaServer.Controllers.Api; namespace AobaServer.Controllers.Api;
[ApiController, Authorize] [ApiController, Authorize]
[Route("/api/media")] [Route("/api/media")]
public class MediaApi(AobaService aoba) : ControllerBase public class MediaApi(AobaService aoba) : ControllerBase
{ {
[HttpPost("upload")] [HttpPost("upload")]
public async Task<IActionResult> UploadAsync([FromForm] IFormFile file, CancellationToken cancellationToken) public async Task<IActionResult> UploadAsync([FromForm] IFormFile file, CancellationToken cancellationToken)
{ {
var media = await aoba.UploadFileAsync(file.OpenReadStream(), file.FileName, User.GetId(), cancellationToken); var media = await aoba.UploadFileAsync(file.OpenReadStream(), file.FileName, User.GetId(), cancellationToken);
if (media.HasError) if (media.HasError)
return Problem(detail: media.Error.Message, statusCode: StatusCodes.Status400BadRequest); return Problem(detail: media.Error.Message, statusCode: StatusCodes.Status400BadRequest);
return Ok(new return Ok(new
{ {
media = media.Value, media = media.Value,
url = media.Value.GetMediaUrl() url = media.Value.GetMediaUrl()
}); });
} }
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<IActionResult> Delete(ObjectId id, CancellationToken cancellationToken) public async Task<IActionResult> Delete(ObjectId id, CancellationToken cancellationToken)
{ {
await aoba.DeleteFileAsync(id, cancellationToken); await aoba.DeleteFileAsync(id, cancellationToken);
return Ok(); return Ok();
} }
} }

View File

@@ -1,37 +1,37 @@
using AobaCore.Services; using AobaCore.Services;
using AobaServer.Models; using AobaServer.Models;
using AobaServer.Utils; using AobaServer.Utils;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Net; using System.Net;
namespace AobaServer.Controllers; namespace AobaServer.Controllers;
//allow login via http during debug testing //allow login via http during debug testing
#if DEBUG #if DEBUG
[AllowAnonymous] [AllowAnonymous]
[Route("auth")] [Route("auth")]
public class AuthController(AccountsService accountsService, AuthInfo authInfo) : Controller public class AuthController(AccountsService accountsService, AuthInfo authInfo) : Controller
{ {
[HttpPost("login")] [HttpPost("login")]
public async Task<IActionResult> Login([FromForm] string username, [FromForm] string password, CancellationToken cancellationToken) public async Task<IActionResult> Login([FromForm] string username, [FromForm] string password, CancellationToken cancellationToken)
{ {
var user = await accountsService.VerifyLoginAsync(username, password, cancellationToken); var user = await accountsService.VerifyLoginAsync(username, password, cancellationToken);
if (user == null) if (user == null)
return Problem("Invalid login Credentials", statusCode: StatusCodes.Status400BadRequest); return Problem("Invalid login Credentials", statusCode: StatusCodes.Status400BadRequest);
Response.Cookies.Append("token", user.GetToken(authInfo), new CookieOptions Response.Cookies.Append("token", user.GetToken(authInfo), new CookieOptions
{ {
IsEssential = true, IsEssential = true,
SameSite = SameSiteMode.Strict, SameSite = SameSiteMode.Strict,
Secure = true, Secure = true,
}); });
return Ok(); return Ok();
} }
} }
#endif #endif

View File

@@ -1,65 +1,65 @@
using AobaCore.Models; using AobaCore.Models;
using AobaCore.Services; using AobaCore.Services;
using HeyRed.Mime; using HeyRed.Mime;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
namespace AobaServer.Controllers; namespace AobaServer.Controllers;
[Route("/m")] [Route("/m")]
public class MediaController(AobaService aobaService, ILogger<MediaController> logger) : Controller public class MediaController(AobaService aobaService, ILogger<MediaController> logger) : Controller
{ {
[HttpGet("{id}")] [HttpGet("{id}")]
[ResponseCache(Duration = int.MaxValue)] [ResponseCache(Duration = int.MaxValue)]
public async Task<IActionResult> MediaAsync(ObjectId id, [FromServices] MongoClient client, CancellationToken cancellationToken) public async Task<IActionResult> MediaAsync(ObjectId id, [FromServices] MongoClient client, CancellationToken cancellationToken)
{ {
var file = await aobaService.GetFileStreamAsync(id, cancellationToken: cancellationToken); var file = await aobaService.GetFileStreamAsync(id, cancellationToken: cancellationToken);
if (file.HasError) if (file.HasError)
{ {
logger.LogError(file.Error.Exception, "Failed to load media stream"); logger.LogError(file.Error.Exception, "Failed to load media stream");
return NotFound(); return NotFound();
} }
var mime = MimeTypesMap.GetMimeType(file.Value.FileInfo.Filename); var mime = MimeTypesMap.GetMimeType(file.Value.FileInfo.Filename);
_ = aobaService.IncrementFileViewCountAsync(id, cancellationToken); _ = aobaService.IncrementFileViewCountAsync(id, cancellationToken);
return File(file, mime, true); return File(file, mime, true);
} }
/// <summary> /// <summary>
/// Redirect legacy media urls to the new url /// Redirect legacy media urls to the new url
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id"></param>
/// <param name="rest"></param> /// <param name="rest"></param>
/// <param name="aoba"></param> /// <param name="aoba"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("/i/{id}/{*rest}")] [HttpGet("/i/{id}/{*rest}")]
public async Task<IActionResult> LegacyRedirectAsync(ObjectId id, string rest, CancellationToken cancellationToken) public async Task<IActionResult> LegacyRedirectAsync(ObjectId id, string rest, CancellationToken cancellationToken)
{ {
var media = await aobaService.GetMediaAsync(id, cancellationToken); var media = await aobaService.GetMediaAsync(id, cancellationToken);
if (media == null) if (media == null)
return NotFound(); return NotFound();
return LocalRedirectPermanent($"/m/{media.MediaId}/{rest}"); return LocalRedirectPermanent($"/m/{media.MediaId}/{rest}");
} }
[HttpGet("thumb/{id}")] [HttpGet("thumb/{id}")]
[ResponseCache(Duration = int.MaxValue)] [ResponseCache(Duration = int.MaxValue)]
public async Task<IActionResult> ThumbAsync(ObjectId id, [FromServices] ThumbnailService thumbnailService, [FromQuery] ThumbnailSize size = ThumbnailSize.Medium, CancellationToken cancellationToken = default) public async Task<IActionResult> ThumbAsync(ObjectId id, [FromServices] ThumbnailService thumbnailService, [FromQuery] ThumbnailSize size = ThumbnailSize.Medium, CancellationToken cancellationToken = default)
{ {
var thumb = await thumbnailService.GetOrCreateThumbnailAsync(id, size, cancellationToken); var thumb = await thumbnailService.GetOrCreateThumbnailAsync(id, size, cancellationToken);
if (thumb.HasError) if (thumb.HasError)
{ {
logger.LogError("Failed to generate thumbnail: {}", thumb.Error); logger.LogError("Failed to generate thumbnail: {}", thumb.Error);
return DefaultThumbnailAsync(); return DefaultThumbnailAsync();
} }
return File(thumb, "image/webp", true); return File(thumb, "image/webp", true);
} }
[NonAction] [NonAction]
private IActionResult DefaultThumbnailAsync() private IActionResult DefaultThumbnailAsync()
{ {
return NoContent(); return NoContent();
} }
} }

View File

@@ -1,61 +1,61 @@
# Client Side build - prep deps # Client Side build - prep deps
FROM rust:1 AS chef FROM rust:1 AS chef
RUN rustup target add wasm32-unknown-unknown RUN rustup target add wasm32-unknown-unknown
RUN cargo install cargo-chef RUN cargo install cargo-chef
WORKDIR /app WORKDIR /app
FROM chef AS planner FROM chef AS planner
COPY . . COPY . .
WORKDIR /app/AobaClient WORKDIR /app/AobaClient
RUN cargo chef prepare --recipe-path recipe.json RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS client-builder FROM chef AS client-builder
WORKDIR /app/AobaClient WORKDIR /app/AobaClient
COPY --from=planner /app/AobaClient/recipe.json recipe.json COPY --from=planner /app/AobaClient/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json RUN cargo chef cook --release --recipe-path recipe.json
COPY /AobaClient /app/AobaClient COPY /AobaClient /app/AobaClient
COPY /AobaServer/Proto /app/AobaServer/Proto COPY /AobaServer/Proto /app/AobaServer/Proto
# Install Protobuf # Install Protobuf
RUN apt update RUN apt update
RUN apt install -y protobuf-compiler libprotobuf-dev RUN apt install -y protobuf-compiler libprotobuf-dev
# Install `dx` # Install `dx`
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
RUN cargo binstall dioxus-cli --root /.cargo -y --force RUN cargo binstall dioxus-cli --root /.cargo -y --force
ENV PATH="/.cargo/bin:$PATH" ENV PATH="/.cargo/bin:$PATH"
# Create the final bundle folder. Bundle always executes in release mode with optimizations enabled # Create the final bundle folder. Bundle always executes in release mode with optimizations enabled
RUN dx bundle --platform web RUN dx bundle --platform web
# Server Build # Server Build
# This stage is used when running from VS in fast mode (Default for Debug configuration) # This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID USER $APP_UID
WORKDIR /app WORKDIR /app
EXPOSE 8080 EXPOSE 8080
EXPOSE 8081 EXPOSE 8081
# This stage is used to build the service project # This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release ARG BUILD_CONFIGURATION=Release
WORKDIR /src WORKDIR /src
COPY ["AobaServer/AobaServer.csproj", "AobaServer/"] COPY ["AobaServer/AobaServer.csproj", "AobaServer/"]
RUN dotnet restore "./AobaServer/AobaServer.csproj" RUN dotnet restore "./AobaServer/AobaServer.csproj"
COPY . . COPY . .
# Copy Built bundle from client builder # Copy Built bundle from client builder
COPY --from=client-builder /app/AobaClient/target/dx/aoba-client/release/web/public /src/AobaServer/wwwroot COPY --from=client-builder /app/AobaClient/target/dx/aoba-client/release/web/public /src/AobaServer/wwwroot
WORKDIR "/src/AobaServer" WORKDIR "/src/AobaServer"
RUN dotnet build "./AobaServer.csproj" -c $BUILD_CONFIGURATION -o /app/build RUN dotnet build "./AobaServer.csproj" -c $BUILD_CONFIGURATION -o /app/build
# This stage is used to publish the service project to be copied to the final stage # This stage is used to publish the service project to be copied to the final stage
FROM build AS publish FROM build AS publish
ARG BUILD_CONFIGURATION=Release ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./AobaServer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false RUN dotnet publish "./AobaServer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM base AS final FROM base AS final
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
RUN sudo apt-get install -y ffmpeg libgdiplus RUN sudo apt-get install -y ffmpeg libgdiplus
ENTRYPOINT ["dotnet", "AobaServer.dll"] ENTRYPOINT ["dotnet", "AobaServer.dll"]

View File

@@ -1,73 +1,73 @@
#nullable enable #nullable enable
using AobaServer.Middleware; using AobaServer.Middleware;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry; using OpenTelemetry;
using OpenTelemetry.Metrics; using OpenTelemetry.Metrics;
using OpenTelemetry.Resources; using OpenTelemetry.Resources;
using OpenTelemetry.Trace; using OpenTelemetry.Trace;
namespace AobaServer.Middleware; namespace AobaServer.Middleware;
public static class OpenTelemetry public static class OpenTelemetry
{ {
public static void AddObersability(this IServiceCollection services, IConfiguration configuration) public static void AddObersability(this IServiceCollection services, IConfiguration configuration)
{ {
var otel = services.AddOpenTelemetry(); var otel = services.AddOpenTelemetry();
otel.ConfigureResource(res => otel.ConfigureResource(res =>
{ {
res.AddService(serviceName: $"Breeze: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}"); res.AddService(serviceName: $"Breeze: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}");
}); });
// Add Metrics for ASP.NET Core and our custom metrics and export to Prometheus // Add Metrics for ASP.NET Core and our custom metrics and export to Prometheus
otel.WithMetrics(metrics => metrics otel.WithMetrics(metrics => metrics
// Metrics provider from OpenTelemetry // Metrics provider from OpenTelemetry
.AddAspNetCoreInstrumentation() .AddAspNetCoreInstrumentation()
.AddCustomMetrics() .AddCustomMetrics()
// Metrics provides by ASP.NET Core in .NET 8 // Metrics provides by ASP.NET Core in .NET 8
.AddMeter("Microsoft.AspNetCore.Hosting") .AddMeter("Microsoft.AspNetCore.Hosting")
.AddMeter("Microsoft.AspNetCore.Server.Kestrel") .AddMeter("Microsoft.AspNetCore.Server.Kestrel")
// Metrics provided by System.Net libraries // Metrics provided by System.Net libraries
.AddMeter("System.Net.Http") .AddMeter("System.Net.Http")
.AddMeter("System.Net.NameResolution") .AddMeter("System.Net.NameResolution")
.AddPrometheusExporter()); .AddPrometheusExporter());
// Add Tracing for ASP.NET Core and our custom ActivitySource and export to Jaeger // Add Tracing for ASP.NET Core and our custom ActivitySource and export to Jaeger
var tracingOtlpEndpoint = configuration["OTLP_ENDPOINT_URL"]; var tracingOtlpEndpoint = configuration["OTLP_ENDPOINT_URL"];
otel.WithTracing(tracing => otel.WithTracing(tracing =>
{ {
tracing.AddSource("MongoDB.Driver.Core.Extensions.DiagnosticSources"); tracing.AddSource("MongoDB.Driver.Core.Extensions.DiagnosticSources");
tracing.AddAspNetCoreInstrumentation(); tracing.AddAspNetCoreInstrumentation();
tracing.AddHttpClientInstrumentation(); tracing.AddHttpClientInstrumentation();
if (!string.IsNullOrWhiteSpace(tracingOtlpEndpoint)) if (!string.IsNullOrWhiteSpace(tracingOtlpEndpoint))
{ {
tracing.AddOtlpExporter(otlpOptions => tracing.AddOtlpExporter(otlpOptions =>
{ {
otlpOptions.Endpoint = new Uri(tracingOtlpEndpoint); otlpOptions.Endpoint = new Uri(tracingOtlpEndpoint);
}); });
} }
}); });
} }
public static MeterProviderBuilder AddCustomMetrics(this MeterProviderBuilder builder) public static MeterProviderBuilder AddCustomMetrics(this MeterProviderBuilder builder)
{ {
return builder; return builder;
} }
public static IEndpointRouteBuilder MapObserability(this IEndpointRouteBuilder endpoints) public static IEndpointRouteBuilder MapObserability(this IEndpointRouteBuilder endpoints)
{ {
endpoints.MapPrometheusScrapingEndpoint().RequireAuthorization(); endpoints.MapPrometheusScrapingEndpoint().RequireAuthorization();
return endpoints; return endpoints;
} }
} }

View File

@@ -1,75 +1,75 @@
using MongoDB.Bson.IO; using MongoDB.Bson.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.Json; using System.Text.Json;
namespace AobaServer.Models; namespace AobaServer.Models;
public class AuthInfo public class AuthInfo
{ {
public required string Issuer { get; set; } public required string Issuer { get; set; }
public required string Audience { get; set; } public required string Audience { get; set; }
public required byte[] SecureKey { get; set; } public required byte[] SecureKey { get; set; }
/// <summary> /// <summary>
/// Save this auth into in a json format to the sepcified file /// Save this auth into in a json format to the sepcified file
/// </summary> /// </summary>
/// <param name="path">File path</param> /// <param name="path">File path</param>
/// <returns></returns> /// <returns></returns>
public AuthInfo Save(string path) public AuthInfo Save(string path)
{ {
File.WriteAllText(path, JsonSerializer.Serialize(this)); File.WriteAllText(path, JsonSerializer.Serialize(this));
return this; return this;
} }
/// <summary> /// <summary>
/// Generate a new Auth Info with newly generated keys /// Generate a new Auth Info with newly generated keys
/// </summary> /// </summary>
/// <param name="issuer"></param> /// <param name="issuer"></param>
/// <param name="audience"></param> /// <param name="audience"></param>
/// <returns></returns> /// <returns></returns>
public static AuthInfo Create(string issuer, string audience) public static AuthInfo Create(string issuer, string audience)
{ {
var auth = new AuthInfo var auth = new AuthInfo
{ {
Issuer = issuer, Issuer = issuer,
Audience = audience, Audience = audience,
SecureKey = GenetateJWTKey() SecureKey = GenetateJWTKey()
}; };
return auth; return auth;
} }
/// <summary> /// <summary>
/// Load auth info from a json file /// Load auth info from a json file
/// </summary> /// </summary>
/// <param name="path">File path</param> /// <param name="path">File path</param>
/// <returns></returns> /// <returns></returns>
internal static AuthInfo? Load(string path) internal static AuthInfo? Load(string path)
{ {
return JsonSerializer.Deserialize<AuthInfo>(File.ReadAllText(path)); return JsonSerializer.Deserialize<AuthInfo>(File.ReadAllText(path));
} }
internal static AuthInfo LoadOrCreate(string path, string issuer, string audience) internal static AuthInfo LoadOrCreate(string path, string issuer, string audience)
{ {
if (File.Exists(path)) if (File.Exists(path))
{ {
var loaded = Load(path); var loaded = Load(path);
if (loaded != null) if (loaded != null)
return loaded; return loaded;
} }
var info = Create(issuer, audience); var info = Create(issuer, audience);
info.Save(path); info.Save(path);
return info; return info;
} }
/// <summary> /// <summary>
/// Generate a new key for use by JWT /// Generate a new key for use by JWT
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static byte[] GenetateJWTKey(int size = 64) public static byte[] GenetateJWTKey(int size = 64)
{ {
var key = new byte[size]; var key = new byte[size];
RandomNumberGenerator.Fill(key); RandomNumberGenerator.Fill(key);
return key; return key;
} }
} }

View File

@@ -1,31 +1,31 @@
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using MongoDB.Bson; using MongoDB.Bson;
namespace AobaServer.Models; namespace AobaServer.Models;
public class BsonIdModelBinderProvider : IModelBinderProvider public class BsonIdModelBinderProvider : IModelBinderProvider
{ {
public IModelBinder? GetBinder(ModelBinderProviderContext context) public IModelBinder? GetBinder(ModelBinderProviderContext context)
{ {
if (context.Metadata.ModelType == typeof(ObjectId)) if (context.Metadata.ModelType == typeof(ObjectId))
return new BsonIdModelBinder(); return new BsonIdModelBinder();
return default; return default;
} }
} }
public class BsonIdModelBinder : IModelBinder public class BsonIdModelBinder : IModelBinder
{ {
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)
{ {
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == ValueProviderResult.None) if (value == ValueProviderResult.None)
return Task.CompletedTask; return Task.CompletedTask;
if (ObjectId.TryParse(value.FirstValue, out var id)) if (ObjectId.TryParse(value.FirstValue, out var id))
bindingContext.Result = ModelBindingResult.Success(id); bindingContext.Result = ModelBindingResult.Success(id);
else else
bindingContext.Result = ModelBindingResult.Failed(); bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@@ -1,18 +1,18 @@
namespace AobaServer.Models; namespace AobaServer.Models;
public class ShareXDestination public class ShareXDestination
{ {
public string Version { get; set; } = "14.0.1"; public string Version { get; set; } = "14.0.1";
public string Name { get; set; } = "Aoba"; public string Name { get; set; } = "Aoba";
public string DestinationType { get; set; } = "ImageUploader, TextUploader, FileUploader"; public string DestinationType { get; set; } = "ImageUploader, TextUploader, FileUploader";
public string RequestMethod { get; set; } = "POST"; public string RequestMethod { get; set; } = "POST";
public string RequestURL { get; set; } = "https://aoba.app/api/media/upload"; public string RequestURL { get; set; } = "https://aoba.app/api/media/upload";
public Dictionary<string, string> Headers { get; set; } = []; public Dictionary<string, string> Headers { get; set; } = [];
public string Body { get; set; } = "MultipartFormData"; public string Body { get; set; } = "MultipartFormData";
public Dictionary<string, string> Arguments { get; set; } = new() { { "name", "$filename$" } }; public Dictionary<string, string> Arguments { get; set; } = new() { { "name", "$filename$" } };
public string FileFormName { get; set; } = "file"; public string FileFormName { get; set; } = "file";
public string[] RegexList { get; set; } = ["([^/]+)/?$"]; public string[] RegexList { get; set; } = ["([^/]+)/?$"];
public string URL { get; set; } = "https://aoba.app{json:url}"; public string URL { get; set; } = "https://aoba.app{json:url}";
public string? ThumbnailURL { get; set; } public string? ThumbnailURL { get; set; }
public string? DeletionURL { get; set; } public string? DeletionURL { get; set; }
} }

View File

@@ -1,144 +1,144 @@
using AobaCore; using AobaCore;
using AobaServer.Auth; using AobaServer.Auth;
using AobaServer.Middleware; using AobaServer.Middleware;
using AobaServer.Models; using AobaServer.Models;
using AobaServer.Services; using AobaServer.Services;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(o => builder.WebHost.ConfigureKestrel(o =>
{ {
o.Limits.MaxRequestBodySize = null; o.Limits.MaxRequestBodySize = null;
#if !DEBUG #if !DEBUG
o.ListenAnyIP(8081, lo => o.ListenAnyIP(8081, lo =>
{ {
lo.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2; lo.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2;
}); });
o.ListenAnyIP(8080, lo => o.ListenAnyIP(8080, lo =>
{ {
lo.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2; lo.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
}); });
#endif #endif
}); });
var config = builder.Configuration; var config = builder.Configuration;
// Add services to the container. // Add services to the container.
builder.Services.AddControllers(opt => opt.ModelBinderProviders.Add(new BsonIdModelBinderProvider())); builder.Services.AddControllers(opt => opt.ModelBinderProviders.Add(new BsonIdModelBinderProvider()));
builder.Services.AddObersability(builder.Configuration); builder.Services.AddObersability(builder.Configuration);
builder.Services.AddGrpc(); builder.Services.AddGrpc();
var authInfo = AuthInfo.LoadOrCreate("Auth.json", "aobaV2", "aoba"); var authInfo = AuthInfo.LoadOrCreate("Auth.json", "aobaV2", "aoba");
builder.Services.AddSingleton(authInfo); builder.Services.AddSingleton(authInfo);
var signingKey = new SymmetricSecurityKey(authInfo.SecureKey); var signingKey = new SymmetricSecurityKey(authInfo.SecureKey);
var validationParams = new TokenValidationParameters var validationParams = new TokenValidationParameters
{ {
ValidateIssuerSigningKey = true, ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey, IssuerSigningKey = signingKey,
ValidateIssuer = true, ValidateIssuer = true,
ValidIssuer = authInfo.Issuer, ValidIssuer = authInfo.Issuer,
ValidateAudience = true, ValidateAudience = true,
ValidAudience = authInfo.Audience, ValidAudience = authInfo.Audience,
ValidateLifetime = false, ValidateLifetime = false,
ClockSkew = TimeSpan.FromMinutes(1), ClockSkew = TimeSpan.FromMinutes(1),
}; };
builder.Services.AddCors(o => builder.Services.AddCors(o =>
{ {
o.AddPolicy("AllowAll", p => o.AddPolicy("AllowAll", p =>
{ {
p.AllowAnyOrigin(); p.AllowAnyOrigin();
p.AllowAnyMethod(); p.AllowAnyMethod();
p.AllowAnyHeader(); p.AllowAnyHeader();
}); });
o.AddPolicy("RPC", p => o.AddPolicy("RPC", p =>
{ {
p.AllowAnyMethod(); p.AllowAnyMethod();
p.AllowAnyHeader(); p.AllowAnyHeader();
p.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding"); p.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
p.AllowAnyOrigin(); p.AllowAnyOrigin();
}); });
}); });
builder.Services.AddAuthentication(options => builder.Services.AddAuthentication(options =>
{ {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "Aoba"; options.DefaultChallengeScheme = "Aoba";
}).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => //Bearer auth }).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => //Bearer auth
{ {
options.TokenValidationParameters = validationParams; options.TokenValidationParameters = validationParams;
options.TokenHandlers.Add(new MetricsTokenValidator(authInfo)); options.TokenHandlers.Add(new MetricsTokenValidator(authInfo));
options.Events = new JwtBearerEvents options.Events = new JwtBearerEvents
{ {
OnMessageReceived = ctx => //Retreive token from cookie if not found in headers OnMessageReceived = ctx => //Retreive token from cookie if not found in headers
{ {
if (string.IsNullOrWhiteSpace(ctx.Token)) if (string.IsNullOrWhiteSpace(ctx.Token))
ctx.Token = ctx.Request.Headers.Authorization.FirstOrDefault()?.Replace("Bearer ", ""); ctx.Token = ctx.Request.Headers.Authorization.FirstOrDefault()?.Replace("Bearer ", "");
#if DEBUG //allow cookie based auth when in debug mode #if DEBUG //allow cookie based auth when in debug mode
if (string.IsNullOrWhiteSpace(ctx.Token)) if (string.IsNullOrWhiteSpace(ctx.Token))
ctx.Token = ctx.Request.Cookies.FirstOrDefault(c => c.Key == "token").Value; ctx.Token = ctx.Request.Cookies.FirstOrDefault(c => c.Key == "token").Value;
#endif #endif
return Task.CompletedTask; return Task.CompletedTask;
}, },
OnAuthenticationFailed = ctx => OnAuthenticationFailed = ctx =>
{ {
ctx.Response.Cookies.Append("token", "", new CookieOptions ctx.Response.Cookies.Append("token", "", new CookieOptions
{ {
MaxAge = TimeSpan.Zero, MaxAge = TimeSpan.Zero,
Expires = DateTime.Now Expires = DateTime.Now
}); });
ctx.Options.ForwardChallenge = "Aoba"; ctx.Options.ForwardChallenge = "Aoba";
return Task.CompletedTask; return Task.CompletedTask;
} }
}; };
}).AddScheme<AuthenticationSchemeOptions, AobaAuthenticationHandler>("Aoba", null); }).AddScheme<AuthenticationSchemeOptions, AobaAuthenticationHandler>("Aoba", null);
var dbString = config["DB_STRING"]; var dbString = config["DB_STRING"];
builder.Services.AddAoba(dbString ?? "mongodb://localhost:27017"); builder.Services.AddAoba(dbString ?? "mongodb://localhost:27017");
builder.Services.Configure<FormOptions>(opt => builder.Services.Configure<FormOptions>(opt =>
{ {
opt.ValueLengthLimit = int.MaxValue; opt.ValueLengthLimit = int.MaxValue;
opt.MultipartBodyLengthLimit = int.MaxValue; opt.MultipartBodyLengthLimit = int.MaxValue;
}); });
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/Home/Error"); app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts(); app.UseHsts();
} }
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.UseStaticFiles(); app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseCors(); app.UseCors();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.MapObserability(); app.MapObserability();
app.MapGrpcService<AobaRpcService>() app.MapGrpcService<AobaRpcService>()
.RequireAuthorization() .RequireAuthorization()
.RequireCors("RPC"); .RequireCors("RPC");
app.MapGrpcService<AobaAuthService>() app.MapGrpcService<AobaAuthService>()
.AllowAnonymous() .AllowAnonymous()
.RequireCors("RPC"); .RequireCors("RPC");
app.MapFallbackToFile("index.html"); app.MapFallbackToFile("index.html");
app.Run(); app.Run();

View File

@@ -1,30 +1,30 @@
{ {
"profiles": { "profiles": {
"http": { "http": {
"commandName": "Project", "commandName": "Project",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
"applicationUrl": "http://localhost:8081" "applicationUrl": "http://localhost:8081"
}, },
"https": { "https": {
"commandName": "Project", "commandName": "Project",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
"applicationUrl": "http://localhost:8081" "applicationUrl": "http://localhost:8081"
}, },
"Container (Dockerfile)": { "Container (Dockerfile)": {
"commandName": "Docker", "commandName": "Docker",
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "8081" "ASPNETCORE_HTTP_PORTS": "8081"
}, },
"publishAllPorts": true, "publishAllPorts": true,
"useSSL": false "useSSL": false
} }
}, },
"$schema": "https://json.schemastore.org/launchsettings.json" "$schema": "https://json.schemastore.org/launchsettings.json"
} }

View File

@@ -1,85 +1,85 @@
syntax = "proto3"; syntax = "proto3";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
option csharp_namespace = "Aoba.RPC"; option csharp_namespace = "Aoba.RPC";
package aoba; package aoba;
service AobaRpc { service AobaRpc {
rpc GetMedia (Id) returns (MediaResponse); rpc GetMedia (Id) returns (MediaResponse);
rpc DeleteMedia (Id) returns (google.protobuf.Empty); rpc DeleteMedia (Id) returns (google.protobuf.Empty);
rpc UpdateMedia (google.protobuf.Empty) returns (google.protobuf.Empty); rpc UpdateMedia (google.protobuf.Empty) returns (google.protobuf.Empty);
rpc ListMedia(PageFilter) returns (ListResponse); rpc ListMedia(PageFilter) returns (ListResponse);
rpc GetUser(Id) returns (UserResponse); rpc GetUser(Id) returns (UserResponse);
rpc GetShareXDestination(google.protobuf.Empty) returns (ShareXResponse); rpc GetShareXDestination(google.protobuf.Empty) returns (ShareXResponse);
} }
message PageFilter { message PageFilter {
optional int32 page = 1; optional int32 page = 1;
optional int32 pageSize = 2; optional int32 pageSize = 2;
optional string query = 3; optional string query = 3;
} }
message Id { message Id {
string value = 1; string value = 1;
} }
message MediaResponse { message MediaResponse {
oneof result { oneof result {
MediaModel value = 1; MediaModel value = 1;
google.protobuf.Empty empty = 2; google.protobuf.Empty empty = 2;
} }
} }
message ListResponse { message ListResponse {
repeated MediaModel items = 1; repeated MediaModel items = 1;
Pagination pagination = 2; Pagination pagination = 2;
} }
message Pagination { message Pagination {
int32 page = 1; int32 page = 1;
int32 pageSize = 2; int32 pageSize = 2;
int64 totalPages = 3; int64 totalPages = 3;
int64 totalItems = 4; int64 totalItems = 4;
optional string query = 5; optional string query = 5;
} }
message UserResponse { message UserResponse {
oneof userResult { oneof userResult {
UserModel user = 1; UserModel user = 1;
google.protobuf.Empty empty = 2; google.protobuf.Empty empty = 2;
} }
} }
message UserModel { message UserModel {
Id id = 1; Id id = 1;
string username = 2; string username = 2;
string email = 3; string email = 3;
bool isAdmin = 4; bool isAdmin = 4;
} }
message MediaModel { message MediaModel {
Id id = 1; Id id = 1;
Id mediaId = 2; Id mediaId = 2;
string fileName = 3; string fileName = 3;
MediaType mediaType = 4; MediaType mediaType = 4;
string ext = 5; string ext = 5;
int32 viewCount = 6; int32 viewCount = 6;
Id owner = 7; Id owner = 7;
} }
enum MediaType { enum MediaType {
Image = 0; Image = 0;
Audio = 1; Audio = 1;
Video = 2; Video = 2;
Text = 3; Text = 3;
Code = 4; Code = 4;
Raw = 5; Raw = 5;
} }
message ShareXResponse { message ShareXResponse {
oneof dstResult { oneof dstResult {
string destination = 1; string destination = 1;
string error = 2; string error = 2;
} }
} }

View File

@@ -1,33 +1,33 @@
syntax = "proto3"; syntax = "proto3";
option csharp_namespace = "Aoba.RPC.Auth"; option csharp_namespace = "Aoba.RPC.Auth";
package aoba.Auth; package aoba.Auth;
service AuthRpc { service AuthRpc {
rpc Login(Credentials) returns (LoginResponse); rpc Login(Credentials) returns (LoginResponse);
rpc LoginPasskey(PassKeyPayload) returns (LoginResponse); rpc LoginPasskey(PassKeyPayload) returns (LoginResponse);
} }
message Credentials{ message Credentials{
string user = 1; string user = 1;
string password = 2; string password = 2;
} }
message PassKeyPayload { message PassKeyPayload {
} }
message Jwt{ message Jwt{
string token = 1; string token = 1;
} }
message LoginResponse{ message LoginResponse{
oneof result { oneof result {
Jwt jwt = 1; Jwt jwt = 1;
LoginError error = 2; LoginError error = 2;
} }
} }
message LoginError{ message LoginError{
string message = 1; string message = 1;
} }

View File

@@ -1,43 +1,43 @@
using Aoba.RPC.Auth; using Aoba.RPC.Auth;
using AobaCore.Models; using AobaCore.Models;
using AobaCore.Services; using AobaCore.Services;
using AobaServer.Models; using AobaServer.Models;
using AobaServer.Utils; using AobaServer.Utils;
using Grpc.Core; using Grpc.Core;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
namespace AobaServer.Services; namespace AobaServer.Services;
public class AobaAuthService(AccountsService accountsService, AuthInfo authInfo) : Aoba.RPC.Auth.AuthRpc.AuthRpcBase public class AobaAuthService(AccountsService accountsService, AuthInfo authInfo) : Aoba.RPC.Auth.AuthRpc.AuthRpcBase
{ {
[AllowAnonymous] [AllowAnonymous]
public override async Task<LoginResponse> Login(Credentials request, ServerCallContext context) public override async Task<LoginResponse> Login(Credentials request, ServerCallContext context)
{ {
var user = await accountsService.VerifyLoginAsync(request.User, request.Password, context.CancellationToken); var user = await accountsService.VerifyLoginAsync(request.User, request.Password, context.CancellationToken);
if (user == null) if (user == null)
return new LoginResponse return new LoginResponse
{ {
Error = new LoginError Error = new LoginError
{ {
Message = "Invalid login credentials" Message = "Invalid login credentials"
} }
}; };
var token = user.GetToken(authInfo); var token = user.GetToken(authInfo);
return new LoginResponse return new LoginResponse
{ {
Jwt = new Jwt Jwt = new Jwt
{ {
Token = token Token = token
} }
}; };
} }
} }

View File

@@ -1,59 +1,59 @@
using Aoba.RPC; using Aoba.RPC;
using AobaCore.Services; using AobaCore.Services;
using AobaServer.Models; using AobaServer.Models;
using AobaServer.Utils; using AobaServer.Utils;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using Grpc.Core; using Grpc.Core;
using MongoDB.Bson.IO; using MongoDB.Bson.IO;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace AobaServer.Services; namespace AobaServer.Services;
public class AobaRpcService(AobaService aobaService, AccountsService accountsService, AuthInfo authInfo) : AobaRpc.AobaRpcBase public class AobaRpcService(AobaService aobaService, AccountsService accountsService, AuthInfo authInfo) : AobaRpc.AobaRpcBase
{ {
public override async Task<MediaResponse> GetMedia(Id request, ServerCallContext context) public override async Task<MediaResponse> GetMedia(Id request, ServerCallContext context)
{ {
var media = await aobaService.GetMediaAsync(request.ToObjectId()); var media = await aobaService.GetMediaAsync(request.ToObjectId());
return media.ToResponse(); return media.ToResponse();
} }
public override async Task<ListResponse> ListMedia(PageFilter request, ServerCallContext context) public override async Task<ListResponse> ListMedia(PageFilter request, ServerCallContext context)
{ {
var user = context.GetUserId(); var user = context.GetUserId();
var result = await aobaService.FindMediaAsync(request.Query, user, request.HasPage ? request.Page : 1, request.HasPageSize ? request.PageSize : 100); var result = await aobaService.FindMediaAsync(request.Query, user, request.HasPage ? request.Page : 1, request.HasPageSize ? request.PageSize : 100);
return result.ToResponse(); return result.ToResponse();
} }
public override async Task<ShareXResponse> GetShareXDestination(Empty request, ServerCallContext context) public override async Task<ShareXResponse> GetShareXDestination(Empty request, ServerCallContext context)
{ {
var userId = context.GetHttpContext().User.GetId(); var userId = context.GetHttpContext().User.GetId();
var user = await accountsService.GetUserAsync(userId, context.CancellationToken); var user = await accountsService.GetUserAsync(userId, context.CancellationToken);
if (user == null) if (user == null)
return new ShareXResponse { Error = "User does not exist" }; return new ShareXResponse { Error = "User does not exist" };
var token = user.GetToken(authInfo); var token = user.GetToken(authInfo);
var dest = new ShareXDestination var dest = new ShareXDestination
{ {
DeletionURL = string.Empty, DeletionURL = string.Empty,
ThumbnailURL = string.Empty, ThumbnailURL = string.Empty,
Headers = new() Headers = new()
{ {
{ "Authorization", $"Bearer {token}" } { "Authorization", $"Bearer {token}" }
} }
}; };
return new ShareXResponse return new ShareXResponse
{ {
Destination = JsonSerializer.Serialize(dest, new JsonSerializerOptions Destination = JsonSerializer.Serialize(dest, new JsonSerializerOptions
{ {
WriteIndented = true WriteIndented = true
}) })
}; };
} }
} }

View File

@@ -1,47 +1,47 @@
using AobaCore.Models; using AobaCore.Models;
using AobaServer.Models; using AobaServer.Models;
using Grpc.Core; using Grpc.Core;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
namespace AobaServer.Utils; namespace AobaServer.Utils;
public static class Extensions public static class Extensions
{ {
public static ObjectId ToObjectId(this string? value) public static ObjectId ToObjectId(this string? value)
{ {
if(value == null) if(value == null)
return ObjectId.Empty; return ObjectId.Empty;
if(ObjectId.TryParse(value, out ObjectId result)) if(ObjectId.TryParse(value, out ObjectId result))
return result; return result;
return ObjectId.Empty; return ObjectId.Empty;
} }
public static string GetToken(this User user, AuthInfo authInfo) public static string GetToken(this User user, AuthInfo authInfo)
{ {
var handler = new JwtSecurityTokenHandler(); var handler = new JwtSecurityTokenHandler();
var signCreds = new SigningCredentials(new SymmetricSecurityKey(authInfo.SecureKey), SecurityAlgorithms.HmacSha256); var signCreds = new SigningCredentials(new SymmetricSecurityKey(authInfo.SecureKey), SecurityAlgorithms.HmacSha256);
var identity = user.GetIdentity(); var identity = user.GetIdentity();
var token = handler.CreateEncodedJwt(authInfo.Issuer, authInfo.Audience, identity, notBefore: DateTime.Now, expires: null, issuedAt: DateTime.Now, signCreds); var token = handler.CreateEncodedJwt(authInfo.Issuer, authInfo.Audience, identity, notBefore: DateTime.Now, expires: null, issuedAt: DateTime.Now, signCreds);
return token; return token;
} }
public static ObjectId GetId(this ClaimsPrincipal user) public static ObjectId GetId(this ClaimsPrincipal user)
{ {
return user.FindFirstValue(ClaimTypes.NameIdentifier).ToObjectId(); return user.FindFirstValue(ClaimTypes.NameIdentifier).ToObjectId();
} }
public static ObjectId GetUserId(this ServerCallContext context) public static ObjectId GetUserId(this ServerCallContext context)
{ {
return context.GetHttpContext().User.GetId(); return context.GetHttpContext().User.GetId();
} }
} }

View File

@@ -1,68 +1,68 @@
using AobaCore.Models; using AobaCore.Models;
using Aoba.RPC; using Aoba.RPC;
using MongoDB.Bson; using MongoDB.Bson;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
namespace AobaServer.Utils; namespace AobaServer.Utils;
public static class ProtoExtensions public static class ProtoExtensions
{ {
public static ListResponse ToResponse(this PagedResult<Media> result) public static ListResponse ToResponse(this PagedResult<Media> result)
{ {
var res = new ListResponse() var res = new ListResponse()
{ {
Pagination = result.ToPagination(), Pagination = result.ToPagination(),
}; };
res.Items.AddRange(result.Items.Select(i => i.ToMediaModel())); res.Items.AddRange(result.Items.Select(i => i.ToMediaModel()));
return res; return res;
} }
public static Pagination ToPagination<T>(this PagedResult<T> result) public static Pagination ToPagination<T>(this PagedResult<T> result)
{ {
var p =new Pagination() var p =new Pagination()
{ {
Page = result.Page, Page = result.Page,
PageSize = result.PageSize, PageSize = result.PageSize,
TotalItems = result.TotalItems, TotalItems = result.TotalItems,
TotalPages = result.TotalPages, TotalPages = result.TotalPages,
}; };
if(result.Query != null) if(result.Query != null)
p.Query = result.Query; p.Query = result.Query;
return p; return p;
} }
public static MediaResponse ToResponse(this Media? media) public static MediaResponse ToResponse(this Media? media)
{ {
if(media == null) if(media == null)
return new MediaResponse() { Empty = new Empty() }; return new MediaResponse() { Empty = new Empty() };
return new MediaResponse() return new MediaResponse()
{ {
Value = media.ToMediaModel() Value = media.ToMediaModel()
}; };
} }
public static MediaModel ToMediaModel(this Media media) public static MediaModel ToMediaModel(this Media media)
{ {
return new MediaModel() return new MediaModel()
{ {
Ext = media.Ext, Ext = media.Ext,
FileName = media.Filename, FileName = media.Filename,
Id = media.Id.ToId(), Id = media.Id.ToId(),
MediaId = media.MediaId.ToId(), MediaId = media.MediaId.ToId(),
MediaType = (Aoba.RPC.MediaType)media.MediaType, MediaType = (Aoba.RPC.MediaType)media.MediaType,
Owner = media.Owner.ToId(), Owner = media.Owner.ToId(),
ViewCount = media.ViewCount, ViewCount = media.ViewCount,
}; };
} }
public static Id ToId(this ObjectId id) public static Id ToId(this ObjectId id)
{ {
return new Id() { Value = id.ToString() }; return new Id() { Value = id.ToString() };
} }
public static ObjectId ToObjectId(this Id id) public static ObjectId ToObjectId(this Id id)
{ {
return id.Value.ToObjectId(); return id.Value.ToObjectId();
} }
} }

View File

@@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"DB_STRING": "mongodb://NinoIna:27017" "DB_STRING": "mongodb://NinoIna:27017"
} }

View File

@@ -1,11 +1,11 @@
{ {
"Kestrel": { "Kestrel": {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*"
} }
} }

View File

@@ -1,31 +1,31 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.13.35919.96 VisualStudioVersion = 17.13.35919.96
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AobaServer", "AobaServer\AobaServer.csproj", "{A97400AB-4B57-4074-9A31-8D46A305E633}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AobaServer", "AobaServer\AobaServer.csproj", "{A97400AB-4B57-4074-9A31-8D46A305E633}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AobaCore", "AobaCore\AobaCore.csproj", "{65EEC037-E845-471D-A838-BEEADF781C17}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AobaCore", "AobaCore\AobaCore.csproj", "{65EEC037-E845-471D-A838-BEEADF781C17}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A97400AB-4B57-4074-9A31-8D46A305E633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A97400AB-4B57-4074-9A31-8D46A305E633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A97400AB-4B57-4074-9A31-8D46A305E633}.Debug|Any CPU.Build.0 = Debug|Any CPU {A97400AB-4B57-4074-9A31-8D46A305E633}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A97400AB-4B57-4074-9A31-8D46A305E633}.Release|Any CPU.ActiveCfg = Release|Any CPU {A97400AB-4B57-4074-9A31-8D46A305E633}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A97400AB-4B57-4074-9A31-8D46A305E633}.Release|Any CPU.Build.0 = Release|Any CPU {A97400AB-4B57-4074-9A31-8D46A305E633}.Release|Any CPU.Build.0 = Release|Any CPU
{65EEC037-E845-471D-A838-BEEADF781C17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {65EEC037-E845-471D-A838-BEEADF781C17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{65EEC037-E845-471D-A838-BEEADF781C17}.Debug|Any CPU.Build.0 = Debug|Any CPU {65EEC037-E845-471D-A838-BEEADF781C17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{65EEC037-E845-471D-A838-BEEADF781C17}.Release|Any CPU.ActiveCfg = Release|Any CPU {65EEC037-E845-471D-A838-BEEADF781C17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{65EEC037-E845-471D-A838-BEEADF781C17}.Release|Any CPU.Build.0 = Release|Any CPU {65EEC037-E845-471D-A838-BEEADF781C17}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B51CD9E1-22BE-4CDB-82A8-4C6027687C60} SolutionGuid = {B51CD9E1-22BE-4CDB-82A8-4C6027687C60}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -1,21 +1,21 @@
MIT License MIT License
Copyright (c) [year] [fullname] Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

View File

@@ -1,9 +1,9 @@
services: services:
aoba: aoba:
build: build:
context: . context: .
dockerfile: AobaServer/Dockerfile dockerfile: AobaServer/Dockerfile
ports: ports:
- "4321:8080" - "4321:8080"
environment: environment:
- DB_STRING="mongodb://192.168.86.63:27017" - DB_STRING="mongodb://192.168.86.63:27017"