6 Commits

Author SHA1 Message Date
Lars Jellema
8df5fff105 Use boolean ok field for api 2022-04-21 23:02:14 +02:00
Lars Jellema
ced7d83fd9 Gitignore more sqlite files 2022-04-21 23:01:46 +02:00
Lars Jellema
73c9ad61a7 Make test scripts verbose about HTTP codes and headers 2022-04-21 23:01:35 +02:00
Lars Jellema
52647759cc Fix test login script to work with test register script 2022-04-21 22:57:01 +02:00
Lars Jellema
9d1e3539a8 Rework api errors 2022-04-21 22:51:08 +02:00
Lars Jellema
5fca980af9 Replace ApiResponseVariant with Result 2022-04-21 22:51:06 +02:00
261 changed files with 30832 additions and 16002 deletions

4
.gitignore vendored
View File

@@ -1,5 +1 @@
/target/
**/*.rs.bk
Cargo.lock
.vscode .vscode
**/.idea

View File

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

View File

@@ -1,27 +0,0 @@
<Project>
<!-- https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management -->
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Avalonia packages -->
<!-- Important: keep version in sync! -->
<PackageVersion Include="Avalonia" Version="11.3.10" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.8" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.10" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.10" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.10" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.10" />
<PackageVersion Include="Avalonia.iOS" Version="11.3.10" />
<PackageVersion Include="Avalonia.Browser" Version="11.3.10" />
<PackageVersion Include="Avalonia.Android" Version="11.3.10" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="2.6.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageVersion>
<PackageVersion Include="Refit" Version="9.0.2" />
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.15" />
</ItemGroup>
</Project>

View File

@@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<ApplicationId>com.CompanyName.Gamenight.Ui</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
</PropertyGroup>
<ItemGroup>
<AndroidResource Include="Icon.png">
<Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Android" />
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gamenight.Ui\Gamenight.Ui.csproj" />
</ItemGroup>
</Project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,21 +0,0 @@
using Android.App;
using Android.Content.PM;
using Avalonia;
using Avalonia.Android;
namespace Gamenight.Ui.Android;
[Activity(
Label = "Gamenight.Ui.Android",
Theme = "@style/MyTheme.NoActionBar",
Icon = "@drawable/icon",
MainLauncher = true,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
.WithInterFont();
}
}

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
<application android:label="Gamenight.Ui" android:icon="@drawable/Icon" />
</manifest>

View File

@@ -1,44 +0,0 @@
Images, layout descriptions, binary blobs and string dictionaries can be included
in your application as resource files. Various Android APIs are designed to
operate on the resource IDs instead of dealing with images, strings or binary blobs
directly.
For example, a sample Android app that contains a user interface layout (main.axml),
an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
would keep its resources in the "Resources" directory of the application:
Resources/
drawable/
icon.png
layout/
main.axml
values/
strings.xml
In order to get the build system to recognize Android resources, set the build action to
"AndroidResource". The native Android APIs do not operate directly with filenames, but
instead operate on resource IDs. When you compile an Android application that uses resources,
the build system will package the resources for distribution and generate a class called "R"
(this is an Android convention) that contains the tokens for each one of the resources
included. For example, for the above Resources layout, this is what the R class would expose:
public class R {
public class drawable {
public const int icon = 0x123;
}
public class layout {
public const int main = 0x456;
}
public class strings {
public const int first_string = 0xabc;
public const int second_string = 0xbcd;
}
}
You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main
to reference the layout/main.axml file, or R.strings.first_string to reference the first
string in the dictionary file values/strings.xml.

View File

@@ -1,66 +0,0 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -1,71 +0,0 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="500"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/splash_background"/>
</item>
<item android:drawable="@drawable/icon"
android:width="120dp"
android:height="120dp"
android:gravity="center" />
</layer-list>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#212121</color>
</resources>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowSplashScreenBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avalonia_anim</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
<item name="postSplashScreenTheme">@style/MyTheme.Main</item>
</style>
<style name="MyTheme.Main"
parent ="MyTheme.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
</style>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#FFFFFF</color>
</resources>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>

View File

@@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
<PropertyGroup>
<TargetFramework>net9.0-browser</TargetFramework>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Browser" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gamenight.Ui\Gamenight.Ui.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,15 +0,0 @@
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Browser;
using Gamenight.Ui;
internal sealed partial class Program
{
private static Task Main(string[] args) => BuildAvaloniaApp()
.WithInterFont()
.StartBrowserAppAsync("out");
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>();
}

View File

@@ -1 +0,0 @@
[assembly:System.Runtime.Versioning.SupportedOSPlatform("browser")]

View File

@@ -1,13 +0,0 @@
{
"profiles": {
"Gamenight.Ui.Browser": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:7169;http://localhost:5235",
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
}
}
}

View File

@@ -1,10 +0,0 @@
{
"wasmHostProperties": {
"perHostConfig": [
{
"name": "browser",
"host": "browser"
}
]
}
}

View File

@@ -1,58 +0,0 @@
/* HTML styles for the splash screen */
.avalonia-splash {
position: absolute;
height: 100%;
width: 100%;
background: white;
font-family: 'Outfit', sans-serif;
justify-content: center;
align-items: center;
display: flex;
pointer-events: none;
}
/* Light theme styles */
@media (prefers-color-scheme: light) {
.avalonia-splash {
background: white;
}
.avalonia-splash h2 {
color: #1b2a4e;
}
.avalonia-splash a {
color: #0D6EFD;
}
}
@media (prefers-color-scheme: dark) {
.avalonia-splash {
background: #1b2a4e;
}
.avalonia-splash h2 {
color: white;
}
.avalonia-splash a {
color: white;
}
}
.avalonia-splash h2 {
font-weight: 400;
font-size: 1.5rem;
}
.avalonia-splash a {
text-decoration: none;
font-size: 2.5rem;
display: block;
}
.avalonia-splash.splash-close {
transition: opacity 200ms, display 200ms;
display: none;
opacity: 0;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -1,36 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Gamenight.Ui.Browser</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./app.css" />
</head>
<body style="margin: 0; overflow: hidden">
<div id="out">
<div class="avalonia-splash">
<h2>
Powered by
<a href="https://www.avaloniaui.net/" target="_blank">
<svg width="266" height="52" viewBox="0 0 266 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M55.8592 47.3941C54.9035 47.3941 54.1184 47.1723 53.504 46.7285C52.9237 46.2848 52.5483 45.6875 52.3776 44.9365C52.2411 44.1856 52.3947 43.3493 52.8384 42.4277L65.9456 13.7045C66.4917 12.544 67.1403 11.7077 67.8912 11.1957C68.6421 10.6496 69.5125 10.3765 70.5024 10.3765C71.4923 10.3765 72.3627 10.6496 73.1136 11.1957C73.8645 11.7077 74.496 12.544 75.008 13.7045L88.2176 42.4277C88.6613 43.3493 88.8149 44.2027 88.6784 44.9877C88.576 45.7387 88.2347 46.336 87.6544 46.7797C87.0741 47.1893 86.3232 47.3941 85.4016 47.3941C84.2411 47.3941 83.3365 47.1211 82.688 46.5749C82.0736 46.0288 81.5275 45.1755 81.0496 44.0149L78.9279 39.0997H62.0415L59.9552 44.0149C59.4432 45.2096 58.8971 46.08 58.3168 46.6261C57.7707 47.1381 56.9515 47.3941 55.8592 47.3941ZM70.4 19.2853L64.6844 32.9045H76.2627L70.5024 19.2853H70.4Z" fill="currentColor"/>
<path d="M101.869 47.3941C100.879 47.3941 100.009 47.1381 99.258 46.6261C98.5071 46.08 97.9096 45.2779 97.4659 44.2197L89.7348 26.4021C89.3593 25.5147 89.2228 24.6955 89.3252 23.9445C89.4276 23.1595 89.786 22.5451 90.4004 22.1013C91.0489 21.6235 91.9364 21.3845 93.0628 21.3845C93.9844 21.3845 94.7353 21.6064 95.3156 22.0501C95.8959 22.4597 96.4079 23.2619 96.8516 24.4565L102.018 37.95L107.552 24.4053C108.03 23.2448 108.559 22.4597 109.14 22.0501C109.72 21.6064 110.522 21.3845 111.546 21.3845C112.433 21.3845 113.133 21.6235 113.645 22.1013C114.191 22.5451 114.516 23.1424 114.618 23.8933C114.755 24.6443 114.618 25.4635 114.208 26.3509L106.324 44.2197C105.88 45.312 105.283 46.1141 104.532 46.6261C103.815 47.1381 102.927 47.3941 101.869 47.3941Z" fill="currentColor"/>
<path d="M126.569 47.4965C124.726 47.4965 123.07 47.1381 121.602 46.4213C120.135 45.7045 118.991 44.7317 118.172 43.5029C117.353 42.2741 116.943 40.8917 116.943 39.3557C116.943 37.5125 117.421 36.0619 118.377 35.0037C119.333 33.9115 120.886 33.1435 123.036 32.6997C125.186 32.2219 128.037 31.9829 131.586 31.9829H133.43V35.9765H131.638C129.897 35.9765 128.48 36.0789 127.388 36.2837C126.33 36.4544 125.562 36.7616 125.084 37.2053C124.64 37.6491 124.418 38.2635 124.418 39.0485C124.418 40.0043 124.743 40.7893 125.391 41.4037C126.074 42.0181 127.047 42.3253 128.31 42.3253C129.299 42.3253 130.17 42.1035 130.921 41.6597C131.706 41.1819 132.32 40.5504 132.764 39.7653C133.208 38.9461 133.43 38.0245 133.43 37.0005V31.1125C133.43 29.6107 133.088 28.5525 132.406 27.9381C131.723 27.2896 130.562 26.9653 128.924 26.9653C128.002 26.9653 126.995 27.0848 125.903 27.3237C124.845 27.5285 123.667 27.8869 122.37 28.3989C121.619 28.7403 120.954 28.8256 120.374 28.6549C119.793 28.4501 119.35 28.1088 119.042 27.6309C118.735 27.1189 118.582 26.5728 118.582 25.9925C118.582 25.3781 118.752 24.7979 119.094 24.2517C119.435 23.6715 119.998 23.2448 120.783 22.9717C122.387 22.3232 123.889 21.8795 125.289 21.6405C126.722 21.4016 128.037 21.2821 129.231 21.2821C131.859 21.2821 134.01 21.6747 135.682 22.4597C137.389 23.2107 138.669 24.3883 139.522 25.9925C140.376 27.5627 140.802 29.5936 140.802 32.0853V43.4517C140.802 44.7147 140.495 45.6875 139.881 46.3701C139.266 47.0528 138.379 47.3941 137.218 47.3941C136.058 47.3941 135.153 47.0528 134.505 46.3701C133.89 45.6875 133.583 44.7147 133.583 43.4517L133.594 43.15C133.594 43.15 133.293 44.032 132.61 44.8853C131.962 45.7045 131.126 46.3531 130.102 46.8309C129.078 47.2747 127.9 47.4965 126.569 47.4965Z" fill="currentColor"/>
<path d="M155.632 47.4965C152.594 47.4965 150.324 46.6603 148.822 44.9877C147.321 43.2811 146.57 40.7552 146.57 37.4101V14.3189C146.57 13.0219 146.894 12.0491 147.542 11.4005C148.225 10.7179 149.198 10.3765 150.461 10.3765C151.69 10.3765 152.628 10.7179 153.277 11.4005C153.959 12.0491 154.301 13.0219 154.301 14.3189V37.1029C154.301 38.5024 154.591 39.5435 155.171 40.2261C155.786 40.8747 156.588 41.1989 157.578 41.1989C157.851 41.1989 158.107 41.1819 158.346 41.1477C158.585 41.1136 158.841 41.0965 159.114 41.0965C159.66 41.0283 160.035 41.1989 160.24 41.6085C160.479 41.984 160.598 42.752 160.598 43.9125C160.598 44.9365 160.394 45.7216 159.984 46.2677C159.574 46.7797 158.943 47.1211 158.09 47.2917C157.748 47.3259 157.356 47.36 156.912 47.3941C156.468 47.4624 156.042 47.4965 155.632 47.4965Z" fill="currentColor"/>
<path d="M175.453 47.4965C172.756 47.4965 170.401 46.9675 168.387 45.9093C166.407 44.8512 164.871 43.3323 163.779 41.3525C162.687 39.3728 162.141 37.0347 162.141 34.3381C162.141 32.3243 162.448 30.5152 163.062 28.9109C163.677 27.3067 164.564 25.9413 165.725 24.8149C166.919 23.6544 168.336 22.784 169.974 22.2037C171.613 21.5893 173.439 21.2821 175.453 21.2821C178.149 21.2821 180.487 21.8112 182.467 22.8693C184.481 23.9275 186.034 25.4293 187.126 27.3749C188.253 29.3205 188.816 31.6416 188.816 34.3381C188.816 36.3861 188.492 38.2123 187.843 39.8165C187.229 41.4208 186.341 42.8032 185.181 43.9637C184.02 45.1243 182.604 46.0117 180.931 46.6261C179.293 47.2064 177.467 47.4965 175.453 47.4965ZM175.453 41.7109C176.579 41.7109 177.552 41.4379 178.371 40.8917C179.19 40.3456 179.839 39.5435 180.317 38.4853C180.795 37.3931 181.034 36.0107 181.034 34.3381C181.034 31.8464 180.522 30.0203 179.498 28.8597C178.474 27.6651 177.125 27.0677 175.453 27.0677C174.361 27.0677 173.388 27.3237 172.534 27.8357C171.715 28.3477 171.067 29.1499 170.589 30.2421C170.145 31.3003 169.923 32.6656 169.923 34.3381C169.923 36.8299 170.435 38.6901 171.459 39.9189C172.483 41.1136 173.814 41.7109 175.453 41.7109Z" fill="currentColor"/>
<path d="M197.411 47.3941C196.148 47.3941 195.175 47.0528 194.492 46.3701C193.844 45.6875 193.52 44.7147 193.52 43.4517V25.2757C193.52 24.0128 193.844 23.0571 194.492 22.4085C195.175 21.7259 196.114 21.3845 197.308 21.3845C198.537 21.3845 199.476 21.7259 200.124 22.4085C200.773 23.0571 201.112 24.1871 201.112 25.45C201.141 25.3955 202.48 23.552 204.016 22.6645C205.586 21.7429 207.361 21.2821 209.34 21.2821C211.354 21.2821 213.01 21.6747 214.307 22.4597C215.604 23.2107 216.577 24.3712 217.225 25.9413C217.874 27.4773 218.198 29.44 218.198 31.8293V43.4517C218.198 44.7147 217.857 45.6875 217.174 46.3701C216.525 47.0528 215.57 47.3941 214.307 47.3941C213.078 47.3941 212.122 47.0528 211.44 46.3701C210.791 45.6875 210.467 44.7147 210.467 43.4517V32.1877C210.467 30.4469 210.143 29.2011 209.494 28.4501C208.88 27.6651 207.924 27.2725 206.627 27.2725C204.988 27.2725 203.674 27.7845 202.684 28.8085C201.729 29.8325 201.251 31.1979 201.251 32.9045V43.4517C201.251 46.08 199.971 47.3941 197.411 47.3941Z" fill="currentColor"/>
<path d="M227.861 47.3429C226.598 47.3429 225.625 46.9845 224.942 46.2677C224.294 45.5168 223.97 44.4757 223.97 43.1445V25.6341C223.97 24.2688 224.294 23.2277 224.942 22.5109C225.625 21.76 226.598 21.3845 227.861 21.3845C229.09 21.3845 230.028 21.76 230.677 22.5109C231.359 23.2277 231.701 24.2688 231.701 25.6341V43.1445C231.701 44.4757 231.377 45.5168 230.728 46.2677C230.079 46.9845 229.124 47.3429 227.861 47.3429ZM227.861 17.1861C226.427 17.1861 225.318 16.8619 224.533 16.2133C223.782 15.5307 223.406 14.5749 223.406 13.3461C223.406 12.0832 223.782 11.1275 224.533 10.4789C225.318 9.79629 226.427 9.45496 227.861 9.45496C229.294 9.45496 230.387 9.79629 231.138 10.4789C231.889 11.1275 232.264 12.0832 232.264 13.3461C232.264 14.5749 231.889 15.5307 231.138 16.2133C230.387 16.8619 229.294 17.1861 227.861 17.1861Z" fill="currentColor"/>
<path d="M246.169 47.4965C244.326 47.4965 242.67 47.1381 241.202 46.4213C239.735 45.7045 238.591 44.7317 237.772 43.5029C236.953 42.2741 236.543 40.8917 236.543 39.3557C236.543 37.5125 237.021 36.0619 237.977 35.0037C238.933 33.9115 240.486 33.1435 242.636 32.6997C244.786 32.2219 247.637 31.9829 251.186 31.9829H253.03V35.9765H251.238C249.497 35.9765 248.08 36.0789 246.988 36.2837C245.93 36.4544 245.162 36.7616 244.684 37.2053C244.24 37.6491 244.018 38.2635 244.018 39.0485C244.018 40.0043 244.343 40.7893 244.991 41.4037C245.674 42.0181 246.647 42.3253 247.91 42.3253C248.899 42.3253 249.77 42.1035 250.521 41.6597C251.306 41.1819 251.92 40.5504 252.364 39.7653C252.808 38.9461 253.03 38.0245 253.03 37.0005V31.1125C253.03 29.6107 252.688 28.5525 252.006 27.9381C251.323 27.2896 250.162 26.9653 248.524 26.9653C247.602 26.9653 246.595 27.0848 245.503 27.3237C244.445 27.5285 243.267 27.8869 241.97 28.3989C241.219 28.7403 240.554 28.8256 239.974 28.6549C239.393 28.4501 238.95 28.1088 238.642 27.6309C238.335 27.1189 238.182 26.5728 238.182 25.9925C238.182 25.3781 238.352 24.7979 238.694 24.2517C239.035 23.6715 239.598 23.2448 240.383 22.9717C241.987 22.3232 243.489 21.8795 244.889 21.6405C246.322 21.4016 247.637 21.2821 248.831 21.2821C251.459 21.2821 253.61 21.6747 255.282 22.4597C256.989 23.2107 258.269 24.3883 259.122 25.9925C259.976 27.5627 260.402 29.5936 260.402 32.0853V43.4517C260.402 44.7147 260.095 45.6875 259.481 46.3701C258.866 47.0528 257.979 47.3941 256.818 47.3941C255.658 47.3941 254.753 47.0528 254.105 46.3701C253.49 45.6875 253.183 44.7147 253.183 43.4517V43.1789C253.183 43.3144 252.893 44.032 252.21 44.8853C251.562 45.7045 250.726 46.3531 249.702 46.8309C248.678 47.2747 247.5 47.4965 246.169 47.4965Z" fill="currentColor"/>
<path d="M22.3444 20.9916C18.7895 20.9916 15.9077 24.0073 15.9077 27.7274C15.9077 31.4475 18.7895 34.4632 22.3444 34.4632C25.8993 34.4632 28.7811 31.4475 28.7811 27.7274C28.7811 24.0073 25.8993 20.9916 22.3444 20.9916Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.6937 49.0667H21.9271C10.8346 48.8653 1.90173 39.3893 1.90173 27.7333C1.90173 15.9513 11.0289 6.40002 22.2878 6.40002C33.3638 6.40002 42.3768 15.6435 42.6667 27.161L42.6314 44.4824C42.338 47.0679 40.2434 49.0667 37.6937 49.0667ZM9.22617 24.667C10.5612 18.3725 15.9275 13.6656 22.3444 13.6656C29.7657 13.6656 35.7818 19.9613 35.7818 27.7274C35.7818 27.7857 35.7825 27.8488 35.7831 27.9136C35.7846 28.0483 35.7861 28.1901 35.7818 28.3103V41.6939H28.7907V40.0685C26.877 41.1655 24.6803 41.7892 22.3444 41.7892C15.9275 41.7892 10.5612 37.0823 9.22617 30.7878C10.5043 30.4129 11.4416 29.1847 11.4416 27.7274C11.4416 26.2701 10.5043 25.0419 9.22617 24.667ZM8.33937 29.9683C9.52696 29.9683 10.4897 28.9609 10.4897 27.7181C10.4897 26.4753 9.52696 25.4678 8.33937 25.4678C7.15178 25.4678 6.18904 26.4753 6.18904 27.7181C6.18904 28.9609 7.15178 29.9683 8.33937 29.9683Z" fill="currentColor"/>
</svg>
</a>
</h2>
</div>
</div>
<script type='module' src="./main.js"></script>
</body>
</html>

View File

@@ -1,13 +0,0 @@
import { dotnet } from './_framework/dotnet.js'
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
const dotnetRuntime = await dotnet
.withDiagnosticTracing(false)
.withApplicationArgumentsFromQuery()
.create();
const config = dotnetRuntime.getConfig();
await dotnetRuntime.runMain(config.mainAssemblyName, [globalThis.location.href]);

View File

@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!--If you are willing to use platform-specific APIs, use conditional compilation.
See https://docs.avaloniaui.net/docs/guides/platforms/platform-specific-code/dotnet for more details.-->
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Desktop" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" >
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gamenight.Ui\Gamenight.Ui.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,23 +0,0 @@
using System;
using Avalonia;
using Avalonia.ReactiveUI;
namespace Gamenight.Ui.Desktop;
sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.UseReactiveUI();
}

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Gamenight.Ui.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@@ -1,23 +0,0 @@
using Foundation;
using UIKit;
using Avalonia;
using Avalonia.Controls;
using Avalonia.iOS;
using Avalonia.Media;
namespace Gamenight.Ui.iOS;
// The UIApplicationDelegate for the application. This class is responsible for launching the
// User Interface of the application, as well as listening (and optionally responding) to
// application events from iOS.
[Register("AppDelegate")]
#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
public partial class AppDelegate : AvaloniaAppDelegate<App>
#pragma warning restore CA1711 // Identifiers should not have incorrect suffix
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
.WithInterFont();
}
}

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0-ios</TargetFramework>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.iOS" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gamenight.Ui\Gamenight.Ui.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>Gamenight.Ui</string>
<key>CFBundleIdentifier</key>
<string>companyName.Gamenight.Ui</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>13.0</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -1,14 +0,0 @@
using UIKit;
namespace Gamenight.Ui.iOS;
public class Application
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View File

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6214" systemVersion="14A314h" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6207" />
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1" />
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" />
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder" />
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480" />
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" />
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2022 " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="441" height="21" />
<fontDescription key="fontDescription" type="system" pointSize="17" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Gamenight.Ui" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="140" width="441" height="43" />
<fontDescription key="fontDescription" type="boldSystem" pointSize="36" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" />
<constraints>
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC" />
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk" />
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l" />
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0" />
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9" />
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g" />
</constraints>
<nil key="simulatedStatusBarMetrics" />
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics" />
<point key="canvasLocation" x="548" y="455" />
</view>
</objects>
</document>

View File

@@ -1,54 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32811.315
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gamenight.Ui", "Gamenight.Ui\Gamenight.Ui.csproj", "{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gamenight.Ui.Desktop", "Gamenight.Ui.Desktop\Gamenight.Ui.Desktop.csproj", "{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gamenight.Ui.Browser", "Gamenight.Ui.Browser\Gamenight.Ui.Browser.csproj", "{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}"
EndProject
#Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gamenight.Ui.iOS", "Gamenight.Ui.iOS\Gamenight.Ui.iOS.csproj", "{EBD9022F-BC83-4846-9A11-6F7F3772DC64}"
#EndProject
#Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gamenight.Ui.Android", "Gamenight.Ui.Android\Gamenight.Ui.Android.csproj", "{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}"
#EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3DA99C4E-89E3-4049-9C22-0A7EC60D83D8}"
ProjectSection(SolutionItems) = preProject
Directory.Packages.props = Directory.Packages.props
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.Build.0 = Release|Any CPU
{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.Build.0 = Release|Any CPU
{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.Build.0 = Release|Any CPU
{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.Build.0 = Release|Any CPU
{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {83CB65B8-011F-4ED7-BCD3-A6CFA935EF7E}
EndGlobalSection
EndGlobal

View File

@@ -1,15 +0,0 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Gamenight.Ui"
x:Class="Gamenight.Ui.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View File

@@ -1,94 +0,0 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using System.Linq;
using Avalonia.Markup.Xaml;
using Gamenight.Ui.ViewModels;
using Gamenight.Ui.Views;
using Microsoft.Extensions.DependencyInjection;
using Gamenight.Ui.Services;
using System;
using Refit;
using Gamenight.Ui.Models;
using ReactiveUI;
namespace Gamenight.Ui;
public partial class App : Application
{
private IServiceProvider? ServiceProvider { get; set; }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// Register all the services needed for the application to run
var collection = new ServiceCollection();
new AppBootstrapper(collection, new RoutingState());
var state = new GamenightState();
collection.AddSingleton(state);
var gamenightApi = RestService.For<IGamenightApi>("http://localhost:8080",
new RefitSettings
{
AuthorizationHeaderValueGetter = state.GetBearerToken
}
);
collection.AddSingleton(gamenightApi);
collection.AddSingleton<MainViewModel>();
collection.AddSingleton<HeaderViewModel>();
collection.AddSingleton<SideBarViewModel>();
collection.AddSingleton<GamenightsViewModel>();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
{
collection.AddSingleton<MainWindow>();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime)
{
collection.AddSingleton<MainView>();
}
RxApp.SuspensionHost.GetAppState<AppBootstrapper>();
ServiceProvider = collection.BuildServiceProvider();
var mainViewModel = ServiceProvider.GetRequiredService<MainViewModel>();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
var window = ServiceProvider.GetRequiredService<MainWindow>();
window.DataContext = mainViewModel;
desktop.MainWindow = window;
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
var mainView = ServiceProvider.GetRequiredService<MainView>();
mainView.DataContext = mainViewModel;
singleViewPlatform.MainView = mainView;
}
base.OnFrameworkInitializationCompleted();
}
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
}

View File

@@ -1,26 +0,0 @@
using ReactiveUI;
using Splat;
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Gamenight.Ui
{
public class AppBootstrapper : ReactiveObject, IScreen
{
public AppBootstrapper(ServiceCollection collection, RoutingState router)
{
Router = router ?? new RoutingState();
collection.AddSingleton<IScreen>(this);
}
public RoutingState Router { get; }
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -1,31 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.ReactiveUI" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="ReactiveUI.SourceGenerators">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Refit" />
</ItemGroup>
</Project>

View File

@@ -1,13 +0,0 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Gamenight.Ui.Models;
public class GamenightState
{
public string JwtToken { get; set; } = "";
public Task<string> GetBearerToken(HttpRequestMessage req, CancellationToken ct) => Task.FromResult($"Bearer: {JwtToken}");
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Gamenight.Ui.ViewModels;
using Gamenight.Ui.Views;
using ReactiveUI;
namespace Gamenight.Ui;
/// <summary>
/// Given a view model, returns the corresponding view if possible.
/// </summary>
[RequiresUnreferencedCode(
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
public class ViewLocator : IDataTemplate, ReactiveUI.IViewLocator
{
public Control? Build(object? param)
{
if (param is null)
return null;
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
public IViewFor ResolveView<T>(T? viewModel, string? contract = null) => viewModel switch
{
GamenightsViewModel context => new GamenightsView { DataContext = context },
_ => throw new ArgumentOutOfRangeException(nameof(viewModel))
};
}

View File

@@ -1,19 +0,0 @@
using System;
using Gamenight.Ui.Services;
using ReactiveUI;
namespace Gamenight.Ui.ViewModels;
public class GamenightsViewModel : ReactiveObject, IRoutableViewModel
{
private IGamenightApi GamenightApi { get; }
public IScreen HostScreen { get; }
public string? UrlPathSegment { get; } = Guid.NewGuid().ToString().Substring(0, 5);
public GamenightsViewModel(IGamenightApi gamenightApi, IScreen hostScreen)
{
GamenightApi = gamenightApi;
HostScreen = hostScreen;
}
}

View File

@@ -1,7 +0,0 @@
namespace Gamenight.Ui.ViewModels;
public class HeaderViewModel : ViewModelBase
{
}

View File

@@ -1,42 +0,0 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Gamenight.Ui.Services;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace Gamenight.Ui.ViewModels;
public partial class MainViewModel : ReactiveObject
{
[ObservableAsProperty]
private string _greeting = "Welcome to Avalonia!";
private IGamenightApi GamenightApi { get; }
public HeaderViewModel HeaderViewModel { get; }
public SideBarViewModel SideBarViewModel { get; }
[ObservableAsProperty]
private IReactiveCommand _loginCommand;
[ObservableAsProperty]
private IScreen _screen;
public MainViewModel(IGamenightApi gamenightApi, IScreen screen, HeaderViewModel headerViewModel, SideBarViewModel sideBarViewModel)
{
GamenightApi = gamenightApi;
_screen = screen;
HeaderViewModel = headerViewModel;
SideBarViewModel = sideBarViewModel;
_loginCommand = ReactiveCommand.Create(Test);
}
public async Task Test() {
var token = await GamenightApi.Token(new Login() {
Username = "admin",
Password = "gamenight!"
});
System.Console.WriteLine($"Token: {token.JwtToken}");
}
}

View File

@@ -1,29 +0,0 @@
using System.Reactive;
using ReactiveUI;
namespace Gamenight.Ui.ViewModels;
public partial class SideBarViewModel : ReactiveObject
{
public SideBarViewModel(IScreen screen, GamenightsViewModel gamenightsViewModel)
{
Screen = screen;
PushViewModel = ReactiveCommand.CreateFromObservable((IRoutableViewModel x) => Screen.Router.Navigate.Execute(x));
GamenightsViewModel = gamenightsViewModel;
}
public IReactiveCommand<IRoutableViewModel, IRoutableViewModel> PushViewModel
{
get;
set => this.RaiseAndSetIfChanged(ref field, value);
}
public GamenightsViewModel GamenightsViewModel
{
get;
set => this.RaiseAndSetIfChanged(ref field, value);
}
private IScreen Screen { get; }
}

View File

@@ -1,7 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Gamenight.Ui.ViewModels;
public abstract class ViewModelBase : ObservableObject
{
}

View File

@@ -1,15 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Gamenight.Ui.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Gamenight.Ui.Views.GamenightsView"
x:DataType="vm:GamenightsViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:GamenightsViewModel />
</Design.DataContext>
<Label Content="LOL"></Label>
</UserControl>

View File

@@ -1,18 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Gamenight.Ui.ViewModels;
namespace Gamenight.Ui.Views;
public partial class GamenightsView : ReactiveUserControl<GamenightsViewModel>
{
public GamenightsView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@@ -1,14 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Gamenight.Ui.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Gamenight.Ui.Views.HeaderView"
x:DataType="vm:HeaderViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:HeaderViewModel />
</Design.DataContext>
</UserControl>

View File

@@ -1,17 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Gamenight.Ui.Views;
public partial class HeaderView : UserControl
{
public HeaderView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@@ -1,42 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Gamenight.Ui.ViewModels"
xmlns:views="clr-namespace:Gamenight.Ui.Views"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:ui="clr-namespace:Gamenight.Ui"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Gamenight.Ui.Views.MainView"
x:DataType="vm:MainViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainViewModel />
</Design.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--<views:HeaderView Grid.Row="0" DataContext="{Binding HeaderViewModel}"/>-->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<views:SideBarView Grid.Column="0" DataContext="{Binding SideBarViewModel}"/>
<reactiveUi:RoutedViewHost Grid.Column="1" Router="{Binding Screen.Router}">
<reactiveUi:RoutedViewHost.DefaultContent>
<TextBlock Text="Default content"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</reactiveUi:RoutedViewHost.DefaultContent>
<reactiveUi:RoutedViewHost.ViewLocator>
<!-- See AppViewLocator.cs section below -->
<ui:ViewLocator />
</reactiveUi:RoutedViewHost.ViewLocator>
</reactiveUi:RoutedViewHost>
</Grid>
</Grid>
</UserControl>

View File

@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace Gamenight.Ui.Views;
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
}
}

View File

@@ -1,11 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:Gamenight.Ui.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Gamenight.Ui.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="Gamenight.Ui">
<views:MainView />
</Window>

View File

@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace Gamenight.Ui.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

View File

@@ -1,16 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Gamenight.Ui.ViewModels"
xmlns:views="clr-namespace:Gamenight.Ui.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Gamenight.Ui.Views.SideBarView"
x:DataType="vm:SideBarViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:SideBarViewModel />
</Design.DataContext>
<Button Content="Gamenights" Command="{Binding PushViewModel}" CommandParameter="{Binding GamenightsViewModel}"/>
</UserControl>

View File

@@ -1,17 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Gamenight.Ui.Views;
public partial class SideBarView : UserControl
{
public SideBarView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@@ -1,3 +0,0 @@
target
src/models/
docs/

2894
backend-actix/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
[package]
name = "backend-actix"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
gamenight-database = { path = "../gamenight-database"}
actix-web = "4"
actix-cors = "0.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.3.0", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] }
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
validator = { version = "0.20", features = ["derive"] }
env_logger = "0.11"
tracing-actix-web = "0.7"

View File

@@ -1,43 +0,0 @@
use std::{
fs::{exists, read_dir, remove_dir_all, File},
io::Write,
process::Command,
};
use std::fs::create_dir;
fn main() {
println!("cargo::rerun-if-changed=gamenight-api.yaml");
if exists("src/models").unwrap() {
remove_dir_all("src/models").unwrap();
}
create_dir("src/models/").unwrap();
let _ = Command::new("openapi-generator")
.args([
"generate",
"-i",
"gamenight-api.yaml",
"-g",
"rust",
"--global-property",
"models",
])
.output()
.expect("Failed to generate models sources for the gamenight API");
let mut file = File::create("./src/models/mod.rs").unwrap();
let paths = read_dir("./src/models").unwrap();
for path in paths {
let path = path.unwrap();
let path = path.path();
let stem = path.file_stem().unwrap();
if stem == "mod" {
continue;
}
let line = format!("pub mod {};\n", stem.to_str().unwrap());
let _ = file.write(line.as_bytes()).unwrap();
}
}

View File

@@ -1,852 +0,0 @@
openapi: 3.0.0
info:
title: Gamenight
version: '1.0'
contact:
name: Dennis Brentjes
email: dennis@brentj.es
url: 'https://brentj.es'
description: Api specification for a Gamenight server
license:
name: MIT
servers:
- url: 'http://localhost:8080'
description: Gamenight
paths:
/token:
post:
summary: 'Login a user.'
operationId: post-token
responses:
'200':
$ref: '#/components/responses/TokenResponse'
'401':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/LoginRequest'
description: Submit your credentials to get a JWT-token to use with the rest of the api.
parameters: []
/refresh_token:
post:
summary: 'Refresh a user token'
operationId: post-refresh-token
responses:
'200':
$ref: '#/components/responses/TokenResponse'
'401':
$ref: '#/components/responses/FailureResponse'
description: Refresh your JWT-token without logging in again.
parameters: []
security:
- JWT-Auth: []
/users:
get:
summary: 'Get all users'
operationId: get-users
responses:
'200':
$ref: '#/components/responses/UsersResponse'
'400':
$ref: '#/components/responses/FailureResponse'
'401':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
post:
summary: 'Registers a user into gamenight.'
operationId: post-users
requestBody:
$ref: '#/components/requestBodies/RegisterRequest'
responses:
'200':
description: ''
'422':
$ref: '#/components/responses/FailureResponse'
description: 'Create a new user given a registration token and user information, username and email must be unique, and password and password_repeat must match.'
parameters: [ ]
security:
- JWT-Auth: [ ]
/user/{userId}:
get:
description: 'Get a user from primary id'
operationId: 'get-user'
parameters:
- in: path
name: userId
schema:
type: string
required: true
description: Uuid of user to get
responses:
'200':
$ref: '#/components/responses/UserResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'404':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
/user/{userId}/owned_games:
get:
summary: Get owned games of user
operationId: get-user-owned_games
parameters:
- in: path
name: userId
schema:
type: string
required: true
description: Uuid of user to get owned games for.
responses:
'200':
$ref: '#/components/responses/OwnedGamesResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: [ ]
/gamenights:
get:
summary: Get a all gamenights
operationId: get-gamenights
responses:
'200':
$ref: '#/components/responses/GamenightsResponse'
'400':
$ref: '#/components/responses/FailureResponse'
'401':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
description: Retrieve the list of gamenights on this gamenight server. Requires authorization.
post:
summary: 'Gets the gamenight'
operationId: post-gamenights
responses:
'200':
description: OK
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: [ ]
requestBody:
$ref: '#/components/requestBodies/AddGamenight'
description: 'Add a gamenight by providing a name and a date, only available when providing an JWT token.'
/gamenight/{gamenightId}:
get:
summary: 'get the specified gamenight'
operationId: get-gamenight
parameters:
- in: path
name: gamenightId
schema:
type: string
required: true
description: Uuid of gamenight to get.
responses:
'200':
$ref: '#/components/responses/GamenightResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
/gamenight/{gamenightId}/participants:
get:
summary: Get all participants for a gamenight
operationId: get-gamenight-participants
parameters:
- in: path
name: gamenightId
schema:
type: string
required: true
description: Uuid of gamenight to get participants for.
responses:
'200':
$ref: '#/components/responses/ParticipantsResponse'
'400':
$ref: '#/components/responses/FailureResponse'
'401':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: [ ]
description: Retrieve the participants of a single gamenight by id.
post:
summary: Add a participant
operationId: post-gamenight-participants
parameters:
- in: path
name: gamenightId
schema:
type: string
required: true
description: Uuid of gamenight to add participants for.
responses:
'200':
description: OK
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/UserIdRequestBody'
security:
- JWT-Auth: []
/gamenight/{gamenightId}/participant/{userId}:
delete:
summary: deletes a gamenight participant
operationId: delete-gamenight-participant
parameters:
- in: path
name: gamenightId
schema:
type: string
required: true
description: Uuid of gamenight to delete participant for
- in: path
name: userId
schema:
type: string
required: true
description: Uuid of the of the participant to remove
responses:
'200':
description: "OK"
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: [ ]
/games:
get:
summary: get all games
operationId: get-games
responses:
'200':
$ref: '#/components/responses/GamesResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
post:
summary: add a game
operationId: post-games
responses:
'200':
$ref: '#/components/responses/GameIdResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/AddGameRequest'
security:
- JWT-Auth: [ ]
/game/{gameId}:
get:
summary: Get this specific game
operationId: get-game
parameters:
- in: path
name: gameId
schema:
type: string
required: true
description: Uuid of game to get.
responses:
'200':
$ref: '#/components/responses/GameResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
delete:
summary: Delete this game.
operationId: delete-game
parameters:
- in: path
name: gameId
schema:
type: string
required: true
description: Uuid of game to delete.
responses:
'200':
description: "Ok"
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: [ ]
put:
summary: Changes this game resource
operationId: put-game
parameters:
- in: path
name: gameId
schema:
type: string
required: true
description: Uuid of game to change.
responses:
'200':
description: "OK"
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/EditGameRequest'
security:
- JWT-Auth: []
/game/{gameId}/owners:
post:
summary: Own this game
operationId: post-game-owners
parameters:
- in: path
name: gameId
schema:
type: string
required: true
description: Uuid of game to own.
requestBody:
$ref: '#/components/requestBodies/OwnGameRequestBody'
responses:
'200':
description: "OK"
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
/game/{gameId}/owner/{userId}:
delete:
summary: no longer own this game
operationId: delete-game-owner
parameters:
- in: path
name: gameId
schema:
type: string
required: true
description: Uuid of game that user no longer owns.
- in: path
name: userId
schema:
type: string
required: true
description: Uuid of user that no longer owns.
responses:
'200':
description: "OK"
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
/locations:
get:
summary: get all locations
operationId: get-locations
responses:
'200':
$ref: '#/components/responses/LocationsResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: [ ]
post:
summary: add a location
operationId: post-locations
responses:
'200':
$ref: '#/components/responses/LocationIdResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/AddLocationRequest'
security:
- JWT-Auth: [ ]
/location/{locationId}:
get:
summary: gets this location
operationId: get-location
parameters:
- in: path
name: locationId
schema:
type: string
required: true
description: Uuid of location to get.
responses:
'200':
$ref: '#/components/responses/LocationResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
/location/{locationId}/authorized_users/:
get:
summary: gets this locations authorized users
operationId: get-location-authorized_users
parameters:
- in: path
name: locationId
schema:
type: string
required: true
description: Uuid of location to get authorized users for.
responses:
'200':
$ref: "#/components/responses/UserIdsResponse"
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: [ ]
post:
summary: Authorize a user
operationId: post-location-authorized_users
parameters:
- in: path
name: locationId
schema:
type: string
required: true
description: Uuid location to authorize for.
responses:
'200':
description: 'Ok'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/UserIdRequestBody'
security:
- JWT-Auth: []
/location/{locationId}/authorized_user/{userId}:
delete:
summary: remove an authorized user from a location
operationId: delete-location-authorized_user
parameters:
- in: path
name: locationId
schema:
type: string
required: true
description: Uuid of location to deauthorize for.
- in: path
name: userId
schema:
type: string
required: true
description: Uuid of user ot deauthorize.
responses:
'200':
description: 'Ok'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: [ ]
components:
schemas:
Gamenight:
title: Gamenight
type: object
properties:
id:
type: string
name:
type: string
location_id:
type: string
datetime:
type: string
owner_id:
type: string
required:
- id
- name
- datetime
- owner_id
Participants:
title: participants
type: object
properties:
participants:
type: array
items:
type: string
required:
- participants
Failure:
title: Failure
type: object
properties:
message:
type: string
description: 'Failure Reason'
Token:
title: Token
type: object
properties:
jwt_token:
type: string
Login:
title: Login
type: object
properties:
username:
type: string
password:
type: string
required:
- username
- password
Registration:
title: Registration
type: object
properties:
username:
type: string
email:
type: string
password:
type: string
password_repeat:
type: string
registration_token:
type: string
required:
- username
- email
- password
- password_repeat
- registration_token
UserId:
title: UserId
type: object
properties:
user_id:
type: string
required:
- user_id
OwnGame:
title: OwnGame
type: object
properties:
user_id:
type: string
location_id:
type: string
required:
- user_id
OwnedGame:
title: OwnedGame
type: object
properties:
user_id:
type: string
game_id:
type: string
location_id:
type: string
required:
- user_id
- game_id
LocationId:
title: LocationId
type: object
properties:
location_id:
type: string
required:
- location_id
AddGamenightRequestBody:
title: AddGamenightRequestBody
type: object
properties:
name:
type: string
datetime:
type: string
required:
- name
- datetime
GamenightId:
title: GamenightId
type: object
properties:
gamenight_id:
type: string
required:
- gamenight_id
GameId:
title: GameId
type: object
properties:
game_id:
type: string
required:
- game_id
User:
type: object
properties:
id:
type: string
username:
type: string
email:
type: string
required:
- id
- username
Game:
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
AddGameRequestBody:
type: object
properties:
name:
type: string
required:
- name
EditGameRequestBody:
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
GameIdsResponse:
type: array
items:
$ref: "#/components/schemas/GameId"
UserIdsResponse:
type: array
items:
$ref: "#/components/schemas/UserId"
AddLocationRequestBody:
type: object
properties:
name:
type: string
address:
type: string
note:
type: string
required:
- name
Location:
type: object
properties:
id:
type: string
name:
type: string
address:
type: string
note:
type: string
required:
- id
- name
requestBodies:
UserIdRequestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UserId'
OwnGameRequestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/OwnGame'
LoginRequest:
content:
application/json:
schema:
$ref: '#/components/schemas/Login'
RegisterRequest:
content:
application/json:
schema:
$ref: '#/components/schemas/Registration'
AddGamenight:
content:
application/json:
schema:
$ref: '#/components/schemas/AddGamenightRequestBody'
GetParticipants:
content:
application/json:
schema:
$ref: '#/components/schemas/GamenightId'
AddGameRequest:
content:
application/json:
schema:
$ref: '#/components/schemas/AddGameRequestBody'
EditGameRequest:
content:
application/json:
schema:
$ref: '#/components/schemas/EditGameRequestBody'
AddLocationRequest:
content:
application/json:
schema:
$ref: '#/components/schemas/AddLocationRequestBody'
responses:
TokenResponse:
description: Example response
content:
application/json:
schema:
$ref: '#/components/schemas/Token'
FailureResponse:
description: Example response
content:
application/json:
schema:
$ref: '#/components/schemas/Failure'
ParticipantsResponse:
description: Response with a list of participant uuids
content:
application/json:
schema:
$ref: '#/components/schemas/Participants'
GamenightsResponse:
description: Example response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Gamenight'
UsersResponse:
description: List of all Users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
GamenightResponse:
description: A gamenight being hosted
content:
application/json:
schema:
$ref: '#/components/schemas/Gamenight'
UserResponse:
description: A user in the gamenight system
content:
application/json:
schema:
$ref: '#/components/schemas/User'
GamesResponse:
description: A list of Games in this gamenight instance.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Game'
GameResponse:
description: A game.
content:
application/json:
schema:
$ref: '#/components/schemas/Game'
GameIdResponse:
description: a game id
content:
application/json:
schema:
$ref: '#/components/schemas/GameId'
GameIdsResponse:
description: A list of game ids.
content:
application/json:
schema:
$ref: '#/components/schemas/GameIdsResponse'
UserIdsResponse:
description: A list of user ids.
content:
application/json:
schema:
$ref: '#/components/schemas/UserIdsResponse'
LocationResponse:
description: A location
content:
application/json:
schema:
$ref: '#/components/schemas/Location'
LocationIdResponse:
description: A location Id
content:
application/json:
schema:
$ref: '#/components/schemas/LocationId'
LocationsResponse:
description: A list of all LocationsResponse
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Location'
OwnedGamesResponse:
description: A list of OwnedGames
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/OwnedGame'
securitySchemes:
JWT-Auth:
type: http
scheme: bearer
bearerFormat: JWT
description: ''

View File

@@ -1,68 +0,0 @@
#[allow(unused_imports)]
pub mod models;
pub mod request;
use actix_cors::Cors;
use actix_web::http;
use actix_web::middleware::Logger;
use actix_web::web;
use actix_web::App;
use actix_web::HttpServer;
use gamenight_database::{get_connection_pool, run_migration, GetConnection};
use request::*;
use tracing_actix_web::TracingLogger;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let url = "postgres://root:root@127.0.0.1/gamenight";
let pool = get_connection_pool(url);
let mut conn = pool.get_conn();
run_migration(&mut conn);
env_logger::init_from_env(env_logger::Env::new().default_filter_or("debug"));
HttpServer::new(move || {
let cors = Cors::default()
.allowed_origin("0.0.0.0")
.allowed_origin_fn(|_origin, _req_head| true)
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(3600);
App::new()
.wrap(cors)
.wrap(Logger::default())
.wrap(TracingLogger::default())
.app_data(web::Data::new(pool.clone()))
.service(token::post_token)
.service(token::post_refresh_token)
.service(users::get_users)
.service(users::post_users)
.service(user::get_user)
.service(user_owned_games::get_user_owned_games)
.service(gamenights::get_gamenights)
.service(gamenights::post_gamenights)
.service(gamenight::get_gamenight)
.service(gamenight_participants::get_gamenight_participants)
.service(gamenight_participants::post_gamenight_participants)
.service(gamenight_participant::delete_gamenight_participant)
.service(games::get_games)
.service(games::post_games)
.service(game::get_game)
.service(game::delete_game)
.service(game::put_game)
.service(game_owners::post_game_owners)
.service(game_owner::delete_game_owner)
.service(locations::get_locations)
.service(locations::post_locations)
.service(location_authorized_users::get_location_authorized_users)
.service(location_authorized_users::post_location_authorized_users)
.service(location_authorized_user::delete_location_authorized_user)
})
.bind(("::1", 8080))?
.run()
.await
}

View File

@@ -1,81 +0,0 @@
use std::future::{ready, Ready};
use actix_web::{dev::Payload, http, web::Data, FromRequest, HttpRequest};
use chrono::Utc;
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use gamenight_database::{
user::{get_user, User},
DbPool,
};
use super::error::ApiError;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
exp: i64,
uid: Uuid,
}
pub struct AuthUser(pub User);
impl From<User> for AuthUser {
fn from(value: User) -> Self {
Self(value)
}
}
fn get_claims(req: &HttpRequest) -> Result<Claims, ApiError> {
let token = req
.headers()
.get(http::header::AUTHORIZATION)
.map(|h| h.to_str().unwrap().split_at(7).1.to_string());
let token = token.ok_or(ApiError {
status: 400,
message: "JWT-token was not specified in the Authorization header as Bearer: token"
.to_string(),
})?;
let secret = "secret";
Ok(decode::<Claims>(
token.as_str(),
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default(),
)?
.claims)
}
pub fn get_token(user: &User) -> Result<String, ApiError> {
let claims = Claims {
exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(),
uid: user.id,
};
let secret = "secret";
Ok(encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)?)
}
impl FromRequest for AuthUser {
type Error = ApiError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
ready((|| -> Result<AuthUser, ApiError> {
let pool = req
.app_data::<Data<DbPool>>()
.expect("No database configured");
let mut conn = pool.get().expect("couldn't get db connection from pool");
let uid = get_claims(req)?.uid;
let user = get_user(&mut conn, uid)?;
Ok(user.into())
})())
}
}

View File

@@ -1,94 +0,0 @@
use actix_web::{
error::BlockingError,
http::{header::ContentType, StatusCode},
HttpResponse, ResponseError,
};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter, Result};
use validator::ValidationErrors;
use gamenight_database::error::DatabaseError;
#[derive(Serialize, Deserialize, Debug)]
pub struct ApiError {
#[serde(skip_serializing)]
pub status: u16,
pub message: String,
}
impl Display for ApiError {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", self.message)
}
}
impl ResponseError for ApiError {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(StatusCode::from_u16(self.status).unwrap())
.content_type(ContentType::json())
.body(serde_json::to_string(&self).unwrap())
}
}
impl From<DatabaseError> for ApiError {
fn from(value: DatabaseError) -> Self {
ApiError {
status: 500,
message: value.0,
}
}
}
impl From<BlockingError> for ApiError {
fn from(value: BlockingError) -> Self {
ApiError {
status: 500,
message: value.to_string(),
}
}
}
impl From<serde_json::Error> for ApiError {
fn from(value: serde_json::Error) -> Self {
ApiError {
status: 500,
message: value.to_string(),
}
}
}
impl From<jsonwebtoken::errors::Error> for ApiError {
fn from(value: jsonwebtoken::errors::Error) -> Self {
ApiError {
status: 500,
message: value.to_string(),
}
}
}
impl From<ValidationErrors> for ApiError {
fn from(value: ValidationErrors) -> Self {
ApiError {
status: 422,
message: value.to_string(),
}
}
}
impl From<chrono::ParseError> for ApiError {
fn from(value: chrono::ParseError) -> Self {
ApiError {
status: 422,
message: value.to_string(),
}
}
}
impl From<uuid::Error> for ApiError {
fn from(value: uuid::Error) -> Self {
ApiError {
status: 422,
message: value.to_string(),
}
}
}

View File

@@ -1,64 +0,0 @@
use actix_web::{delete, get, http::header::ContentType, put, web, HttpResponse, Responder};
use gamenight_database::game::{load_game, remove_game, rename_game};
use gamenight_database::{
user::Role,
DbPool, GetConnection,
};
use uuid::Uuid;
use crate::{models::{
edit_game_request_body::EditGameRequestBody, game::Game
,
}, request::{authorization::AuthUser, error::ApiError}};
#[get("/game/{game_id}")]
pub async fn get_game(
pool: web::Data<DbPool>,
_user: AuthUser,
game_id: web::Path<Uuid>
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let db_game = load_game(&mut conn, game_id.into_inner())?;
let model = Game {
id: db_game.id.to_string(),
name: db_game.name,
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}
#[delete("/game/{game_id}")]
pub async fn delete_game(
pool: web::Data<DbPool>,
user: AuthUser,
game_id: web::Path<Uuid>,
) -> Result<impl Responder, ApiError> {
if user.0.role != Role::Admin {
Ok(HttpResponse::Unauthorized())
} else {
let mut conn = pool.get_conn();
remove_game(&mut conn, game_id.into_inner())?;
Ok(HttpResponse::Ok())
}
}
#[put("/game/{gameId}")]
pub async fn put_game(
pool: web::Data<DbPool>,
_user: AuthUser,
game_id: web::Path<Uuid>,
edit_data: web::Json<EditGameRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
rename_game(
&mut conn,
game_id.into_inner(),
edit_data.name.clone(),
)?;
Ok(HttpResponse::Ok())
}

View File

@@ -1,26 +0,0 @@
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
use actix_web::{delete, web, HttpResponse, Responder};
use gamenight_database::owned_game::{disown_game, OwnedGame};
use gamenight_database::{DbPool, GetConnection};
use uuid::Uuid;
#[delete("/game/{gameId}/owner/{ownerId}")]
pub async fn delete_game_owner(
pool: web::Data<DbPool>,
_user: AuthUser,
game_id: web::Path<Uuid>,
owner_id: web::Path<Uuid>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
disown_game(
&mut conn,
OwnedGame {
user_id: owner_id.into_inner(),
game_id: game_id.into_inner(),
location_id: None
},
)?;
Ok(HttpResponse::Ok())
}

View File

@@ -1,27 +0,0 @@
use actix_web::{post, web, HttpResponse, Responder};
use gamenight_database::{DbPool, GetConnection};
use gamenight_database::owned_game::{own_game, OwnedGame};
use uuid::Uuid;
use crate::models::game_id::GameId;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
#[post("/game/{gameId}/owned_games")]
pub async fn post_game_owners(
pool: web::Data<DbPool>,
_user: AuthUser,
game_id: web::Path<GameId>,
own_data: web::Json<OwnedGame>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
own_game(
&mut conn,
OwnedGame {
user_id: own_data.user_id,
game_id: Uuid::parse_str(&game_id.game_id)?,
location_id: own_data.location_id,
},
)?;
Ok(HttpResponse::Ok())
}

View File

@@ -1,32 +0,0 @@
use actix_web::{get, http::header::ContentType, web, HttpResponse, Responder};
use uuid::Uuid;
use gamenight_database::{gamenight, DbPool, GetConnection};
use crate::models::gamenight_id::GamenightId;
use crate::request::error::ApiError;
use crate::{
models::gamenight::Gamenight,
request::authorization::AuthUser,
};
#[get("/gamenight/{gamenightId}")]
pub async fn get_gamenight(
pool: web::Data<DbPool>,
_user: AuthUser,
path: web::Path<GamenightId>
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let gamenight = gamenight::get_gamenight(&mut conn, Uuid::parse_str(&path.gamenight_id)?)?;
let model = Gamenight {
id: gamenight.id.to_string(),
datetime: gamenight.datetime.to_rfc3339(),
location_id: gamenight.location_id.map(|x| x.to_string()),
name: gamenight.name,
owner_id: gamenight.owner_id.to_string(),
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}

View File

@@ -1,28 +0,0 @@
use actix_web::{delete, web, HttpResponse, Responder};
use gamenight_database::{DbPool, GetConnection};
use gamenight_database::gamenight_participants::GamenightParticipant;
use uuid::Uuid;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
#[delete("/gamenight/{gamenightId}/participant/{userId}")]
pub async fn delete_gamenight_participant(
pool: web::Data<DbPool>,
_user: AuthUser,
gamenight_id: web::Path<Uuid>,
user_id: web::Path<Uuid>
) -> Result<impl Responder, ApiError> {
web::block(move || -> Result<usize, ApiError> {
let mut conn = pool.get_conn();
let participant = GamenightParticipant {
gamenight_id: gamenight_id.into_inner(),
user_id: user_id.into_inner(),
};
let x = gamenight_database::gamenight_participants::delete_gamenight_participant(&mut conn, participant)?;
Ok(x)
})
.await??;
Ok(HttpResponse::Ok())
}

View File

@@ -1,54 +0,0 @@
use crate::models::participants::Participants;
use crate::models::user_id::UserId;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
use actix_web::http::header::ContentType;
use actix_web::{get, post, web, HttpResponse, Responder};
use gamenight_database::gamenight_participants::{insert_gamenight_participant, GamenightParticipant};
use gamenight_database::{DbPool, GetConnection};
use uuid::Uuid;
#[get("/gamenight/{gamenightId}/participants")]
pub async fn get_gamenight_participants(
pool: web::Data<DbPool>,
_user: AuthUser,
gamenight_id: web::Path<Uuid>
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let users = gamenight_database::get_participants(
&mut conn,
&gamenight_id.into_inner(),
)?
.iter()
.map(|x| x.to_string())
.collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&Participants {
participants: users,
})?))
}
#[post("/gamenight/{gamenightId}/participants")]
pub async fn post_gamenight_participants(
pool: web::Data<DbPool>,
_user: AuthUser,
gamenight_id: web::Path<Uuid>,
user_id: web::Json<UserId>,
) -> Result<impl Responder, ApiError> {
web::block(move || -> Result<usize, ApiError> {
let mut conn = pool.get_conn();
Ok(insert_gamenight_participant(
&mut conn,
GamenightParticipant {
gamenight_id: gamenight_id.into_inner(),
user_id: Uuid::parse_str(&user_id.user_id)?,
},
)?)
})
.await??;
Ok(HttpResponse::Ok())
}

View File

@@ -1,57 +0,0 @@
use actix_web::{get, post, web, HttpResponse, Responder};
use actix_web::http::header::ContentType;
use chrono::{DateTime, ParseError};
use gamenight_database::{gamenight, DbPool, GetConnection};
use uuid::Uuid;
use crate::models::add_gamenight_request_body::AddGamenightRequestBody;
use crate::models::gamenight::Gamenight;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
impl AddGamenightRequestBody {
pub fn into_with_user(&self, user: AuthUser) -> Result<gamenight::Gamenight, ParseError> {
Ok(gamenight::Gamenight {
datetime: DateTime::parse_from_rfc3339(&self.datetime)?.with_timezone(&chrono::Utc),
id: Uuid::new_v4(),
name: self.name.clone(),
owner_id: user.0.id,
location_id: None,
})
}
}
#[get("/gamenights")]
pub async fn get_gamenights(
pool: web::Data<DbPool>,
_user: AuthUser,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let gamenights: Vec<gamenight::Gamenight> = gamenight_database::gamenights(&mut conn)?;
let model: Vec<Gamenight> = gamenights
.iter()
.map(|x| Gamenight {
id: x.id.to_string(),
name: x.name.clone(),
location_id: x.location_id.map(|x| x.to_string()),
datetime: x.datetime.to_rfc3339(),
owner_id: x.owner_id.to_string(),
})
.collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}
#[post("/gamenights")]
pub async fn post_gamenights(
pool: web::Data<DbPool>,
user: AuthUser,
gamenight_data: web::Json<AddGamenightRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
gamenight::add_gamenight(&mut conn, gamenight_data.into_with_user(user)?)?;
Ok(HttpResponse::Ok())
}

View File

@@ -1,54 +0,0 @@
use actix_web::{get, post, web, HttpResponse, Responder};
use actix_web::http::header::ContentType;
use gamenight_database::{DbPool, GetConnection};
use gamenight_database::game::insert_game;
use uuid::Uuid;
use crate::models::add_game_request_body::AddGameRequestBody;
use crate::models::game::Game;
use crate::models::game_id::GameId;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
impl From<AddGameRequestBody> for gamenight_database::game::Game {
fn from(value: AddGameRequestBody) -> Self {
Self {
id: Uuid::new_v4(),
name: value.name,
}
}
}
#[get("/games")]
pub async fn get_games(
pool: web::Data<DbPool>,
_user: AuthUser,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let games: Vec<gamenight_database::game::Game> = gamenight_database::games(&mut conn)?;
let model: Vec<Game> = games
.iter()
.map(|x| Game {
id: x.id.to_string(),
name: x.name.clone(),
})
.collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}
#[post("/games")]
pub async fn post_games(
pool: web::Data<DbPool>,
_user: AuthUser,
game_data: web::Json<AddGameRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let game = game_data.0.into();
insert_game(&mut conn, &game)?;
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&GameId{game_id: game.id.to_string()})?))
}

View File

@@ -1,28 +0,0 @@
use crate::models::location::Location;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
use actix_web::http::header::ContentType;
use actix_web::{get, web, HttpResponse, Responder};
use gamenight_database::{DbPool, GetConnection};
use uuid::Uuid;
#[get("/location/{locationId}")]
pub async fn get_locations(
pool: web::Data<DbPool>,
_user: AuthUser,
location_id: web::Path<Uuid>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let location = gamenight_database::location::load_location(&mut conn, location_id.into_inner())?;
let model = Location {
id: location.id.to_string(),
name: location.name.clone(),
note: location.note.clone(),
address: location.address.clone(),
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}

View File

@@ -1,23 +0,0 @@
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
use actix_web::{delete, web, HttpResponse, Responder};
use gamenight_database::location_owner::{revoke_permission, LocationOwner};
use gamenight_database::{DbPool, GetConnection};
use uuid::Uuid;
#[delete("/location/{location_id}/authorized_users/{user_id}")]
pub async fn delete_location_authorized_user(
pool: web::Data<DbPool>,
_user: AuthUser,
location_id: web::Path<Uuid>,
user_id: web::Path<Uuid>
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
revoke_permission(&mut conn, LocationOwner {
user_id: user_id.into_inner(),
location_id: location_id.into_inner()
})?;
Ok(HttpResponse::Ok())
}

View File

@@ -1,60 +0,0 @@
use actix_web::{get, post, web, HttpResponse, Responder};
use actix_web::http::header::ContentType;
use gamenight_database::{DbPool, GetConnection};
use gamenight_database::location_owner::{grant_permission, location_permissions, LocationOwner};
use gamenight_database::user::Role;
use uuid::Uuid;
use crate::models::location_id::LocationId;
use crate::models::user_id::UserId;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
impl From<Uuid> for UserId {
fn from(uuid: Uuid) -> Self {
Self {
user_id: uuid.into(),
}
}
}
#[get("/location/{locationId}/authorized_users/")]
pub async fn get_location_authorized_users(
pool: web::Data<DbPool>,
_user: AuthUser,
location_id: web::Path<Uuid>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let permissions =
location_permissions(&mut conn, location_id.into_inner())?;
let model: Vec<UserId> = permissions.into_iter().map(Into::into).collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}
#[post("/location/{locationId}/authorized_users/")]
pub async fn post_location_authorized_users(
pool: web::Data<DbPool>,
user: AuthUser,
location_id1: web::Path<LocationId>,
user_id: web::Json<UserId>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let location_uuid = Uuid::parse_str(&location_id1.location_id)?;
let user_uuid = Uuid::parse_str(&user_id.user_id)?;
let authorized = location_permissions(&mut conn, location_uuid)?;
if user.0.role != Role::Admin && !authorized.contains(&user.0.id) {
Ok(HttpResponse::Unauthorized())
} else {
grant_permission(&mut conn, LocationOwner {
location_id: location_uuid,
user_id: user_uuid,
})?;
Ok(HttpResponse::Ok())
}
}

View File

@@ -1,62 +0,0 @@
use actix_web::{get, post, web, HttpResponse, Responder};
use actix_web::http::header::ContentType;
use gamenight_database::{DbPool, GetConnection};
use gamenight_database::location::{insert_location, locations};
use uuid::Uuid;
use crate::models::add_location_request_body::AddLocationRequestBody;
use crate::models::location::Location;
use crate::models::location_id::LocationId;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
impl From<AddLocationRequestBody> for gamenight_database::location::Location {
fn from(value: AddLocationRequestBody) -> Self {
Self {
id: Uuid::new_v4(),
name: value.name,
address: value.address,
note: value.note,
}
}
}
#[get("/locations")]
pub async fn get_locations(
pool: web::Data<DbPool>,
_user: AuthUser,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let games: Vec<gamenight_database::location::Location> = locations(&mut conn)?;
let model: Vec<Location> = games
.iter()
.map(|x| Location {
id: x.id.to_string(),
name: x.name.clone(),
address: x.address.clone(),
note: x.note.clone(),
})
.collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}
#[post("/locations")]
pub async fn post_locations(
pool: web::Data<DbPool>,
_user: AuthUser,
game_data: web::Json<AddLocationRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let uuid = insert_location(&mut conn, game_data.0.into())?;
let model = LocationId {
location_id: uuid.to_string(),
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}

View File

@@ -1,20 +0,0 @@
mod authorization;
mod error;
pub mod token;
pub mod users;
pub mod user;
pub mod user_owned_games;
pub mod gamenights;
pub mod gamenight;
pub mod gamenight_participants;
pub mod gamenight_participant;
pub mod games;
pub mod game;
pub mod game_owners;
pub mod game_owner;
pub mod locations;
pub mod location;
pub mod location_authorized_users;
pub mod location_authorized_user;

View File

@@ -1,54 +0,0 @@
use crate::models::login::Login;
use crate::models::token::Token;
use crate::request::authorization::{get_token, AuthUser};
use crate::request::error::ApiError;
use actix_web::http::header::ContentType;
use actix_web::{post, web, HttpResponse, Responder};
use gamenight_database::{DbPool, GetConnection};
impl From<Login> for gamenight_database::user::LoginUser {
fn from(val: Login) -> Self {
gamenight_database::user::LoginUser {
username: val.username,
password: val.password,
}
}
}
#[post("/token")]
pub async fn post_token(
pool: web::Data<DbPool>,
login_data: web::Json<Login>,
) -> Result<impl Responder, ApiError> {
let data = login_data.into_inner();
if let Ok(Some(user)) = web::block(move || {
let mut conn = pool.get_conn();
gamenight_database::login(&mut conn, data.into())
})
.await?
{
let token = get_token(&user)?;
let response = Token {
jwt_token: Some(token),
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&response)?))
} else {
Err(ApiError {
status: 401,
message: "User doesn't exist or password doesn't match".to_string(),
})
}
}
#[post("/refresh_token")]
pub async fn post_refresh_token(user: AuthUser) -> Result<impl Responder, ApiError> {
let new_token = get_token(&user.0)?;
let response = Token {
jwt_token: Some(new_token),
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&response)?))
}

View File

@@ -1,37 +0,0 @@
use crate::models::user::User;
use crate::request::error::ApiError;
use actix_web::{get, http::header::ContentType, web, HttpResponse, Responder};
use gamenight_database::{DbPool, GetConnection};
use serde_json;
use uuid::Uuid;
use super::authorization::AuthUser;
impl From<gamenight_database::user::User> for User {
fn from(value: gamenight_database::user::User) -> Self {
Self {
id: value.id.to_string(),
username: value.username,
email: None,
}
}
}
#[get("/user/{userId}")]
pub async fn get_user(
pool: web::Data<DbPool>,
_user: AuthUser,
user_id: web::Path<Uuid>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let user = gamenight_database::user::get_user(
&mut conn,
user_id.into_inner()
)?;
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&user)?))
}

View File

@@ -1,28 +0,0 @@
use actix_web::{get, web, HttpResponse, Responder};
use actix_web::http::header::ContentType;
use gamenight_database::{DbPool, GetConnection};
use gamenight_database::owned_game::owned_games;
use uuid::Uuid;
use crate::models;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
#[get("/user/{userId}/owned_games")]
pub async fn get_user_owned_games(
pool: web::Data<DbPool>,
_user: AuthUser,
user_id: web::Path<Uuid>
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let game_ids = owned_games(&mut conn, user_id.into_inner())?;
let model = game_ids.iter().map(|(u,g , l)| models::owned_game::OwnedGame {
user_id: u.to_string(),
game_id: g.to_string(),
location_id: l.map(|x| x.to_string())
}).collect::<Vec<models::owned_game::OwnedGame>>();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}

View File

@@ -1,106 +0,0 @@
use actix_web::{get, post, web, HttpResponse, Responder};
use actix_web::http::header::ContentType;
use gamenight_database::{DbPool, GetConnection};
use gamenight_database::user::{count_users_with_email, count_users_with_username};
use serde::{Deserialize, Serialize};
use validator::{Validate, ValidateArgs, ValidationError};
use crate::models::registration::Registration;
use crate::models::user::User;
use crate::request::authorization::AuthUser;
use crate::request::error::ApiError;
impl From<Registration> for gamenight_database::user::Register {
fn from(val: Registration) -> Self {
gamenight_database::user::Register {
email: val.email,
username: val.username,
password: val.password,
}
}
}
pub struct RegisterContext<'v_a> {
pub pool: &'v_a DbPool,
}
pub fn unique_username(
username: &String,
context: &RegisterContext,
) -> Result<(), ValidationError> {
let mut conn = context.pool.get_conn();
match count_users_with_username(&mut conn, username) {
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("User already exists")),
Err(_) => Err(ValidationError::new("Database error while validating user")),
}
}
pub fn unique_email(email: &String, context: &RegisterContext) -> Result<(), ValidationError> {
let mut conn = context.pool.get_conn();
match count_users_with_email(&mut conn, email) {
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("email already exists")),
Err(_) => Err(ValidationError::new(
"Database error while validating email",
)),
}
}
#[derive(Serialize, Deserialize, Clone, Validate)]
#[validate(context = RegisterContext::<'v_a>)]
pub struct ValidatableRegistration {
#[validate(length(min = 1), custom(function = "unique_username", use_context))]
pub username: String,
#[validate(email, custom(function = "unique_email", use_context))]
pub email: String,
#[validate(length(min = 10), must_match(other = "password_repeat",))]
pub password: String,
pub password_repeat: String,
}
impl From<Registration> for ValidatableRegistration {
fn from(value: Registration) -> Self {
Self {
username: value.username,
email: value.email,
password: value.password,
password_repeat: value.password_repeat,
}
}
}
#[get("/users")]
pub async fn get_users(
pool: web::Data<DbPool>,
_user: AuthUser,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let users = gamenight_database::user::get_users(&mut conn)?;
let model: Vec<User> = users.into_iter().map(Into::into).collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}
#[post("/users")]
pub async fn post_users(
pool: web::Data<DbPool>,
register_data: web::Json<Registration>,
) -> Result<impl Responder, ApiError> {
web::block(move || -> Result<(), ApiError> {
let validatable_registration: ValidatableRegistration = register_data.clone().into();
validatable_registration.validate_with_args(&RegisterContext { pool: &pool })?;
let register_request = register_data.into_inner().into();
let mut conn = pool.get_conn();
gamenight_database::register(&mut conn, register_request)?;
Ok(())
})
.await??;
Ok(HttpResponse::Ok())
}

6
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/target
.vscode
App.toml
*.sqlite
*.sqlite-shm
*.sqlite-wal

8
backend/App.toml.example Normal file
View File

@@ -0,0 +1,8 @@
#Copy this file over to Rocket.toml after changing all relevant values.
[default]
jwt_secret = "some really good secret"
[global.databases]
gamenight_database = { url = "gamenight.sqlite" }

2434
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
backend/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "gamenight"
version = "0.1.0"
authors = ["Dennis Brentjes <d.brentjes@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = { version = "0.5.0-rc.1", features = ["default", "json"] }
libsqlite3-sys = { version = ">=0.8.0, <0.19.0", features = ["bundled"] }
rocket_sync_db_pools = { version = "0.1.0-rc.1", features = ["diesel_sqlite_pool"] }
diesel = { version = "1.4.8", features = ["sqlite"] }
diesel_migrations = "1.4.0"
rocket_dyn_templates = { version = "0.1.0-rc.1", features = ["handlebars"] }
chrono = "0.4.19"
serde = "1.0.136"
password-hash = "0.4"
argon2 = "0.4"
rand_core = { version = "0.6", features = ["std"] }
diesel-derive-enum = { version = "1.1", features = ["sqlite"] }
jsonwebtoken = "8.1"
validator = { version = "0.14", features = ["derive"] }

View File

@@ -1,4 +1,4 @@
-- This file should undo anything in `up.sql` -- This file should undo anything in `up.sql`
drop table gamenight; drop table gamenight;
drop table game; drop table known_games;

View File

@@ -0,0 +1,12 @@
-- Your SQL goes here
CREATE TABLE gamenight (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
game text TEXT NOT NULL,
datetime TEXT NOT NULL
);
CREATE TABLE known_games (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
game TEXT UNIQUE NOT NULL
);

View File

@@ -1,6 +1,4 @@
-- This file should undo anything in `up.sql` -- This file should undo anything in `up.sql`
drop table pwd; drop table pwd;
drop table client; drop table user;
drop type Role;

View File

@@ -0,0 +1,12 @@
CREATE TABLE user (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
role TEXT NOT NULL
);
CREATE TABLE pwd (
user_id INTEGER NOT NULL PRIMARY KEY,
password TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
);

3
backend/requests/gamenights.sh Executable file
View File

@@ -0,0 +1,3 @@
echo $JWT
curl -v -X GET -H "Authorization: Bearer ${JWT}" localhost:8000/api/gamenights

1
backend/requests/login.sh Executable file
View File

@@ -0,0 +1 @@
curl -v -X POST -H "Content-Type: application/json" -d '{"username": "roflin", "password": "oreokoekje123"}' localhost:8000/api/login

1
backend/requests/register.sh Executable file
View File

@@ -0,0 +1 @@
curl -v -X POST -H "Content-Type: application/json" -d '{"username": "roflin", "email": "user@example.com", "password": "oreokoekje123", "password_repeat": "oreokoekje123"}' localhost:8000/api/register

215
backend/src/api.rs Normal file
View File

@@ -0,0 +1,215 @@
use crate::schema;
use crate::schema::DbConn;
use crate::AppConfig;
use chrono::Utc;
use jsonwebtoken::decode;
use jsonwebtoken::encode;
use jsonwebtoken::DecodingKey;
use jsonwebtoken::Validation;
use jsonwebtoken::{EncodingKey, Header};
use rocket::http::Status;
use rocket::request::Outcome;
use rocket::request::{FromRequest, Request};
use rocket::response;
use rocket::serde::json;
use rocket::serde::json::{json, Json};
use rocket::State;
use serde::{Deserialize, Serialize};
use serde::ser::{SerializeStruct, Serializer};
use std::borrow::Cow;
use std::fmt;
use validator::{ValidateArgs, ValidationErrors};
#[derive(Serialize, Deserialize, Debug)]
struct ApiResponse {
ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
jwt: Option<Cow<'static, str>>,
}
impl ApiResponse {
const SUCCES: Self = Self {
ok: true,
message: None,
jwt: None,
};
fn login_response(jwt: String) -> Self {
Self {
ok: true,
message: None,
jwt: Some(Cow::Owned(jwt)),
}
}
}
#[derive(Debug)]
pub enum ApiError {
RequestError(String),
ValidationErrors(ValidationErrors),
Unauthorized,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ApiError::*;
write!(f, "{}", match &self {
RequestError(e) => e,
ValidationErrors(_) => "???",
Unauthorized => "username and password didn't match",
})
}
}
impl From<schema::DatabaseError> for ApiError {
fn from(e: schema::DatabaseError) -> Self {
ApiError::RequestError(e.to_string())
}
}
impl From<ValidationErrors> for ApiError {
fn from(e: ValidationErrors) -> Self {
ApiError::ValidationErrors(e)
}
}
impl Serialize for ApiError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("ApiError", 2)?;
state.serialize_field("ok", &false)?;
state.serialize_field("message", &self.to_string())?;
state.end()
}
}
impl<'r> response::Responder<'r, 'static> for ApiError {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
use ApiError::*;
let status = match self {
RequestError(_) => Status::BadRequest,
ValidationErrors(_) => Status::BadRequest,
Unauthorized => Status::Unauthorized,
};
response::Response::build()
.merge(json!(self).respond_to(req)?)
.status(status)
.ok()
}
}
const AUTH_HEADER: &str = "Authorization";
const BEARER: &str = "Bearer ";
#[rocket::async_trait]
impl<'r> FromRequest<'r> for schema::User {
type Error = ApiError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let header = match req.headers().get_one(AUTH_HEADER) {
Some(header) => header,
None => {
return Outcome::Forward(())
}
};
if !header.starts_with(BEARER) {
return Outcome::Forward(());
};
let app_config = req.guard::<&State<AppConfig>>().await.unwrap().inner();
let jwt = header.trim_start_matches(BEARER).to_owned();
let token = match decode::<Claims>(
&jwt,
&DecodingKey::from_secret(app_config.jwt_secret.as_bytes()),
&Validation::default(),
) {
Ok(token) => token,
Err(_) => {
return Outcome::Forward(())
}
};
let id = token.claims.uid;
let conn = req.guard::<DbConn>().await.unwrap();
return Outcome::Success(schema::get_user(conn, id).await);
}
}
#[get("/gamenights")]
pub async fn gamenights(conn: DbConn, _user: schema::User) -> json::Value {
json!(schema::get_all_gamenights(conn).await)
}
#[get("/gamenights", rank = 2)]
pub async fn gamenights_unauthorized() -> Status {
Status::Unauthorized
}
#[post("/gamenight", format = "application/json", data = "<gamenight_json>")]
pub async fn gamenight_post_json(
conn: DbConn,
user: Option<schema::User>,
gamenight_json: Json<schema::GameNightNoId>,
) -> Result<json::Value, Status> {
if user.is_some() {
schema::insert_gamenight(conn, gamenight_json.into_inner()).await;
Ok(json!(ApiResponse::SUCCES))
} else {
Err(Status::Unauthorized)
}
}
#[post("/register", format = "application/json", data = "<register_json>")]
pub async fn register_post_json(
conn: DbConn,
register_json: Json<schema::Register>,
) -> Result<json::Value, ApiError> {
let register = register_json.into_inner();
let register_clone = register.clone();
conn.run(move |c| register_clone.validate_args((c, c)))
.await?;
schema::insert_user(conn, register).await?;
Ok(json!(ApiResponse::SUCCES))
}
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
exp: i64,
uid: i32,
role: schema::Role,
}
#[post("/login", format = "application/json", data = "<login_json>")]
pub async fn login_post_json(
conn: DbConn,
config: &State<AppConfig>,
login_json: Json<schema::Login>,
) -> Result<json::Value, ApiError> {
let login_result = schema::login(conn, login_json.into_inner()).await?;
if !login_result.result {
return Err(ApiError::Unauthorized);
}
let my_claims = Claims {
exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(),
uid: login_result.id.unwrap(),
role: login_result.role.unwrap(),
};
let secret = &config.inner().jwt_secret;
match encode(
&Header::default(),
&my_claims,
&EncodingKey::from_secret(secret.as_bytes()),
) {
Ok(token) => Ok(json!(ApiResponse::login_response(token))),
Err(error) => Err(ApiError::RequestError(error.to_string())),
}
}

69
backend/src/main.rs Normal file
View File

@@ -0,0 +1,69 @@
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate diesel_migrations;
#[macro_use]
extern crate diesel;
use rocket::{
fairing::AdHoc,
figment::{
providers::{Env, Format, Serialized, Toml},
Figment, Profile,
},
};
use rocket_dyn_templates::Template;
use serde::{Deserialize, Serialize};
mod api;
pub mod schema;
mod site;
#[derive(Debug, Deserialize, Serialize)]
pub struct AppConfig {
jwt_secret: String,
}
impl Default for AppConfig {
fn default() -> AppConfig {
AppConfig {
jwt_secret: String::from("secret"),
}
}
}
#[launch]
fn rocket() -> _ {
let figment = Figment::from(rocket::Config::default())
.merge(Serialized::defaults(AppConfig::default()))
.merge(Toml::file("App.toml").nested())
.merge(Env::prefixed("APP_").global())
.select(Profile::from_env_or("APP_PROFILE", "default"));
let rocket = rocket::custom(figment)
.attach(schema::DbConn::fairing())
.attach(Template::fairing())
.attach(AdHoc::on_ignite("Run Migrations", schema::run_migrations))
.attach(AdHoc::config::<AppConfig>())
.mount(
"/",
routes![
site::index,
site::gamenights,
site::add_game_night,
site::register
],
)
.mount(
"/api",
routes![
api::gamenights,
api::gamenights_unauthorized,
api::gamenight_post_json,
api::register_post_json,
api::login_post_json
],
);
rocket
}

319
backend/src/schema.rs Normal file
View File

@@ -0,0 +1,319 @@
use crate::diesel::BoolExpressionMethods;
use crate::diesel::Connection;
use crate::diesel::ExpressionMethods;
use crate::diesel::QueryDsl;
use argon2::password_hash::SaltString;
use argon2::PasswordHash;
use argon2::PasswordVerifier;
use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher},
Argon2,
};
use diesel::dsl::count;
use diesel::RunQueryDsl;
use diesel_derive_enum::DbEnum;
use rocket::{Build, Rocket};
use rocket_sync_db_pools::database;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use validator::{Validate, ValidationError};
#[database("gamenight_database")]
pub struct DbConn(diesel::SqliteConnection);
impl Deref for DbConn {
type Target = rocket_sync_db_pools::Connection<DbConn, diesel::SqliteConnection>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
table! {
gamenight (id) {
id -> Integer,
game -> Text,
datetime -> Text,
}
}
table! {
known_games (game) {
id -> Integer,
game -> Text,
}
}
table! {
use diesel::sql_types::Integer;
use diesel::sql_types::Text;
use super::RoleMapping;
user(id) {
id -> Integer,
username -> Text,
email -> Text,
role -> RoleMapping,
}
}
table! {
pwd(user_id) {
user_id -> Integer,
password -> Text,
}
}
allow_tables_to_appear_in_same_query!(gamenight, known_games,);
pub enum DatabaseError {
Hash(password_hash::Error),
Query(String),
}
impl std::fmt::Display for DatabaseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match self {
DatabaseError::Hash(err) => write!(f, "{}", err),
DatabaseError::Query(err) => write!(f, "{}", err),
}
}
}
pub async fn get_all_gamenights(conn: DbConn) -> Vec<GameNight> {
conn.run(|c| gamenight::table.load::<GameNight>(c).unwrap())
.await
}
pub async fn insert_gamenight(conn: DbConn, new_gamenight: GameNightNoId) -> () {
conn.run(|c| {
diesel::insert_into(gamenight::table)
.values(new_gamenight)
.execute(c)
.unwrap()
})
.await;
}
pub async fn insert_user(conn: DbConn, new_user: Register) -> Result<(), DatabaseError> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = match argon2.hash_password(new_user.password.as_bytes(), &salt) {
Ok(hash) => hash.to_string(),
Err(error) => return Err(DatabaseError::Hash(error)),
};
let user_insert_result = conn
.run(move |c| {
c.transaction(|| {
diesel::insert_into(user::table)
.values((
user::username.eq(&new_user.username),
user::email.eq(&new_user.email),
user::role.eq(Role::User),
))
.execute(c)?;
let ids: Vec<i32> = match user::table
.filter(
user::username
.eq(&new_user.username)
.and(user::email.eq(&new_user.email)),
)
.select(user::id)
.get_results(c)
{
Ok(id) => id,
Err(e) => return Err(e),
};
diesel::insert_into(pwd::table)
.values((pwd::user_id.eq(ids[0]), pwd::password.eq(&password_hash)))
.execute(c)
})
})
.await;
match user_insert_result {
Err(e) => Err(DatabaseError::Query(e.to_string())),
_ => Ok(()),
}
}
pub async fn login(conn: DbConn, login: Login) -> Result<LoginResult, DatabaseError> {
conn.run(move |c| -> Result<LoginResult, DatabaseError> {
let id: i32 = match user::table
.filter(user::username.eq(&login.username))
.or_filter(user::email.eq(&login.username))
.select(user::id)
.first(c)
{
Ok(id) => id,
Err(error) => return Err(DatabaseError::Query(error.to_string())),
};
let pwd: String = match pwd::table
.filter(pwd::user_id.eq(id))
.select(pwd::password)
.first(c)
{
Ok(pwd) => pwd,
Err(error) => return Err(DatabaseError::Query(error.to_string())),
};
let parsed_hash = match PasswordHash::new(&pwd) {
Ok(hash) => hash,
Err(error) => return Err(DatabaseError::Hash(error)),
};
if Argon2::default()
.verify_password(&login.password.as_bytes(), &parsed_hash)
.is_ok()
{
let role: Role = match user::table
.filter(user::id.eq(id))
.select(user::role)
.first(c)
{
Ok(role) => role,
Err(error) => return Err(DatabaseError::Query(error.to_string())),
};
Ok(LoginResult {
result: true,
id: Some(id),
role: Some(role),
})
} else {
Ok(LoginResult {
result: false,
id: None,
role: None,
})
}
})
.await
}
pub async fn get_user(conn: DbConn, id: i32) -> User {
conn.run(move |c| user::table.filter(user::id.eq(id)).first(c).unwrap())
.await
}
pub fn unique_username(
username: &String,
conn: &diesel::SqliteConnection,
) -> Result<(), ValidationError> {
match user::table
.select(count(user::username))
.filter(user::username.eq(username))
.execute(conn)
{
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("User already exists")),
Err(_) => Err(ValidationError::new("Database error while validating user")),
}
}
pub fn unique_email(
email: &String,
conn: &diesel::SqliteConnection,
) -> Result<(), ValidationError> {
match user::table
.select(count(user::email))
.filter(user::email.eq(email))
.execute(conn)
{
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("email already exists")),
Err(_) => Err(ValidationError::new(
"Database error while validating email",
)),
}
}
pub async fn run_migrations(rocket: Rocket<Build>) -> Rocket<Build> {
// This macro from `diesel_migrations` defines an `embedded_migrations`
// module containing a function named `run`. This allows the example to be
// run and tested without any outside setup of the database.
embed_migrations!();
let conn = DbConn::get_one(&rocket).await.expect("database connection");
conn.run(|c| embedded_migrations::run(c))
.await
.expect("can run migrations");
rocket
}
#[derive(Debug, Serialize, Deserialize, DbEnum, Clone)]
pub enum Role {
Admin,
User,
}
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
#[table_name = "user"]
pub struct User {
pub id: i32,
pub username: String,
pub email: String,
pub role: Role,
}
#[derive(Serialize, Deserialize, Debug, FromForm, Insertable)]
#[table_name = "known_games"]
pub struct GameNoId {
pub game: String,
}
#[derive(Serialize, Deserialize, Debug, FromForm, Queryable)]
pub struct Game {
pub id: i32,
pub game: String,
}
#[derive(Serialize, Deserialize, Debug, FromForm, Insertable)]
#[table_name = "gamenight"]
pub struct GameNightNoId {
pub game: String,
pub datetime: String,
}
#[derive(Serialize, Deserialize, Debug, FromForm, Queryable)]
pub struct GameNight {
pub id: i32,
pub game: String,
pub datetime: String,
}
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct Register {
#[validate(
length(min = 1),
custom(function = "unique_username", arg = "&'v_a diesel::SqliteConnection")
)]
pub username: String,
#[validate(
email,
custom(function = "unique_email", arg = "&'v_a diesel::SqliteConnection")
)]
pub email: String,
#[validate(length(min = 10), must_match = "password_repeat")]
pub password: String,
pub password_repeat: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Login {
pub username: String,
pub password: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LoginResult {
pub result: bool,
pub id: Option<i32>,
pub role: Option<Role>,
}

90
backend/src/site.rs Normal file
View File

@@ -0,0 +1,90 @@
use crate::schema;
use rocket::request::FlashMessage;
use rocket::response::Redirect;
use rocket_dyn_templates::Template;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
#[derive(Serialize, Deserialize, Debug)]
struct FlashData {
has_data: bool,
kind: Cow<'static, str>,
message: Cow<'static, str>,
}
impl FlashData {
const EMPTY: Self = Self {
has_data: false,
message: Cow::Borrowed(""),
kind: Cow::Borrowed(""),
};
}
#[derive(Serialize, Deserialize, Debug)]
struct GameNightsData {
gamenights: Vec<schema::GameNight>,
flash: FlashData,
}
#[get("/gamenights")]
pub async fn gamenights(conn: schema::DbConn) -> Template {
let gamenights = schema::get_all_gamenights(conn).await;
let data = GameNightsData {
gamenights: gamenights,
flash: FlashData::EMPTY,
};
Template::render("gamenights", &data)
}
#[get("/")]
pub async fn index() -> Redirect {
Redirect::to(uri!(gamenights))
}
#[derive(Serialize, Deserialize, Debug)]
struct GameNightAddData {
post_url: String,
flash: FlashData,
}
#[get("/gamenight/add")]
pub async fn add_game_night(flash: Option<FlashMessage<'_>>) -> Template {
let flash_data = match flash {
None => FlashData::EMPTY,
Some(flash) => FlashData {
has_data: true,
message: Cow::Owned(flash.message().to_string()),
kind: Cow::Owned(flash.kind().to_string()),
},
};
let data = GameNightAddData {
post_url: "/api/gamenight".to_string(),
flash: flash_data,
};
Template::render("gamenight_add", &data)
}
#[derive(Serialize, Deserialize, Debug)]
struct RegisterData {
flash: FlashData,
}
#[get("/register")]
pub async fn register(flash: Option<FlashMessage<'_>>) -> Template {
let flash_data = match flash {
None => FlashData::EMPTY,
Some(flash) => FlashData {
has_data: true,
message: Cow::Owned(flash.message().to_string()),
kind: Cow::Owned(flash.kind().to_string()),
},
};
let data = RegisterData { flash: flash_data };
Template::render("register", &data)
}

View File

@@ -0,0 +1,5 @@
{{#if has_data}}
<div>
<p>{{kind}}: {{message}}</p>
</div>
{{/if}}

View File

@@ -0,0 +1,16 @@
<html>
<head>
</head>
<body>
{{> flash flash }}
<form action="{{post_url}}" method="post">
<label for="game">Game:</label><br>
<input type="text" id="game" name="game"><br>
<label for="datetime">Wanneer:</label><br>
<input type="text" id="datetime" name="datetime">
<input type="submit" value="Submit">
</form>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<html>
<head>
</head>
<body>
{{> flash flash }}
{{#each gamenights}}
<div>
<span>game: {{this.game}}</span>
<span>when: {{this.datetime}}</span>
</div>
{{/each}}
</body>
</html>

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