JJ Colocate
This commit is contained in:
@@ -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
126
.gitattributes
vendored
@@ -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
724
.gitignore
vendored
@@ -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
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
[build]
|
[build]
|
||||||
target = "wasm32-unknown-unknown"
|
target = "wasm32-unknown-unknown"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
**/target
|
**/target
|
||||||
**/dist
|
**/dist
|
||||||
LICENSES
|
LICENSES
|
||||||
LICENSE
|
LICENSE
|
||||||
temp
|
temp
|
||||||
README.md
|
README.md
|
||||||
14
AobaClient/.gitignore
vendored
14
AobaClient/.gitignore
vendored
@@ -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
4916
AobaClient/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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 |
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
mod button;
|
mod button;
|
||||||
mod input;
|
mod input;
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
pub use input::*;
|
pub use input::*;
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
mod auth_context;
|
mod auth_context;
|
||||||
pub use auth_context::*;
|
pub use auth_context::*;
|
||||||
|
|||||||
@@ -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> {} }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
mod main_layout;
|
mod main_layout;
|
||||||
pub use main_layout::*;
|
pub use main_layout::*;
|
||||||
|
|||||||
@@ -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> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
pub mod media;
|
pub mod media;
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
wwwroot
|
wwwroot
|
||||||
|
|||||||
2
AobaServer/.gitignore
vendored
2
AobaServer/.gitignore
vendored
@@ -1 +1 @@
|
|||||||
wwwroot/*
|
wwwroot/*
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
62
AobaV2.sln
62
AobaV2.sln
@@ -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
|
||||||
|
|||||||
42
LICENSE.txt
42
LICENSE.txt
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user