Compare commits

...

41 Commits

Author SHA1 Message Date
33188a92b2 Update .gitea/workflows/review.yaml 2026-02-05 14:45:19 +00:00
Renovate Bot
aa8a4d2810 Update postgres Docker tag to v18
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m37s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m51s
Generation data pour merge sur master / generateData (pull_request) Failing after 11s
2026-02-05 10:19:55 +00:00
e6d0dda44d Delete .gitea/workflows/renovate.yaml
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m25s
Generation data pour merge sur master / generateData (pull_request) Successful in 2m27s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m29s
Pas necessaire
2025-04-17 12:17:18 +02:00
Renovate Bot
556204cfec Update ghcr.io/renovatebot/renovate Docker tag to v39.235.2
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m33s
Generation data pour merge sur master / generateData (pull_request) Successful in 10s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m26s
renovate / renovate (push) Failing after 8s
2025-04-08 00:01:18 +00:00
Renovate Bot
409ee35f40 Update ghcr.io/renovatebot/renovate Docker tag to v39.233.5
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m26s
Generation data pour merge sur master / generateData (pull_request) Successful in 9s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m35s
renovate / renovate (push) Failing after 44s
2025-04-07 00:00:42 +00:00
Renovate Bot
f8a288f965 Update ghcr.io/renovatebot/renovate Docker tag to v39.233.3
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m27s
Generation data pour merge sur master / generateData (pull_request) Successful in 9s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m21s
2025-04-05 00:00:43 +00:00
Renovate Bot
28b58585d4 Update ghcr.io/renovatebot/renovate Docker tag to v39.232.0
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m26s
Generation data pour merge sur master / generateData (pull_request) Successful in 10s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m26s
renovate / renovate (push) Failing after 8s
2025-04-02 13:54:03 +00:00
Renovate Bot
3c29fcd1f9 Update ghcr.io/renovatebot/renovate Docker tag to v39.230.1
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 2m28s
Generation data pour merge sur master / generateData (pull_request) Successful in 9s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m24s
2025-04-02 00:00:46 +00:00
Renovate Bot
319da32a35 Update ghcr.io/renovatebot/renovate Docker tag to v39.219.2
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m28s
Generation data pour merge sur master / generateData (pull_request) Successful in 2m39s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m26s
renovate / renovate (push) Failing after 8s
2025-03-27 15:40:16 +00:00
Renovate Bot
813af8d923 Update ghcr.io/renovatebot/renovate Docker tag to v39.205.0
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m21s
Generation data pour merge sur master / generateData (pull_request) Successful in 2m29s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m27s
renovate / renovate (push) Failing after 7s
2025-03-17 00:00:55 +00:00
Renovate Bot
2140e81aeb Update ghcr.io/renovatebot/renovate Docker tag to v39.185.7
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 3m22s
Generation data pour merge sur master / generateData (pull_request) Successful in 4m13s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m31s
renovate / renovate (push) Failing after 7s
2025-03-04 00:01:28 +00:00
Renovate Bot
7478589124 Update ghcr.io/renovatebot/renovate Docker tag to v39.173.1
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m28s
Generation data pour merge sur master / generateData (pull_request) Failing after 2h12m15s
Generation data pour merge sur master / reviewProcess (push) Successful in 3m8s
renovate / renovate (push) Failing after 8s
2025-02-19 00:00:48 +00:00
Renovate Bot
d6ac91c080 Update ghcr.io/renovatebot/renovate Docker tag to v39.63.1
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m2s
Generation data pour merge sur master / generateData (pull_request) Successful in 1m40s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m4s
2024-12-12 23:00:58 +00:00
Renovate Bot
a8d477b529 Update ghcr.io/renovatebot/renovate Docker tag to v39.13.1
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m0s
Generation data pour merge sur master / generateData (pull_request) Successful in 10s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m2s
renovate / renovate (push) Failing after 8s
2024-11-13 23:01:26 +00:00
Renovate Bot
a35124adc7 Update ghcr.io/renovatebot/renovate Docker tag to v39.11.4
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m0s
Generation data pour merge sur master / generateData (pull_request) Successful in 1m37s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m0s
renovate / renovate (push) Failing after 44s
2024-11-12 23:00:52 +00:00
Renovate Bot
924ba6f187 Update ghcr.io/renovatebot/renovate Docker tag to v39
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 55s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m1s
renovate / renovate (push) Failing after 8s
2024-11-10 23:00:50 +00:00
Renovate Bot
2a03502692 Update ghcr.io/renovatebot/renovate Docker tag to v38.142.7
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m0s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m2s
renovate / renovate (push) Failing after 8s
2024-11-04 23:01:05 +00:00
Renovate Bot
b2d007c271 Update ghcr.io/renovatebot/renovate Docker tag to v38.142.5
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m0s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m4s
renovate / renovate (push) Failing after 23s
2024-11-03 23:01:05 +00:00
Renovate Bot
443bccb2ed Update ghcr.io/renovatebot/renovate Docker tag to v38.142.3
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 59s
Generation data pour merge sur master / generateData (pull_request) Successful in 10s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m0s
renovate / renovate (push) Failing after 24s
2024-11-02 23:01:38 +00:00
Renovate Bot
08f4d88969 Update ghcr.io/renovatebot/renovate Docker tag to v38.142.2
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m1s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
Generation data pour merge sur master / reviewProcess (push) Successful in 58s
renovate / renovate (push) Failing after 55s
2024-11-01 23:00:56 +00:00
Renovate Bot
15d2aa9416 Update ghcr.io/renovatebot/renovate Docker tag to v38.138.4
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m0s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
Generation data pour merge sur master / reviewProcess (push) Successful in 2m3s
2024-10-30 23:01:10 +00:00
Renovate Bot
1463b35998 Update ghcr.io/renovatebot/renovate Docker tag to v38.135.2
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 56s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m6s
2024-10-29 23:01:11 +00:00
Renovate Bot
26e8a9c355 Update ghcr.io/renovatebot/renovate Docker tag to v38.132.3
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m4s
Generation data pour merge sur master / generateData (pull_request) Successful in 10s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m1s
renovate / renovate (push) Failing after 12s
2024-10-27 23:01:42 +00:00
Renovate Bot
36b6677506 Update ghcr.io/renovatebot/renovate Docker tag to v38.132.1
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m2s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m2s
renovate / renovate (push) Failing after 42s
2024-10-26 22:01:09 +00:00
Renovate Bot
978f2ed552 Update ghcr.io/renovatebot/renovate Docker tag to v38.130.2
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m3s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m1s
renovate / renovate (push) Failing after 8s
2024-10-23 22:01:45 +00:00
Renovate Bot
b8ed2d0909 Update ghcr.io/renovatebot/renovate Docker tag to v38.130.0
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m2s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m2s
renovate / renovate (push) Failing after 44s
2024-10-22 22:01:54 +00:00
Renovate Bot
40caa952d2 Update ghcr.io/renovatebot/renovate Docker tag to v38.129.1
Some checks failed
Generation data pour merge sur master / reviewProcess (push) Successful in 1m29s
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m4s
Generation data pour merge sur master / generateData (pull_request) Successful in 21s
renovate / renovate (push) Failing after 53s
2024-10-22 09:38:21 +02:00
Renovate Bot
01534fbace Update ghcr.io/renovatebot/renovate Docker tag to v38.127.0
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m11s
Generation data pour merge sur master / generateData (pull_request) Successful in 11s
2024-10-18 05:30:09 +00:00
Renovate Bot
cdaf6c7575 Update ghcr.io/renovatebot/renovate Docker tag to v38.124.3
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 2m24s
Generation data pour merge sur master / generateData (pull_request) Successful in 3m35s
2024-10-17 07:02:48 +00:00
Renovate Bot
e857207486 Update ghcr.io/renovatebot/renovate Docker tag to v38
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 2m34s
Generation data pour merge sur master / generateData (pull_request) Successful in 2m0s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m14s
2024-10-12 07:26:42 +00:00
Renovate Bot
3cfea65a75 Update postgres Docker tag to v17
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m25s
Generation data pour merge sur master / generateData (pull_request) Successful in 2m41s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m43s
renovate / renovate (push) Failing after 11s
2024-09-28 00:01:43 +00:00
ba9eb7cfb3 Suppression de renovate suir les push sur main
Some checks failed
Generation data pour merge sur master / reviewProcess (push) Successful in 1m33s
renovate / renovate (push) Failing after 16s
2024-09-26 11:36:46 +00:00
Renovate Bot
86db6fa3b9 Update dotnet monorepo to v9
Some checks failed
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m43s
Generation data pour merge sur master / generateData (pull_request) Successful in 2m46s
renovate / renovate (push) Failing after 33s
Generation data pour merge sur master / reviewProcess (push) Successful in 1m46s
2024-09-19 21:41:46 +00:00
6afeb6e30d Ajout d'un cron renovate pour les dependances
Some checks failed
Generation data pour merge sur master / reviewProcess (push) Has been cancelled
renovate / renovate (push) Failing after 16s
2024-09-17 09:42:32 +02:00
16275e3cd5 ajout d'un premier test pour l'affichage des jeux
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 3m10s
Generation data pour merge sur master / generateData (pull_request) Successful in 1m41s
Generation data pour merge sur master / reviewProcess (push) Successful in 2m0s
2024-09-10 22:57:19 +02:00
0cde51e01c nettoyage des warning, ajout de localization
All checks were successful
Generation data pour merge sur master / reviewProcess (pull_request) Successful in 1m21s
Generation data pour merge sur master / generateData (pull_request) Successful in 1m31s
Generation data pour merge sur master / generateData (push) Has been skipped
Generation data pour merge sur master / reviewProcess (push) Successful in 1m16s
2024-09-09 23:16:34 +02:00
cd4788b2b0 Ajout de 2 workflows pour les merges et reviews
All checks were successful
Generation data pour merge sur master / generateData (push) Has been skipped
Generation data pour merge sur master / reviewProcess (push) Successful in 1m12s
2024-09-08 22:26:56 +02:00
aa2694585e Correction du chemin dans les includes de document pour le rendu en ligne
All checks were successful
Main Build Process / generate documentation (push) Successful in 17s
2024-09-06 22:06:33 +02:00
ac2b6b2c81 Migration vers postgres
All checks were successful
Main Build Process / generate documentation (pull_request) Successful in 16s
Main Build Process / generate documentation (push) Successful in 18s
- suppression de sqlite
- ajout de postgres
2024-09-06 13:47:30 +02:00
284c58188e ajout d'un compose avec un service de bdd
All checks were successful
Main Build Process / generate documentation (pull_request) Successful in 14s
Main Build Process / generate documentation (push) Successful in 18s
2024-09-05 23:36:11 +02:00
6eacd4c480 changement du template pour inclure l'authentification
All checks were successful
Main Build Process / generate documentation (pull_request) Successful in 14s
Main Build Process / generate documentation (push) Successful in 14s
2024-09-05 23:20:34 +02:00
85 changed files with 4912 additions and 183 deletions

View File

@@ -6,6 +6,7 @@ root = true
dotnet_diagnostic.IDE1006.severity = warning
dotnet_diagnostic.IDE0005.severity = error
dotnet_diagnostic.CA5394.severity = none
dotnet_diagnostic.CA1848.severity = none #optimisatio logger pas nécessaire
# Exclude generated code
[src/**/Migrations/*.cs]

View File

@@ -0,0 +1,36 @@
name: "Generation data pour merge sur master"
on:
pull_request:
types:
- closed
branches: ["main"]
jobs:
generateData:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Get build container
id: adocbuild
uses: tonynv/asciidoctor-action@master
with:
program: "asciidoctor -D docs --backend=html5 -o index.html documentation/readme.adoc"
- name: Print execution time
run: echo "Time ${{ steps.adocbuild.outputs.time }}"
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: Doc
path: ./docs
retention-days: 5
- name: build docker images
run: docker build -t gitea.borealian.ovh/mcmuzzle/ludikzone:latest -f ./docker-prod/Dockerfile .
- name: upload image
run: |
echo "${{ secrets.PERSO_GITEAPASSWORD}}" | docker login gitea.borealian.ovh --username mcmuzzle --password-stdin
docker push gitea.borealian.ovh/mcmuzzle/ludikzone:latest

View File

@@ -0,0 +1,27 @@
name: "Generation data pour merge sur master"
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
reviewProcess:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: '10.0.x'
- name: Install dependencies
run: dotnet restore ./src/LudikZone.sln
- name: Build
run: dotnet build ./src/LudikZone.sln
- name: Test with the dotnet CLI
run: dotnet test ./src/LudikZone.sln

6
.gitignore vendored
View File

@@ -16,4 +16,8 @@ bin
obj
report
tools/
TestResults
TestResults
#docker data
/docker-dev/data
/docker-prod/data

11
docker-dev/compose.yml Normal file
View File

@@ -0,0 +1,11 @@
services:
postgresql:
image: postgres:18
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- ./data/postgres/:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${DB_DATABASE:-ludikZone}
- POSTGRES_USER=${DB_USER:-admin}
- POSTGRES_PASSWORD=${DB_PASSWORD:-admin}

5
docker-prod/.env Normal file
View File

@@ -0,0 +1,5 @@
DB_HOST="postgresql"
DB_PORT=5432
DB_USER="admin"
DB_PASSWORD="admin"
DB_DATABASE="ludikZone"

5
docker-prod/.env.example Normal file
View File

@@ -0,0 +1,5 @@
DB_HOST="localhost"
DB_PORT=5432
DB_USER="admin"
DB_PASSWORD="admin"
DB_DATABASE="ludikZone"

21
docker-prod/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["src/LudikZoneBlazor/LudikZoneBlazor.csproj", "src/LudikZoneBlazor/LudikZoneBlazor.csproj"]
RUN dotnet restore "src/LudikZoneBlazor/LudikZoneBlazor.csproj"
COPY src src
RUN ls -a -l
RUN dotnet build "src/LudikZoneBlazor/LudikZoneBlazor.csproj" -c Release -o /app/build
FROM build AS publish
WORKDIR /src
RUN dotnet publish "src/LudikZoneBlazor/LudikZoneBlazor.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "LudikZoneBlazor.dll"]

24
docker-prod/compose.yml Normal file
View File

@@ -0,0 +1,24 @@
services:
postgresql:
image: postgres:18
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- ./data/postgres/:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${DB_DATABASE:-ludikZone}
- POSTGRES_USER=${DB_USER:-admin}
- POSTGRES_PASSWORD=${DB_PASSWORD:-admin}
ludikzone:
image: gitea.borealian.ovh/mcmuzzle/ludikzone:latest
ports:
- 8080:8080
environment:
- DB_HOST=${DB_HOST:-"postgresql"}
- DB_PORT=${DB_PORT:-5432}
- DB_USER=${DB_USER:-"admin"}
- DB_PASSWORD=${DB_PASSWORD:-"admin"}
- DB_DATABASE=${DB_DATABASE:-"ludikZone"}
depends_on:
- postgresql

View File

@@ -0,0 +1,23 @@
=== Prérequis
* DotNet 8
* Docker (26.0.2 minimum recommandé)
* Docker Compose (2.26.1 minimum recommandé)
=== Cloner le dépot
=== Lancer les containers
[,sh]
----
docker compose -f .\docker-dev\compose.yml -p ludik-zone up -d
----
=== Migrer la bdd
Lors de la migration de la BDD, les données de connections seront celles du *appsettings.Development.json*
[,sh]
----
dotnet ef database update --project .\src\LudikZoneBlazor\LudikZoneBlazor.csproj
----

26
documentation/readme.adoc Normal file
View File

@@ -0,0 +1,26 @@
= Ludik zone documentation (DEX)
:source-highlighter: pygments
:source-language: bash
:doctype: book
:source-highlighter: rouge
:toc: macro
:toc-title: Table des matières
toc::[]
== Présentation
Le projet Ludik zone est un projet open source ayant comme objectif de créer un portait hébergeant différent jeux en tour par tour.
== Instalation du poste de dev
include::{docdir}/documentation_technique/installation-poste.adoc[]
== Architecture du projet
include::{docdir}/documentation_technique/architecture-projet.adoc[]
== Developpement et livraison
include::{docdir}/documentation_technique/developpement-livraison.adoc[]

11
renovate.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": [
"config:base"
],
"packageRules": [
{
"updateTypes": ["minor", "patch", "pin", "digest"],
"automerge": false
}
]
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,20 @@
namespace LudikZone.model;
/// <summary>
/// Informations relative a un jeu précis
/// </summary>
public class Game
{
/// <summary> Identifiant unique d'un jeu </summary>
public int Id { get; init; }
/// <summary> le nom du jeu </summary>
public required string Name { get; init; }
/// <summary> Le nombre minimum de joueur pour une partie </summary>
public int MinPlayerCount { get; init; }
/// <summary> Le nombre maximum de joueur pour une partie </summary>
public int MaxPlayerCount { get; init; }
}

View File

@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LudikZoneBlazor", "LudikZoneBlazor\LudikZoneBlazor.csproj", "{169488B5-AE88-41C0-8EFE-658E5A259EDB}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LudikZoneBlazor", "LudikZoneBlazor\LudikZoneBlazor.csproj", "{79AF398A-0AC3-425C-BA51-B6CFBEECAC28}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LudikZone.model", "LudikZone.model\LudikZone.model.csproj", "{C50658FF-0EE9-4EE8-AAA3-FAF4B9F622EA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -14,9 +16,13 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{169488B5-AE88-41C0-8EFE-658E5A259EDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{169488B5-AE88-41C0-8EFE-658E5A259EDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{169488B5-AE88-41C0-8EFE-658E5A259EDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{169488B5-AE88-41C0-8EFE-658E5A259EDB}.Release|Any CPU.Build.0 = Release|Any CPU
{79AF398A-0AC3-425C-BA51-B6CFBEECAC28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{79AF398A-0AC3-425C-BA51-B6CFBEECAC28}.Debug|Any CPU.Build.0 = Debug|Any CPU
{79AF398A-0AC3-425C-BA51-B6CFBEECAC28}.Release|Any CPU.ActiveCfg = Release|Any CPU
{79AF398A-0AC3-425C-BA51-B6CFBEECAC28}.Release|Any CPU.Build.0 = Release|Any CPU
{C50658FF-0EE9-4EE8-AAA3-FAF4B9F622EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C50658FF-0EE9-4EE8-AAA3-FAF4B9F622EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C50658FF-0EE9-4EE8-AAA3-FAF4B9F622EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C50658FF-0EE9-4EE8-AAA3-FAF4B9F622EA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,112 @@
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using LudikZoneBlazor.Components.Account.Pages;
using LudikZoneBlazor.Components.Account.Pages.Manage;
using LudikZoneBlazor.Data;
namespace Microsoft.AspNetCore.Routing;
internal static class IdentityComponentsEndpointRouteBuilderExtensions
{
// These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project.
public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
var accountGroup = endpoints.MapGroup("/Account");
accountGroup.MapPost("/PerformExternalLogin", (
HttpContext context,
[FromServices] SignInManager<ApplicationUser> signInManager,
[FromForm] string provider,
[FromForm] string returnUrl) =>
{
IEnumerable<KeyValuePair<string, StringValues>> query = [
new("ReturnUrl", returnUrl),
new("Action", ExternalLogin.LoginCallbackAction)];
var redirectUrl = UriHelper.BuildRelative(
context.Request.PathBase,
"/Account/ExternalLogin",
QueryString.Create(query));
var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return TypedResults.Challenge(properties, [provider]);
});
accountGroup.MapPost("/Logout", async (
ClaimsPrincipal user,
SignInManager<ApplicationUser> signInManager,
[FromForm] string returnUrl) =>
{
await signInManager.SignOutAsync().ConfigureAwait(true);
return TypedResults.LocalRedirect($"~/{returnUrl}");
});
var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization();
manageGroup.MapPost("/LinkExternalLogin", async (
HttpContext context,
[FromServices] SignInManager<ApplicationUser> signInManager,
[FromForm] string provider) =>
{
// Clear the existing external cookie to ensure a clean login process
await context.SignOutAsync(IdentityConstants.ExternalScheme).ConfigureAwait(false);
var redirectUrl = UriHelper.BuildRelative(
context.Request.PathBase,
"/Account/Manage/ExternalLogins",
QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction));
var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User));
return TypedResults.Challenge(properties, [provider]);
});
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
var downloadLogger = loggerFactory.CreateLogger("DownloadPersonalData");
manageGroup.MapPost("/DownloadPersonalData", async (
HttpContext context,
[FromServices] UserManager<ApplicationUser> userManager,
[FromServices] AuthenticationStateProvider authenticationStateProvider) =>
{
var user = await userManager.GetUserAsync(context.User).ConfigureAwait(false);
if (user is null)
{
return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'.");
}
var userId = await userManager.GetUserIdAsync(user).ConfigureAwait(false);
downloadLogger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId);
// Only include personal data for download
var personalData = new Dictionary<string, string>();
var personalDataProps = typeof(ApplicationUser).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
foreach (var p in personalDataProps)
{
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
}
var logins = await userManager.GetLoginsAsync(user).ConfigureAwait(false);
foreach (var l in logins)
{
personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey);
}
personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user).ConfigureAwait(false))!);
var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData);
context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json");
return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json");
});
return accountGroup;
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using LudikZoneBlazor.Data;
namespace LudikZoneBlazor.Components.Account;
#pragma warning disable CA1812 // Elle est instanciée en Injection de dependance
/// <summary> Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation. </summary>
internal sealed class IdentityNoOpEmailSender : IEmailSender<ApplicationUser>
{
private readonly NoOpEmailSender emailSender = new NoOpEmailSender();
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
}
#pragma warning restore CA1812

View File

@@ -0,0 +1,62 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
namespace LudikZoneBlazor.Components.Account;
#pragma warning disable CA1812 // Elle est instanciée en Injection de dependance
internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
{
public const string StatusCookieName = "Identity.StatusMessage";
private static readonly CookieBuilder StatusCookieBuilder = new()
{
SameSite = SameSiteMode.Strict,
HttpOnly = true,
IsEssential = true,
MaxAge = TimeSpan.FromSeconds(5),
};
[DoesNotReturn]
public void RedirectTo(string? uri)
{
uri ??= "";
// Prevent open redirects.
if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
{
uri = navigationManager.ToBaseRelativePath(uri);
}
// During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect.
// So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown.
navigationManager.NavigateTo(uri);
throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering.");
}
[DoesNotReturn]
public void RedirectTo(string uri, Dictionary<string, object?> queryParameters)
{
var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path);
var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters);
RedirectTo(newUri);
}
[DoesNotReturn]
public void RedirectToWithStatus(string uri, string message, HttpContext context)
{
context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context));
RedirectTo(uri);
}
private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path);
[DoesNotReturn]
public void RedirectToCurrentPage() => RedirectTo(CurrentPath);
[DoesNotReturn]
public void RedirectToCurrentPageWithStatus(string message, HttpContext context)
=> RedirectToWithStatus(CurrentPath, message, context);
}
#pragma warning restore CA1812

View File

@@ -0,0 +1,51 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using LudikZoneBlazor.Data;
namespace LudikZoneBlazor.Components.Account;
#pragma warning disable CA1812 // Elle est instanciée en Injection de dependance
// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
// every 30 minutes an interactive circuit is connected.
internal sealed class IdentityRevalidatingAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> options)
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
{
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User).ConfigureAwait(false);
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<ApplicationUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal).ConfigureAwait(false);
if (user is null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user).ConfigureAwait(false);
return principalStamp == userStamp;
}
}
}
#pragma warning restore CA1812

View File

@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Identity;
using LudikZoneBlazor.Data;
namespace LudikZoneBlazor.Components.Account;
#pragma warning disable CA1812 // Elle est instanciée en Injection de dependance
internal sealed class IdentityUserAccessor(UserManager<ApplicationUser> userManager, IdentityRedirectManager redirectManager)
{
public async Task<ApplicationUser> GetRequiredUserAsync(HttpContext context)
{
var user = await userManager.GetUserAsync(context.User).ConfigureAwait(false);
if (user is null)
{
redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context);
}
return user;
}
}
#pragma warning restore CA1812

View File

@@ -0,0 +1,5 @@
@page "/Account/AccessDenied"
<PageTitle>Access denied</PageTitle>
<MudAlert Severity="Severity.Error">You do not have access to this resource.</MudAlert>

View File

@@ -0,0 +1,48 @@
@page "/Account/ConfirmEmail"
@using System.Text
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Confirm email</PageTitle>
<h1>Confirm email</h1>
<StatusMessage Message="@statusMessage" />
@code {
private string? statusMessage;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromQuery]
private string? UserId { get; set; }
[SupplyParameterFromQuery]
private string? Code { get; set; }
protected override async Task OnInitializedAsync()
{
if (UserId is null || Code is null)
{
RedirectManager.RedirectTo("");
}
var user = await UserManager.FindByIdAsync(UserId);
if (user is null)
{
HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
statusMessage = $"Error loading user with ID {UserId}";
}
else
{
var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
var result = await UserManager.ConfirmEmailAsync(user, code);
statusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
}
}
}

View File

@@ -0,0 +1,68 @@
@page "/Account/ConfirmEmailChange"
@using System.Text
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Confirm email change</PageTitle>
<h1>Confirm email change</h1>
<StatusMessage Message="@message" />
@code {
private string? message;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromQuery]
private string? UserId { get; set; }
[SupplyParameterFromQuery]
private string? Email { get; set; }
[SupplyParameterFromQuery]
private string? Code { get; set; }
protected override async Task OnInitializedAsync()
{
if (UserId is null || Email is null || Code is null)
{
RedirectManager.RedirectToWithStatus(
"Account/Login", "Error: Invalid email change confirmation link.", HttpContext);
}
var user = await UserManager.FindByIdAsync(UserId);
if (user is null)
{
message = "Unable to find user with Id '{userId}'";
return;
}
var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
var result = await UserManager.ChangeEmailAsync(user, Email, code);
if (!result.Succeeded)
{
message = "Error changing email.";
return;
}
// In our UI email and user name are one and the same, so when we update the email
// we need to update the user name.
var setUserNameResult = await UserManager.SetUserNameAsync(user, Email);
if (!setUserNameResult.Succeeded)
{
message = "Error changing user name.";
return;
}
await SignInManager.RefreshSignInAsync(user);
message = "Thank you for confirming your email change.";
}
}

View File

@@ -0,0 +1,195 @@
@page "/Account/ExternalLogin"
@using System.ComponentModel.DataAnnotations
@using System.Security.Claims
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using LudikZoneBlazor.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ExternalLogin> Logger
<PageTitle>Register</PageTitle>
<StatusMessage Message="@message" />
<h1>Register</h1>
<h2>Associate your @ProviderDisplayName account.</h2>
<MudDivider />
<div class="alert alert-info">
You've successfully authenticated with <strong>@ProviderDisplayName</strong>.
Please enter an email address for this site below and click the Register button to finish
logging in.
</div>
<div class="row">
<div class="col-md-4">
<EditForm Model="Input" OnValidSubmit="OnValidSubmitAsync" FormName="confirmation" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" class="form-control" autocomplete="email" placeholder="Please enter your email." />
<label for="email" class="form-label">Email</label>
<ValidationMessage For="() => Input.Email" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
</EditForm>
</div>
</div>
@code {
public const string LoginCallbackAction = "LoginCallback";
private string? message;
private ExternalLoginInfo externalLoginInfo = default!;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? RemoteError { get; set; }
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
[SupplyParameterFromQuery]
private string? Action { get; set; }
private string? ProviderDisplayName => externalLoginInfo.ProviderDisplayName;
protected override async Task OnInitializedAsync()
{
if (RemoteError is not null)
{
RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext);
}
var info = await SignInManager.GetExternalLoginInfoAsync();
if (info is null)
{
RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
}
externalLoginInfo = info;
if (HttpMethods.IsGet(HttpContext.Request.Method))
{
if (Action == LoginCallbackAction)
{
await OnLoginCallbackAsync();
return;
}
// We should only reach this page via the login callback, so redirect back to
// the login page if we get here some other way.
RedirectManager.RedirectTo("Account/Login");
}
}
private async Task OnLoginCallbackAsync()
{
// Sign in the user with this external login provider if the user already has a login.
var result = await SignInManager.ExternalLoginSignInAsync(
externalLoginInfo.LoginProvider,
externalLoginInfo.ProviderKey,
isPersistent: false,
bypassTwoFactor: true);
if (result.Succeeded)
{
Logger.LogInformation(
"{Name} logged in with {LoginProvider} provider.",
externalLoginInfo.Principal.Identity?.Name,
externalLoginInfo.LoginProvider);
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.IsLockedOut)
{
RedirectManager.RedirectTo("Account/Lockout");
}
// If the user does not have an account, then ask the user to create an account.
if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
{
Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? "";
}
}
private async Task OnValidSubmitAsync()
{
var emailStore = GetEmailStore();
var user = CreateUser();
await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user, externalLoginInfo);
if (result.Succeeded)
{
Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider);
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
// If account confirmation is required, we need to show the link if we don't have a real email sender
if (UserManager.Options.SignIn.RequireConfirmedAccount)
{
RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email });
}
await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider);
RedirectManager.RedirectTo(ReturnUrl);
}
}
message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}";
}
private ApplicationUser CreateUser()
{
try
{
return Activator.CreateInstance<ApplicationUser>();
}
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
}
}
private IUserEmailStore<ApplicationUser> GetEmailStore()
{
if (!UserManager.SupportsUserEmail)
{
throw new NotSupportedException("The default UI requires a user store with email support.");
}
return (IUserEmailStore<ApplicationUser>)UserStore;
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
}
}

View File

@@ -0,0 +1,68 @@
@page "/Account/ForgotPassword"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Forgot your password?</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">Forgot your password?</MudText>
<MudText Typo="Typo.body1" GutterBottom="true">Enter your email.</MudText>
<EditForm Model="Input" FormName="forgot-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<MudGrid>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.Email)" @bind-Value="Input.Email"
Label="Email" Placeholder="name@example.com"
UserAttributes="@(new() { { "autocomplete", "username" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Reset password</MudStaticButton>
</MudItem>
</MudGrid>
</EditForm>
@code {
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
private async Task OnValidSubmitAsync()
{
var user = await UserManager.FindByEmailAsync(Input.Email);
if (user is null || !(await UserManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");
}
// For more information on how to enable account confirmation and password reset please
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await UserManager.GeneratePasswordResetTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri,
new Dictionary<string, object?> { ["code"] = code });
await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
}
}

View File

@@ -0,0 +1,7 @@
@page "/Account/ForgotPasswordConfirmation"
<PageTitle>Forgot password confirmation</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">Forgot password confirmation</MudText>
<MudText Typo="Typo.body1" GutterBottom="true">Please check your email to reset your password.</MudText>

View File

@@ -0,0 +1,8 @@
@page "/Account/InvalidPasswordReset"
<PageTitle>Invalid password reset</PageTitle>
<h1>Invalid password reset</h1>
<p>
The password reset link is invalid.
</p>

View File

@@ -0,0 +1,7 @@
@page "/Account/InvalidUser"
<PageTitle>Invalid user</PageTitle>
<h3>Invalid user</h3>
<StatusMessage />

View File

@@ -0,0 +1,8 @@
@page "/Account/Lockout"
<PageTitle>Locked out</PageTitle>
<header>
<h1 class="text-danger">Locked out</h1>
<p class="text-danger">This account has been locked out, please try again later.</p>
</header>

View File

@@ -0,0 +1,180 @@
@page "/Account/Login"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject ILogger<Login> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Log in</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">Log in</MudText>
<MudGrid>
<MudItem md="6">
<StatusMessage Message="@errorMessage" />
<EditForm Model="Input" method="post" OnValidSubmit="LoginUser" FormName="login">
<DataAnnotationsValidator />
<MudText GutterBottom="true" Typo="Typo.body1">Use a local account to log in.</MudText>
<MudGrid>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.Email)" @bind-Value="Input.Email"
Label="Email" Placeholder="name@example.com"
UserAttributes="@(new() { { "autocomplete", "true" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.Password)" @bind-Value="Input.Password"
Label="Password" InputType="InputType.Password" Placeholder="password"
UserAttributes="@(new() { { "autocomplete", "current-password" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticCheckBox For="@(() => Input.RememberMe)" @bind-Value="Input.RememberMe">Remember me</MudStaticCheckBox>
</MudItem>
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Log in</MudStaticButton>
</MudItem>
</MudGrid>
</EditForm>
<MudGrid Class="mt-4">
<MudItem md="12">
<MudLink Href="Account/ForgotPassword">Forgot your password?</MudLink><br />
<MudLink Href="@(NavigationManager.GetUriWithQueryParameters("Account/Register", new Dictionary<string, object?> { ["ReturnUrl"] = ReturnUrl }))">Register as a new user</MudLink><br />
<MudLink Href="Account/ResendEmailConfirmation">Resend email confirmation</MudLink>
</MudItem>
</MudGrid>
</MudItem>
<MudItem md="6">
<MudText GutterBottom="true" Typo="Typo.body1">Use another service to log in.</MudText>
<ExternalLoginPicker />
</MudItem>
</MudGrid>
@code {
private string? errorMessage;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
if (HttpMethods.IsGet(HttpContext.Request.Method))
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
}
}
public async Task LoginUser()
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
Logger.LogInformation("User logged in.");
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.RequiresTwoFactor)
{
RedirectManager.RedirectTo(
"Account/LoginWith2fa",
new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
}
else if (result.IsLockedOut)
{
Logger.LogWarning("User account locked out.");
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
errorMessage = "Error: Invalid login attempt.";
}
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
[Required]
[DataType(DataType.Password)]
public string Password { get; set; } = "";
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
}

View File

@@ -0,0 +1,101 @@
@page "/Account/LoginWith2fa"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<LoginWith2fa> Logger
<PageTitle>Two-factor authentication</PageTitle>
<h1>Two-factor authentication</h1>
<MudDivider />
<StatusMessage Message="@message" />
<p>Your login is protected with an authenticator app. Enter your authenticator code below.</p>
<div class="row">
<div class="col-md-4">
<EditForm Model="Input" FormName="login-with-2fa" OnValidSubmit="OnValidSubmitAsync" method="post">
<input type="hidden" name="ReturnUrl" value="@ReturnUrl" />
<input type="hidden" name="RememberMe" value="@RememberMe" />
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.TwoFactorCode" class="form-control" autocomplete="off" />
<label for="two-factor-code" class="form-label">Authenticator code</label>
<ValidationMessage For="() => Input.TwoFactorCode" class="text-danger" />
</div>
<div class="checkbox mb-3">
<label for="remember-machine" class="form-label">
<InputCheckbox @bind-Value="Input.RememberMachine" />
Remember this machine
</label>
</div>
<div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
</EditForm>
</div>
</div>
<p>
Don't have access to your authenticator device? You can
<a class="mud-link mud-primary-text mud-link-underline-hover" href="Account/LoginWithRecoveryCode?ReturnUrl=@ReturnUrl">log in with a recovery code</a>.
</p>
@code {
private string? message;
private ApplicationUser user = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
[SupplyParameterFromQuery]
private bool RememberMe { get; set; }
protected override async Task OnInitializedAsync()
{
// Ensure the user has gone through the username & password screen first
user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
throw new InvalidOperationException("Unable to load two-factor authentication user.");
}
private async Task OnValidSubmitAsync()
{
var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty);
var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine);
var userId = await UserManager.GetUserIdAsync(user);
if (result.Succeeded)
{
Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId);
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.IsLockedOut)
{
Logger.LogWarning("User with ID '{UserId}' account locked out.", userId);
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId);
message = "Error: Invalid authenticator code.";
}
}
private sealed class InputModel
{
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Authenticator code")]
public string? TwoFactorCode { get; set; }
[Display(Name = "Remember this machine")]
public bool RememberMachine { get; set; }
}
}

View File

@@ -0,0 +1,132 @@
@page "/Account/LoginWithRecoveryCode"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<LoginWithRecoveryCode> Logger
<PageTitle>Recovery code verification</PageTitle>
<h1>Recovery code verification</h1>
<MudDivider />
<StatusMessage Message="@message" />
<p>
You have requested to log in with a recovery code. This login will not be remembered until you provide
an authenticator app code at log in or disable 2FA and log in again.
</p>
<div class="row">
<div class="col-md-4">
<EditForm Model="Input" FormName="login-with-recovery-code" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.RecoveryCode" class="form-control" autocomplete="off" placeholder="RecoveryCode" />
<label for="recovery-code" class="form-label">Recovery Code</label>
<ValidationMessage For="() => Input.RecoveryCode" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</EditForm>
</div>
</div>
@code {
private string? message;
private ApplicationUser user = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
// Ensure the user has gone through the username & password screen first
user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
throw new InvalidOperationException("Unable to load two-factor authentication user.");
}
private async Task OnValidSubmitAsync()
{
var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty);
var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
var userId = await UserManager.GetUserIdAsync(user);
if (result.Succeeded)
{
Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId);
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.IsLockedOut)
{
Logger.LogWarning("User account locked out.");
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId);
message = "Error: Invalid recovery code entered.";
}
}
private sealed class InputModel
{
[Required]
[DataType(DataType.Text)]
[Display(Name = "Recovery Code")]
public string RecoveryCode { get; set; } = "";
}
}

View File

@@ -0,0 +1,98 @@
@page "/Account/Manage/ChangePassword"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ChangePassword> Logger
<PageTitle>Change password</PageTitle>
<MudText Typo="Typo.h6" GutterBottom="true">Change password</MudText>
<StatusMessage Message="@message" />
<EditForm Model="Input" FormName="change-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<MudGrid>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.OldPassword)" @bind-Value="Input.OldPassword" InputType="InputType.Password"
Label="Old Password" Placeholder="old password" HelperText="Please enter your old password."
UserAttributes="@(new() { { "autocomplete", "current-password" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.NewPassword)" @bind-Value="Input.NewPassword" InputType="InputType.Password"
Label="New Password" Placeholder="new password" HelperText="Please enter your new password."
UserAttributes="@(new() { { "autocomplete", "new-password" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.ConfirmPassword)" @bind-Value="Input.ConfirmPassword" InputType="InputType.Password"
Label="Confirm Password" Placeholder="confirm password" HelperText="Please confirm your new password."
UserAttributes="@(new() { { "autocomplete", "new-password" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Update password</MudStaticButton>
</MudItem>
</MudGrid>
</EditForm>
@code {
private string? message;
private ApplicationUser user = default!;
private bool hasPassword;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
hasPassword = await UserManager.HasPasswordAsync(user);
if (!hasPassword)
{
RedirectManager.RedirectTo("Account/Manage/SetPassword");
}
}
private async Task OnValidSubmitAsync()
{
var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
if (!changePasswordResult.Succeeded)
{
message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}";
return;
}
await SignInManager.RefreshSignInAsync(user);
Logger.LogInformation("User changed their password successfully.");
RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed", HttpContext);
}
private sealed class InputModel
{
[Required]
[DataType(DataType.Password)]
[Display(Name = "Current password")]
public string OldPassword { get; set; } = "";
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; } = "";
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = "";
}
}

View File

@@ -0,0 +1,86 @@
@page "/Account/Manage/DeletePersonalData"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<DeletePersonalData> Logger
<PageTitle>Delete Personal Data</PageTitle>
<MudText Typo="Typo.h6" GutterBottom="true">Delete personal data</MudText>
<StatusMessage Message="@message" />
<MudAlert Severity="Severity.Error" Variant="Variant.Text">
Deleting this data will permanently remove your account, and this cannot be recovered.
</MudAlert>
<EditForm Model="Input" FormName="delete-user" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<MudGrid>
@if (requirePassword)
{
<MudItem md="12">
<MudStaticTextField For="@(() => Input.Password)" @bind-Value="Input.Password" InputType="InputType.Password"
Label="Password" Placeholder="password" HelperText="Please enter your new password."
UserAttributes="@(new() { { "autocomplete", "current-password" }, { "aria-required", "true" } } )" />
</MudItem>
}
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Delete data and close my account</MudStaticButton>
</MudItem>
</MudGrid>
</EditForm>
@code {
private string? message;
private ApplicationUser user = default!;
private bool requirePassword;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
Input ??= new();
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
requirePassword = await UserManager.HasPasswordAsync(user);
}
private async Task OnValidSubmitAsync()
{
if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password))
{
message = "Error: Incorrect password.";
return;
}
var result = await UserManager.DeleteAsync(user);
if (!result.Succeeded)
{
throw new InvalidOperationException("Unexpected error occurred deleting user.");
}
await SignInManager.SignOutAsync();
var userId = await UserManager.GetUserIdAsync(user);
Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
RedirectManager.RedirectToCurrentPage();
}
private sealed class InputModel
{
[DataType(DataType.Password)]
public string Password { get; set; } = "";
}
}

View File

@@ -0,0 +1,64 @@
@page "/Account/Manage/Disable2fa"
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<Disable2fa> Logger
<PageTitle>Disable two-factor authentication (2FA)</PageTitle>
<StatusMessage />
<h3>Disable two-factor authentication (2FA)</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>This action only disables 2FA.</strong>
</p>
<p>
Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
used in an authenticator app you should <a href="Account/Manage/ResetAuthenticator">reset your authenticator keys.</a>
</p>
</div>
<div>
<form @formname="disable-2fa" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken />
<button class="btn btn-danger" type="submit">Disable 2FA</button>
</form>
</div>
@code {
private ApplicationUser user = default!;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
if (HttpMethods.IsGet(HttpContext.Request.Method) && !await UserManager.GetTwoFactorEnabledAsync(user))
{
throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled.");
}
}
private async Task OnSubmitAsync()
{
var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new InvalidOperationException("Unexpected error occurred disabling 2FA.");
}
var userId = await UserManager.GetUserIdAsync(user);
Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId);
RedirectManager.RedirectToWithStatus(
"Account/Manage/TwoFactorAuthentication",
"2fa has been disabled. You can reenable 2fa when you setup an authenticator app",
HttpContext);
}
}

View File

@@ -0,0 +1,122 @@
@page "/Account/Manage/Email"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject IdentityUserAccessor UserAccessor
@inject NavigationManager NavigationManager
<PageTitle>Manage email</PageTitle>
<MudText Typo="Typo.h6" GutterBottom="true">Manage email</MudText>
<StatusMessage Message="@message" />
<form @onsubmit="OnSendEmailVerificationAsync" @formname="send-verification" id="send-verification-form" method="post">
<AntiforgeryToken />
</form>
<EditForm Model="Input" FormName="change-email" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<MudGrid>
@if (isEmailConfirmed)
{
<MudItem md="12">
<MudStaticTextField Value="@email" Label="Email" Placeholder="Please enter your email." Disabled="true" AdornmentIcon="Icons.Material.Filled.Check" AdornmentColor="Color.Success" />
</MudItem>
}
else
{
<MudItem md="12">
<MudStaticTextField Value="@email" Label="Email" Placeholder="Please enter your email." Disabled="true" />
</MudItem>
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Send verification email</MudStaticButton>
</MudItem>
}
<MudItem md="12">
<MudStaticTextField @bind-Value="@Input.NewEmail" For="@(() => Input.NewEmail)" Label="New Email" HelperText="Please enter new email." />
</MudItem>
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Change email</MudStaticButton>
</MudItem>
</MudGrid>
</EditForm>
@code {
private string? message;
private ApplicationUser user = default!;
private string? email;
private bool isEmailConfirmed;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "change-email")]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
email = await UserManager.GetEmailAsync(user);
isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(user);
Input.NewEmail ??= email;
}
private async Task OnValidSubmitAsync()
{
if (Input.NewEmail is null || Input.NewEmail == email)
{
message = "Your email is unchanged.";
return;
}
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl));
message = "Confirmation link to change email sent. Please check your email.";
}
private async Task OnSendEmailVerificationAsync()
{
if (email is null)
{
return;
}
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(callbackUrl));
message = "Verification email sent. Please check your email.";
}
private sealed class InputModel
{
[Required]
[EmailAddress]
[Display(Name = "New email")]
public string? NewEmail { get; set; }
}
}

View File

@@ -0,0 +1,178 @@
@page "/Account/Manage/EnableAuthenticator"
@using System.ComponentModel.DataAnnotations
@using System.Globalization
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject IdentityUserAccessor UserAccessor
@inject UrlEncoder UrlEncoder
@inject IdentityRedirectManager RedirectManager
@inject ILogger<EnableAuthenticator> Logger
<PageTitle>Configure authenticator app</PageTitle>
@if (recoveryCodes is not null)
{
<ShowRecoveryCodes RecoveryCodes="recoveryCodes.ToArray()" StatusMessage="@message" />
}
else
{
<MudText Typo="Typo.h6" GutterBottom="true">Configure authenticator app</MudText>
<StatusMessage Message="@message" />
<MudText Typo="Typo.body1" GutterBottom="true">To use an authenticator app go through the following steps:</MudText>
<ol class="list">
<li>
<MudText Typo="Typo.body2">
Download a two-factor authenticator app like Microsoft Authenticator for
<MudLink Target="_blank" Href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</MudLink> and
<MudLink Target="_blank" Href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</MudLink> or
Google Authenticator for
<MudLink Target="_blank" Href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&amp;hl=en">Android</MudLink> and
<MudLink Target="_blank" Href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</MudLink>.
</MudText>
</li>
<li>
<MudText Typo="Typo.body2">
Scan the QR Code or enter this key into your two factor authenticator app. Spaces and casing do not matter:
</MudText>
<MudAlert Variant="Variant.Text" Severity="Severity.Info" Icon="@Icons.Material.Filled.Key">@sharedKey</MudAlert>
<MudText Typo="Typo.body2">
Learn how to <MudLink Target="_blank" Href="https://go.microsoft.com/fwlink/?Linkid=852423">enable QR code generation</MudLink>.
</MudText>
<div data-url="@authenticatorUri"></div>
</li>
<li>
<MudText Typo="Typo.body2">
Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
with a unique code. Enter the code in the confirmation box below.
</MudText>
<EditForm Model="Input" FormName="send-code" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<MudGrid>
<MudItem md="12">
<MudStaticTextField @bind-Value="@Input.Code" For="@(() => Input.Code)" Label="Verification Code" HelperText="Please enter the code." />
</MudItem>
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Verify</MudStaticButton>
</MudItem>
</MudGrid>
</EditForm>
</li>
</ol>
}
@code {
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
private string? message;
private ApplicationUser user = default!;
private string? sharedKey;
private string? authenticatorUri;
private IEnumerable<string>? recoveryCodes;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
await LoadSharedKeyAndQrCodeUriAsync(user);
}
private async Task OnValidSubmitAsync()
{
// Strip spaces and hyphens
var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync(
user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
message = "Error: Verification code is invalid.";
return;
}
await UserManager.SetTwoFactorEnabledAsync(user, true);
var userId = await UserManager.GetUserIdAsync(user);
Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId);
message = "Your authenticator app has been verified.";
if (await UserManager.CountRecoveryCodesAsync(user) == 0)
{
recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
}
else
{
RedirectManager.RedirectToWithStatus("Account/Manage/TwoFactorAuthentication", message, HttpContext);
}
}
private async ValueTask LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user)
{
// Load the authenticator key & QR code URI to display on the form
var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await UserManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
}
sharedKey = FormatKey(unformattedKey!);
var email = await UserManager.GetEmailAsync(user);
authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!);
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' ');
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.AsSpan(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
CultureInfo.InvariantCulture,
AuthenticatorUriFormat,
UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
UrlEncoder.Encode(email),
unformattedKey);
}
private sealed class InputModel
{
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Verification Code")]
public string Code { get; set; } = "";
}
}

View File

@@ -0,0 +1,140 @@
@page "/Account/Manage/ExternalLogins"
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IUserStore<ApplicationUser> UserStore
@inject IdentityRedirectManager RedirectManager
<PageTitle>Manage your external logins</PageTitle>
<StatusMessage />
@if (currentLogins?.Count > 0)
{
<h3>Registered Logins</h3>
<table class="table">
<tbody>
@foreach (var login in currentLogins)
{
<tr>
<td>@login.ProviderDisplayName</td>
<td>
@if (showRemoveButton)
{
<form @formname="@($"remove-login-{login.LoginProvider}")" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken />
<div>
<input type="hidden" name="@nameof(LoginProvider)" value="@login.LoginProvider" />
<input type="hidden" name="@nameof(ProviderKey)" value="@login.ProviderKey" />
<button type="submit" class="btn btn-primary" title="Remove this @login.ProviderDisplayName login from your account">Remove</button>
</div>
</form>
}
else
{
@: &nbsp;
}
</td>
</tr>
}
</tbody>
</table>
}
@if (otherLogins?.Count > 0)
{
<h4>Add another service to log in.</h4>
<MudDivider />
<form class="form-horizontal" action="Account/Manage/LinkExternalLogin" method="post">
<AntiforgeryToken />
<div>
<p>
@foreach (var provider in otherLogins)
{
<button type="submit" class="btn btn-primary" name="Provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">
@provider.DisplayName
</button>
}
</p>
</div>
</form>
}
@code {
public const string LinkLoginCallbackAction = "LinkLoginCallback";
private ApplicationUser user = default!;
private IList<UserLoginInfo>? currentLogins;
private IList<AuthenticationScheme>? otherLogins;
private bool showRemoveButton;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private string? LoginProvider { get; set; }
[SupplyParameterFromForm]
private string? ProviderKey { get; set; }
[SupplyParameterFromQuery]
private string? Action { get; set; }
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
currentLogins = await UserManager.GetLoginsAsync(user);
otherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync())
.Where(auth => currentLogins.All(ul => auth.Name != ul.LoginProvider))
.ToList();
string? passwordHash = null;
if (UserStore is IUserPasswordStore<ApplicationUser> userPasswordStore)
{
passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted);
}
showRemoveButton = passwordHash is not null || currentLogins.Count > 1;
if (HttpMethods.IsGet(HttpContext.Request.Method) && Action == LinkLoginCallbackAction)
{
await OnGetLinkLoginCallbackAsync();
}
}
private async Task OnSubmitAsync()
{
var result = await UserManager.RemoveLoginAsync(user, LoginProvider!, ProviderKey!);
if (!result.Succeeded)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not removed.", HttpContext);
}
await SignInManager.RefreshSignInAsync(user);
RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed.", HttpContext);
}
private async Task OnGetLinkLoginCallbackAsync()
{
var userId = await UserManager.GetUserIdAsync(user);
var info = await SignInManager.GetExternalLoginInfoAsync(userId);
if (info is null)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: Could not load external login info.", HttpContext);
}
var result = await UserManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not added. External logins can only be associated with one account.", HttpContext);
}
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
RedirectManager.RedirectToCurrentPageWithStatus("The external login was added.", HttpContext);
}
}

View File

@@ -0,0 +1,68 @@
@page "/Account/Manage/GenerateRecoveryCodes"
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<GenerateRecoveryCodes> Logger
<PageTitle>Generate two-factor authentication (2FA) recovery codes</PageTitle>
@if (recoveryCodes is not null)
{
<ShowRecoveryCodes RecoveryCodes="recoveryCodes.ToArray()" StatusMessage="@message" />
}
else
{
<h3>Generate two-factor authentication (2FA) recovery codes</h3>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<strong>Put these codes in a safe place.</strong>
</p>
<p>
If you lose your device and don't have the recovery codes you will lose access to your account.
</p>
<p>
Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
used in an authenticator app you should <a href="Account/Manage/ResetAuthenticator">reset your authenticator keys.</a>
</p>
</div>
<div>
<form @formname="generate-recovery-codes" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken />
<button class="btn btn-danger" type="submit">Generate Recovery Codes</button>
</form>
</div>
}
@code {
private string? message;
private ApplicationUser user = default!;
private IEnumerable<string>? recoveryCodes;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user);
if (!isTwoFactorEnabled)
{
throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled.");
}
}
private async Task OnSubmitAsync()
{
var userId = await UserManager.GetUserIdAsync(user);
recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
message = "You have generated new recovery codes.";
Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
}
}

View File

@@ -0,0 +1,77 @@
@page "/Account/Manage"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
<PageTitle>Profile</PageTitle>
<MudText Typo="Typo.h6" GutterBottom="true">Profile</MudText>
<StatusMessage />
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<MudGrid>
<MudItem md="12">
<MudStaticTextField Value="@username" Label="Username" Disabled="true" Placeholder="Please choose your username." />
</MudItem>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.PhoneNumber)" @bind-Value="Input.PhoneNumber"
Label="Phone Number" HelperText="Please enter your phone number."
UserAttributes="@(new() { { "autocomplete", "tel-national" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Save</MudStaticButton>
</MudItem>
</MudGrid>
</EditForm>
@code {
private ApplicationUser user = default!;
private string? username;
private string? phoneNumber;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
username = await UserManager.GetUserNameAsync(user);
phoneNumber = await UserManager.GetPhoneNumberAsync(user);
Input.PhoneNumber ??= phoneNumber;
}
private async Task OnValidSubmitAsync()
{
if (Input.PhoneNumber != phoneNumber)
{
var setPhoneResult = await UserManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
if (!setPhoneResult.Succeeded)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to set phone number.", HttpContext);
}
}
await SignInManager.RefreshSignInAsync(user);
RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext);
}
private sealed class InputModel
{
[Phone]
[Display(Name = "Phone number")]
public string? PhoneNumber { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
@page "/Account/Manage/PersonalData"
@inject IdentityUserAccessor UserAccessor
<PageTitle>Personal Data</PageTitle>
<MudText Typo="Typo.h6" GutterBottom="true">Personal data</MudText>
<StatusMessage />
<MudGrid>
<MudItem md="12">
<MudText Typo="Typo.body1">
Your account contains personal data that you have given us. This page allows you to download or delete that data.
</MudText>
</MudItem>
<MudItem md="12">
<MudAlert Severity="Severity.Warning" Variant="Variant.Text">
Deleting this data will permanently remove your account, and this cannot be recovered.
</MudAlert>
</MudItem>
<MudItem md="12">
<form action="Account/Manage/DownloadPersonalData" method="post">
<AntiforgeryToken />
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Download</MudStaticButton>
</form>
</MudItem>
<MudItem md="12">
<MudLink Href="Account/Manage/DeletePersonalData" Color="Color.Error">Delete</MudLink>
</MudItem>
</MudGrid>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
_ = await UserAccessor.GetRequiredUserAsync(HttpContext);
}
}

View File

@@ -0,0 +1,52 @@
@page "/Account/Manage/ResetAuthenticator"
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ResetAuthenticator> Logger
<PageTitle>Reset authenticator key</PageTitle>
<MudText Typo="Typo.h6" GutterBottom="true">Reset authenticator key</MudText>
<StatusMessage />
<MudAlert Severity="Severity.Warning" Variant="Variant.Text">
If you reset your authenticator key your authenticator app will not work until you reconfigure it.
</MudAlert>
<MudText Typo="Typo.body2" Class="my-4">
This process disables 2FA until you verify your authenticator app.
If you do not complete your authenticator app configuration you may lose access to your account.
</MudText>
<form @formname="reset-authenticator" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken />
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Reset authenticator key</MudStaticButton>
</form>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private async Task OnSubmitAsync()
{
var user = await UserAccessor.GetRequiredUserAsync(HttpContext);
await UserManager.SetTwoFactorEnabledAsync(user, false);
await UserManager.ResetAuthenticatorKeyAsync(user);
var userId = await UserManager.GetUserIdAsync(user);
Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId);
await SignInManager.RefreshSignInAsync(user);
RedirectManager.RedirectToWithStatus(
"Account/Manage/EnableAuthenticator",
"Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.",
HttpContext);
}
}

View File

@@ -0,0 +1,87 @@
@page "/Account/Manage/SetPassword"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
<PageTitle>Set password</PageTitle>
<h3>Set your password</h3>
<StatusMessage Message="@message" />
<p class="text-info">
You do not have a local username/password for this site. Add a local
account so you can log in without an external login.
</p>
<div class="row">
<div class="col-md-6">
<EditForm Model="Input" FormName="set-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.NewPassword" class="form-control" autocomplete="new-password" placeholder="Please enter your new password." />
<label for="new-password" class="form-label">New password</label>
<ValidationMessage For="() => Input.NewPassword" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.ConfirmPassword" class="form-control" autocomplete="new-password" placeholder="Please confirm your new password." />
<label for="confirm-password" class="form-label">Confirm password</label>
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Set password</button>
</EditForm>
</div>
</div>
@code {
private string? message;
private ApplicationUser user = default!;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
var hasPassword = await UserManager.HasPasswordAsync(user);
if (hasPassword)
{
RedirectManager.RedirectTo("Account/Manage/ChangePassword");
}
}
private async Task OnValidSubmitAsync()
{
var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!);
if (!addPasswordResult.Succeeded)
{
message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}";
return;
}
await SignInManager.RefreshSignInAsync(user);
RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext);
}
private sealed class InputModel
{
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string? NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string? ConfirmPassword { get; set; }
}
}

View File

@@ -0,0 +1,110 @@
@page "/Account/Manage/TwoFactorAuthentication"
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
<PageTitle>Two-factor authentication (2FA)</PageTitle>
<MudText Typo="Typo.h6" GutterBottom="true">Two-factor authentication (2FA)</MudText>
<StatusMessage />
@if (canTrack)
{
if (is2faEnabled)
{
if (recoveryCodesLeft == 0)
{
<MudAlert Variant="Variant.Text" Severity="Severity.Error">You have no recovery codes left.</MudAlert>
<MudText Typo="Typo.body1" Class="pt-4">
You must <MudLink Href="Account/Manage/GenerateRecoveryCodes">generate a new set of recovery codes</MudLink>
before you can log in with a recovery code.
</MudText>
}
else if (recoveryCodesLeft == 1)
{
<MudAlert Variant="Variant.Text" Severity="Severity.Warning">You have 1 recovery code left.</MudAlert>
<MudText Typo="Typo.body1" Class="pt-4">
You can <MudLink Href="Account/Manage/GenerateRecoveryCodes">generate a new set of recovery codes</MudLink>.
</MudText>
}
else if (recoveryCodesLeft <= 3)
{
<MudAlert Variant="Variant.Text" Severity="Severity.Warning">You have @recoveryCodesLeft recovery codes left.</MudAlert>
<MudText Typo="Typo.body1" Class="pt-4">
You should <MudLink Href="Account/Manage/GenerateRecoveryCodes">generate a new set of recovery codes</MudLink>.
</MudText>
}
if (isMachineRemembered)
{
<form style="display: inline-block" @formname="forget-browser" @onsubmit="OnSubmitForgetBrowserAsync" method="post">
<AntiforgeryToken />
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Forget this browser</MudStaticButton>
</form>
}
<MudLink Href="Account/Manage/Disable2fa">Disable 2FA</MudLink><br />
<MudLink Href="Account/Manage/GenerateRecoveryCodes">Reset recovery codes</MudLink>
}
<MudText Typo="Typo.h6" GutterBottom="true">Authenticator app</MudText>
@if (!hasAuthenticator)
{
<MudLink Href="Account/Manage/EnableAuthenticator">Add authenticator app</MudLink><br />
}
else
{
<MudLink Href="Account/Manage/EnableAuthenticator">Set up authenticator app</MudLink><br />
<MudLink Href="Account/Manage/ResetAuthenticator">Reset authenticator app</MudLink>
}
}
else
{
<MudAlert Variant="Variant.Text" Severity="Severity.Error">Privacy and cookie policy have not been accepted.</MudAlert>
<MudText Typo="Typo.body1" Class="pt-4">
You must accept the policy before you can enable two factor authentication.
</MudText>
}
@code {
private bool canTrack;
private bool hasAuthenticator;
private int recoveryCodesLeft;
private bool is2faEnabled;
private bool isMachineRemembered;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
var user = await UserAccessor.GetRequiredUserAsync(HttpContext);
canTrack = HttpContext.Features.Get<ITrackingConsentFeature>()?.CanTrack ?? true;
hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null;
is2faEnabled = await UserManager.GetTwoFactorEnabledAsync(user);
isMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user);
recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user);
}
private async Task OnSubmitForgetBrowserAsync()
{
await SignInManager.ForgetTwoFactorClientAsync();
RedirectManager.RedirectToCurrentPageWithStatus(
"The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.",
HttpContext);
}
}

View File

@@ -0,0 +1,2 @@
@layout ManageLayout
@attribute [Microsoft.AspNetCore.Authorization.Authorize]

View File

@@ -0,0 +1,232 @@
@page "/Account/Register"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject SignInManager<ApplicationUser> SignInManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject ILogger<Register> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Register</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">Register</MudText>
<MudGrid>
<MudItem md="6">
<StatusMessage Message="@Message" />
<EditForm Model="Input" asp-route-returnUrl="@ReturnUrl" method="post" OnValidSubmit="RegisterUser" FormName="register">
<DataAnnotationsValidator />
<MudText Typo="Typo.body1" GutterBottom="true">Create a new account.</MudText>
<MudGrid>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.Email)" @bind-Value="Input.Email"
Label="Email" Placeholder="name@example.com"
UserAttributes="@(new() { { "autocomplete", "true" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.Password)" @bind-Value="Input.Password"
Label="Password" InputType="InputType.Password" Placeholder="password"
UserAttributes="@(new() { { "autocomplete", "new-password" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.ConfirmPassword)" @bind-Value="Input.ConfirmPassword"
Label="Confirm Password" InputType="InputType.Password" Placeholder="confirm password"
UserAttributes="@(new() { { "autocomplete", "new-password" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Register</MudStaticButton>
</MudItem>
</MudGrid>
</EditForm>
</MudItem>
<MudItem md="6">
<MudText Typo="Typo.body1" GutterBottom="true">Use another service to register.</MudText>
<ExternalLoginPicker />
</MudItem>
</MudGrid>
@code {
private IEnumerable<IdentityError>? identityErrors;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}";
public async Task RegisterUser(EditContext editContext)
{
var user = CreateUser();
await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
var emailStore = GetEmailStore();
await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await UserManager.CreateAsync(user, Input.Password);
if (!result.Succeeded)
{
identityErrors = result.Errors;
return;
}
Logger.LogInformation("User created a new account with password.");
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl });
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
if (UserManager.Options.SignIn.RequireConfirmedAccount)
{
RedirectManager.RedirectTo(
"Account/RegisterConfirmation",
new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl });
}
await SignInManager.SignInAsync(user, isPersistent: false);
RedirectManager.RedirectTo(ReturnUrl);
}
private ApplicationUser CreateUser()
{
try
{
return Activator.CreateInstance<ApplicationUser>();
}
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor.");
}
}
private IUserEmailStore<ApplicationUser> GetEmailStore()
{
if (!UserManager.SupportsUserEmail)
{
throw new NotSupportedException("The default UI requires a user store with email support.");
}
return (IUserEmailStore<ApplicationUser>)UserStore;
}
private sealed class InputModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; } = "";
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; } = "";
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = "";
}
}

View File

@@ -0,0 +1,106 @@
@page "/Account/RegisterConfirmation"
@using System.Text
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Register confirmation</PageTitle>
<h1>Register confirmation</h1>
<StatusMessage Message="@statusMessage" />
@if (emailConfirmationLink is not null)
{
<p>
This app does not currently have a real email sender registered, see <a class="mud-link mud-primary-text mud-link-underline-hover" href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender.
Normally this would be emailed: <a class="mud-link mud-primary-text mud-link-underline-hover" href="@emailConfirmationLink">Click here to confirm your account</a>
</p>
}
else
{
<p>Please check your email to confirm your account.</p>
}
@code {
private string? emailConfirmationLink;
private string? statusMessage;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromQuery]
private string? Email { get; set; }
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
if (Email is null)
{
RedirectManager.RedirectTo("");
}
var user = await UserManager.FindByEmailAsync(Email);
if (user is null)
{
HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
statusMessage = "Error finding user for unspecified email";
}
else if (EmailSender is IdentityNoOpEmailSender)
{
// Once you add a real email sender, you should remove this code that lets you confirm the account
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
emailConfirmationLink = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl });
}
}
}

View File

@@ -0,0 +1,100 @@
@page "/Account/ResendEmailConfirmation"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using LudikZoneBlazor.Data
@inject UserManager<ApplicationUser> UserManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Resend email confirmation</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">Resend email confirmation</MudText>
<MudText Typo="Typo.body1" GutterBottom="true">Enter your email.</MudText>
<StatusMessage Message="@message" />
<EditForm Model="Input" FormName="resend-email-confirmation" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<MudGrid>
<MudItem md="12">
<MudStaticTextField For="@(() => Input.Email)" @bind-Value="Input.Email"
Label="Email" Placeholder="name@example.com"
UserAttributes="@(new() { { "autocomplete", "true" }, { "aria-required", "true" } } )" />
</MudItem>
<MudItem md="12">
<MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Resend</MudStaticButton>
</MudItem>
</MudGrid>
</EditForm>
@code {
private string? message;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
private async Task OnValidSubmitAsync()
{
var user = await UserManager.FindByEmailAsync(Input.Email!);
if (user is null)
{
message = "Verification email sent. Please check your email.";
return;
}
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
message = "Verification email sent. Please check your email.";
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
}
}

View File

@@ -0,0 +1,159 @@
@page "/Account/ResetPassword"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using LudikZoneBlazor.Data
@inject IdentityRedirectManager RedirectManager
@inject UserManager<ApplicationUser> UserManager
<PageTitle>Reset password</PageTitle>
<h1>Reset password</h1>
<h2>Reset your password.</h2>
<MudDivider />
<div class="row">
<div class="col-md-4">
<StatusMessage Message="@Message" />
<EditForm Model="Input" FormName="reset-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<input type="hidden" name="Input.Code" value="@Input.Code" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label for="email" class="form-label">Email</label>
<ValidationMessage For="() => Input.Email" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your password." />
<label for="password" class="form-label">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please confirm your password." />
<label for="confirm-password" class="form-label">Confirm password</label>
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset</button>
</EditForm>
</div>
</div>
@code {
private IEnumerable<IdentityError>? identityErrors;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? Code { get; set; }
private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}";
protected override void OnInitialized()
{
if (Code is null)
{
RedirectManager.RedirectTo("Account/InvalidPasswordReset");
}
Input.Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
}
private async Task OnValidSubmitAsync()
{
var user = await UserManager.FindByEmailAsync(Input.Email);
if (user is null)
{
// Don't reveal that the user does not exist
RedirectManager.RedirectTo("Account/ResetPasswordConfirmation");
}
var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password);
if (result.Succeeded)
{
RedirectManager.RedirectTo("Account/ResetPasswordConfirmation");
}
identityErrors = result.Errors;
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string Password { get; set; } = "";
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = "";
[Required]
public string Code { get; set; } = "";
}
}

View File

@@ -0,0 +1,7 @@
@page "/Account/ResetPasswordConfirmation"
<PageTitle>Reset password confirmation</PageTitle>
<h1>Reset password confirmation</h1>
<p>
Your password has been reset. Please <a class="mud-link mud-primary-text mud-link-underline-hover" href="Account/Login">click here to log in</a>.
</p>

View File

@@ -0,0 +1,3 @@
@using MudBlazor
@using LudikZoneBlazor.Components.Account.Shared
@layout AccountLayout

View File

@@ -0,0 +1,43 @@
@inherits LayoutComponentBase
@layout LudikZoneBlazor.Components.Layout.MainLayout
@inject NavigationManager NavigationManager
@if (HttpContext is null)
{
<p>Loading...</p>
}
else
{
@Body
}
@code {
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
protected override void OnParametersSet()
{
if (HttpContext is null)
{
// If this code runs, we're currently rendering in interactive mode, so there is no HttpContext.
// The identity pages need to set cookies, so they require an HttpContext. To achieve this we
// must transition back from interactive mode to a server-rendered page.
NavigationManager.Refresh(forceReload: true);
}
}
}

View File

@@ -0,0 +1,44 @@
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using MudBlazor
@using LudikZoneBlazor.Data
@using MudBlazor.StaticInput
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityRedirectManager RedirectManager
@if (externalLogins.Length == 0)
{
<MudAlert Variant="Variant.Text" Severity="Severity.Warning">There are no external authentication services configured.</MudAlert>
<MudText Typo="Typo.body1" Class="pt-4">
See <MudLink Target="_blank" Href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</MudLink>
about setting up this ASP.NET application to support logging in via external services
</MudText>
}
else
{
<form class="form-horizontal" action="Account/PerformExternalLogin" method="post">
<div>
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="@ReturnUrl" />
<p>
@foreach (var provider in externalLogins)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</p>
</div>
</form>
}
@code {
private AuthenticationScheme[] externalLogins = [];
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray();
}
}

View File

@@ -0,0 +1,14 @@
@inherits LayoutComponentBase
@layout AccountLayout
<MudText Typo="Typo.h3" GutterBottom="true">Manage your account</MudText>
<MudGrid>
<MudItem md="5">
<MudText Typo="Typo.h6" GutterBottom="true">Change your account settings</MudText>
<ManageNavMenu />
</MudItem>
<MudItem md="7">
@Body
</MudItem>
</MudGrid>

View File

@@ -0,0 +1,25 @@
@using Microsoft.AspNetCore.Identity
@using LudikZoneBlazor.Data
@inject SignInManager<ApplicationUser> SignInManager
<MudNavMenu>
<MudNavLink Href="Account/Manage" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Person">Profile</MudNavLink>
<MudNavLink Href="Account/Manage/Email" Icon="@Icons.Material.Filled.Email">Email</MudNavLink>
<MudNavLink Href="Account/Manage/ChangePassword" Icon="@Icons.Material.Filled.Lock">Password</MudNavLink>
@if (hasExternalLogins)
{
<MudNavLink Href="Account/Manage/ExternalLogins" Icon="@Icons.Material.Filled.PhoneLocked">External logins</MudNavLink>
}
<MudNavLink Href="Account/Manage/TwoFactorAuthentication" Icon="@Icons.Material.Filled.LockClock">Two-factor authentication</MudNavLink>
<MudNavLink Href="Account/Manage/PersonalData" Icon="@Icons.Material.Filled.PersonRemove">Personal data</MudNavLink>
</MudNavMenu>
@code {
private bool hasExternalLogins;
protected override async Task OnInitializedAsync()
{
hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
}
}

View File

@@ -0,0 +1,8 @@
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
}
}

View File

@@ -0,0 +1,28 @@
<StatusMessage Message="@StatusMessage" />
<h3>Recovery codes</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>Put these codes in a safe place.</strong>
</p>
<p>
If you lose your device and don't have the recovery codes you will lose access to your account.
</p>
</div>
<div class="row">
<div class="col-md-12">
@foreach (var recoveryCode in RecoveryCodes)
{
<div>
<code class="recovery-code">@recoveryCode</code>
</div>
}
</div>
</div>
@code {
[Parameter]
public string[] RecoveryCodes { get; set; } = [];
[Parameter]
public string? StatusMessage { get; set; }
}

View File

@@ -0,0 +1,28 @@
@if (!string.IsNullOrEmpty(DisplayMessage))
{
var severity = DisplayMessage.StartsWith("Error") ? Severity.Error : Severity.Success;
<MudAlert Variant="Variant.Outlined" Severity="@severity">@DisplayMessage</MudAlert>
}
@code {
private string? messageFromCookie;
[Parameter]
public string? Message { get; set; }
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private string? DisplayMessage => Message ?? messageFromCookie;
protected override void OnInitialized()
{
messageFromCookie = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName];
if (messageFromCookie is not null)
{
HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName);
}
}
}

View File

@@ -8,13 +8,23 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link rel="icon" type="image/ico" href="favicon.ico" />
<HeadOutlet @rendermode="InteractiveServer" />
<HeadOutlet @rendermode="RenderModeForPage" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<Routes @rendermode="RenderModeForPage" />
<script src="_framework/blazor.web.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="_content/Extensions.MudBlazor.StaticInput/NavigationObserver.js"></script>
</body>
</html>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
? null
: InteractiveServer;
}

View File

@@ -0,0 +1,10 @@
@using LudikZone.model
<MudPaper Height="140px" Width="140px">
@Game.Name
</MudPaper>
@code {
[Parameter]
public required Game Game { get; set; }
}

View File

@@ -1,6 +1,6 @@
@inherits LayoutComponentBase
<MudThemeProvider Theme="@_theme" IsDarkMode="_isDarkMode" />
<MudThemeProvider />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
@@ -9,7 +9,6 @@
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())" />
<MudText Typo="Typo.h5" Class="ml-3">Application</MudText>
<MudSpacer />
<MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Inherit" OnClick="@DarkModeToggle" />
<MudIconButton Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Edge="Edge.End" />
</MudAppBar>
<MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
@@ -29,76 +28,12 @@
@code {
private bool _drawerOpen = true;
private bool _isDarkMode = true;
private MudTheme? _theme = null;
protected override void OnInitialized()
{
base.OnInitialized();
_theme = new()
{
PaletteLight = _lightPalette,
PaletteDark = _darkPalette,
LayoutProperties = new LayoutProperties()
};
}
private void DrawerToggle()
{
_drawerOpen = !_drawerOpen;
}
private void DarkModeToggle()
{
_isDarkMode = !_isDarkMode;
}
private readonly PaletteLight _lightPalette = new()
{
Black = "#110e2d",
AppbarText = "#424242",
AppbarBackground = "rgba(255,255,255,0.8)",
DrawerBackground = "#ffffff",
GrayLight = "#e8e8e8",
GrayLighter = "#f9f9f9",
};
private readonly PaletteDark _darkPalette = new()
{
Primary = "#7e6fff",
Surface = "#1e1e2d",
Background = "#1a1a27",
BackgroundGray = "#151521",
AppbarText = "#92929f",
AppbarBackground = "rgba(26,26,39,0.8)",
DrawerBackground = "#1a1a27",
ActionDefault = "#74718e",
ActionDisabled = "#9999994d",
ActionDisabledBackground = "#605f6d4d",
TextPrimary = "#b2b0bf",
TextSecondary = "#92929f",
TextDisabled = "#ffffff33",
DrawerIcon = "#92929f",
DrawerText = "#92929f",
GrayLight = "#2a2833",
GrayLighter = "#1e1e2d",
Info = "#4a86ff",
Success = "#3dcb6c",
Warning = "#ffb545",
Error = "#ff3f5f",
LinesDefault = "#33323e",
TableLines = "#33323e",
Divider = "#292838",
OverlayLight = "#1e1e2d80",
};
public string DarkLightModeButtonIcon => _isDarkMode switch
{
true => Icons.Material.Rounded.AutoMode,
false => Icons.Material.Outlined.DarkMode,
};
}

View File

@@ -1,8 +1,49 @@
@implements IDisposable
@inject NavigationManager NavigationManager
<MudNavMenu>
<MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
<MudNavLink Href="counter" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Add">Counter</MudNavLink>
<MudNavLink Href="weather" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Weather</MudNavLink>
<MudNavLink Href="Games" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.VideogameAsset">Games</MudNavLink>
<AuthorizeView>
<Authorized>
<MudNavLink Href="Account/Manage" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Person">
@context.User.Identity?.Name</MudNavLink>
<form action="Account/Logout" method="post">
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
<button type="submit" class="mud-nav-link mud-ripple">
<MudIcon Icon="@Icons.Material.Filled.Logout" Color="Color.Info" Class="mr-3"></MudIcon> Logout
</button>
</form>
</Authorized>
<NotAuthorized>
<MudNavLink Href="Account/Register" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Person">
Register</MudNavLink>
<MudNavLink Href="Account/Login" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Password">Login
</MudNavLink>
</NotAuthorized>
</AuthorizeView>
</MudNavMenu>
@code {
private string? currentUrl;
protected override void OnInitialized()
{
currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
NavigationManager.LocationChanged += OnLocationChanged;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
StateHasChanged();
}
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
}
}

View File

@@ -1,18 +0,0 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">Counter</MudText>
<MudText Typo="Typo.body1" Class="mb-4">Current count: @currentCount</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" @onclick="IncrementCount">Click me</MudButton>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,31 @@
@page "/games"
@using LudikZoneBlazor.Components.Games
@using LudikZoneBlazor.Data
@using Microsoft.AspNetCore.Authorization
@using LudikZone.model
@inject IStringLocalizer<Games> localizer
@inject ApplicationDbContext context
<PageTitle>@localizer["Games"]</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">@localizer["Game list"]</MudText>
<MudGrid Spacing="10" Justify="Justify.Center">
@foreach (var g in GetGames())
{
<MudItem>
<GameIcon Game=@g />
</MudItem>
}
</MudGrid>
@code {
private List<Game> GetGames()
{
return context.Games.ToList();
}
}

View File

@@ -39,11 +39,16 @@
<br />
<MudText Typo="Typo.h6" GutterBottom="true">Prerendering</MudText>
<MudText Typo="Typo.body2" GutterBottom="true">
If you're exploring the features of .NET 8 Blazor,<br /> you might be pleasantly surprised to learn that each page is prerendered on the server,<br /> regardless of the selected render mode.<br /><br />
This means that you'll need to inject all necessary services on the server,<br /> even when opting for the wasm (WebAssembly) render mode.<br /><br />
This prerendering functionality is crucial to ensuring that WebAssembly mode feels fast and responsive,<br /> especially when it comes to initial page load times.<br /><br />
For more information on how to detect prerendering and leverage the RenderContext, you can refer to the following link:
<MudLink Href="https://github.com/dotnet/aspnetcore/issues/51468#issuecomment-1783568121" Target="_blank" Typo="Typo.body2" Color="Color.Primary">
If you're exploring the features of .NET 8 Blazor,<br /> you might be pleasantly surprised to learn that each page
is prerendered on the server,<br /> regardless of the selected render mode.<br /><br />
This means that you'll need to inject all necessary services on the server,<br /> even when opting for the wasm
(WebAssembly) render mode.<br /><br />
This prerendering functionality is crucial to ensuring that WebAssembly mode feels fast and responsive,<br />
especially when it comes to initial page load times.<br /><br />
For more information on how to detect prerendering and leverage the RenderContext, you can refer to the following
link:
<MudLink Href="https://github.com/dotnet/aspnetcore/issues/51468#issuecomment-1783568121" Target="_blank"
Typo="Typo.body2" Color="Color.Primary">
More details
</MudLink>
</MudText>
@@ -52,7 +57,8 @@
<MudText Typo="Typo.h6" GutterBottom="true">InteractiveAuto</MudText>
<MudText Typo="Typo.body2">
A discussion on how to achieve this can be found here:
<MudLink Href="https://github.com/dotnet/aspnetcore/issues/51468#issue-1950424116" Target="_blank" Typo="Typo.body2" Color="Color.Primary">
<MudLink Href="https://github.com/dotnet/aspnetcore/issues/51468#issue-1950424116" Target="_blank" Typo="Typo.body2"
Color="Color.Primary">
More details
</MudLink>
</MudText>

View File

@@ -1,60 +0,0 @@
@page "/weather"
<PageTitle>Weather</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">Weather forecast</MudText>
<MudText Typo="Typo.body1" Class="mb-8">This component demonstrates fetching data from the server.</MudText>
@if (forecasts == null)
{
<MudProgressCircular Color="Color.Default" Indeterminate="true" />
}
else
{
<MudTable Items="forecasts" Hover="true" SortLabel="Sort By" Elevation="0" AllowUnsorted="false">
<HeaderContent>
<MudTh><MudTableSortLabel InitialDirection="SortDirection.Ascending" SortBy="new Func<WeatherForecast, object>(x=>x.Date)">Date</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<WeatherForecast, object>(x=>x.TemperatureC)">Temp. (C)</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<WeatherForecast, object>(x=>x.TemperatureF)">Temp. (F)</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<WeatherForecast, object>(x=>x.Summary!)">Summary</MudTableSortLabel></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Date">@context.Date</MudTd>
<MudTd DataLabel="Temp. (C)">@context.TemperatureC</MudTd>
<MudTd DataLabel="Temp. (F)">@context.TemperatureF</MudTd>
<MudTd DataLabel="Summary">@context.Summary</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[]{50, 100}" />
</PagerContent>
</MudTable>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate a loading indicator
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -1,6 +1,11 @@
<Router AppAssembly="typeof(Program).Assembly">
@using LudikZoneBlazor.Components.Account.Shared
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -1,5 +1,7 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using MudBlazor.StaticInput
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@@ -10,3 +12,4 @@
@using MudBlazor.Services
@using LudikZoneBlazor
@using LudikZoneBlazor.Components
@using Microsoft.Extensions.Localization

View File

@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using LudikZone.model;
namespace LudikZoneBlazor.Data;
/// <summary>
/// Le context BDD de l'application
/// </summary>
/// <param name="options"></param>
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser>(options)
{
/// <summary> Tout les jeux existant sur la plateforme </summary>
public virtual DbSet<Game> Games => Set<Game>();
}

View File

@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Identity;
namespace LudikZoneBlazor.Data;
/// <summary> Add profile data for application users by adding properties to the ApplicationUser class </summary>
public class ApplicationUser : IdentityUser
{
}

View File

@@ -4,10 +4,27 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisMode>All</AnalysisMode>
<UserSecretsId>aspnet-LudikZoneBlazor-b18f2a2e-d082-4232-8521-6cacaa2a4ba2</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.*" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.*" />
<PackageReference Include="MudBlazor" Version="7.*" />
<PackageReference Include="Extensions.MudBlazor.StaticInput" Version="2.*" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LudikZone.model\LudikZone.model.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,277 @@
// <auto-generated />
using System;
using LudikZoneBlazor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LudikZoneBlazor.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240906111336_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LudikZoneBlazor.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,222 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LudikZoneBlazor.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "text", nullable: false),
ProviderKey = table.Column<string>(type: "text", nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
RoleId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
LoginProvider = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}

View File

@@ -0,0 +1,300 @@
// <auto-generated />
using System;
using LudikZoneBlazor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LudikZoneBlazor.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240910202256_AjoutGames")]
partial class AjoutGames
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LudikZone.model.Game", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("MaxPlayerCount")
.HasColumnType("integer");
b.Property<int>("MinPlayerCount")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Games");
});
modelBuilder.Entity("LudikZoneBlazor.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LudikZoneBlazor.Migrations
{
/// <inheritdoc />
public partial class AjoutGames : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Games",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
MinPlayerCount = table.Column<int>(type: "integer", nullable: false),
MaxPlayerCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Games", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Games");
}
}
}

View File

@@ -0,0 +1,297 @@
// <auto-generated />
using System;
using LudikZoneBlazor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LudikZoneBlazor.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LudikZone.model.Game", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("MaxPlayerCount")
.HasColumnType("integer");
b.Property<int>("MinPlayerCount")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Games");
});
modelBuilder.Entity("LudikZoneBlazor.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("LudikZoneBlazor.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,5 +1,11 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using LudikZoneBlazor.Components;
using LudikZoneBlazor.Components.Account;
using LudikZoneBlazor.Data;
using Npgsql;
var builder = WebApplication.CreateBuilder(args);
@@ -10,10 +16,51 @@ builder.Services.AddMudServices();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies();
#region Base de données
NpgsqlConnectionStringBuilder conStrBuilder = new NpgsqlConnectionStringBuilder(builder.Configuration.GetConnectionString("DefaultConnection"));
conStrBuilder.Host = string.IsNullOrEmpty(builder.Configuration["DB_HOST"]) ? conStrBuilder.Host : builder.Configuration["DB_HOST"];
conStrBuilder.Username = string.IsNullOrEmpty(builder.Configuration["DB_USER"]) ? conStrBuilder.Username : builder.Configuration["DB_USER"];
conStrBuilder.Database = string.IsNullOrEmpty(builder.Configuration["DB_DATABASE"]) ? conStrBuilder.Database : builder.Configuration["DB_DATABASE"];
conStrBuilder.Port = int.TryParse(builder.Configuration["DB_PORT"], out int tempVal) ? tempVal : conStrBuilder.Port;
conStrBuilder.Password = builder.Configuration["DB_PASSWORD"];
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(conStrBuilder.ToString()));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
#endregion
builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
// Begin I18N configuration
builder.Services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
// End I18N configuration
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
@@ -28,4 +75,25 @@ app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
// Add additional endpoints required by the Identity /Account Razor components.
app.MapAdditionalIdentityEndpoints();
// Begin I18N configuration
var supportedCultures = new[] { "fr", "en" };
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
// End I18N configuration
// Appliquer les migrations automatiquement lors du démarrage
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Database.Migrate(); // Applique les migrations
}
app.Run();

View File

@@ -4,8 +4,8 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:15132",
"sslPort": 44355
"applicationUrl": "http://localhost:34997",
"sslPort": 44310
}
},
"profiles": {
@@ -13,7 +13,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5031",
"applicationUrl": "http://localhost:5147",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -22,7 +22,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7201;http://localhost:5031",
"applicationUrl": "https://localhost:7076;http://localhost:5147",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Games" xml:space="preserve">
<value>Games anglais 4</value>
<comment>comment game</comment>
</data>
<data name="Game list" xml:space="preserve">
<value>Game list</value>
<comment>title game list</comment>
</data>
</root>

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Games" xml:space="preserve">
<value>Games francais 4</value>
<comment>comment game</comment>
</data>
<data name="Game list" xml:space="preserve">
<value>Liste des jeux</value>
<comment>Titre de la liste des jeux</comment>
</data>
</root>

View File

@@ -1,8 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "host=localhost;username=admin;database=ludikZone;Pooling=false;Timeout=300;CommandTimeout=300;Port=5432;"
},
"DB_HOST": "localhost",
"DB_PORT": 5432,
"DB_USER": "admin",
"DB_PASSWORD": "admin",
"DB_DATABASE": "ludikZone"
}

View File

@@ -1,9 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "host=localhost;username=admin;database=ludikZone;Pooling=false;Timeout=300;CommandTimeout=300;Port=5432;"
},
"AllowedHosts": "*"
}