summaryrefslogtreecommitdiff
path: root/VPNAuth.Server
diff options
context:
space:
mode:
Diffstat (limited to 'VPNAuth.Server')
-rw-r--r--VPNAuth.Server/Config.cs49
-rw-r--r--VPNAuth.Server/Database/AccessToken.cs10
-rw-r--r--VPNAuth.Server/Database/AuthRequest.cs14
-rw-r--r--VPNAuth.Server/Database/Database.cs14
-rw-r--r--VPNAuth.Server/HttpContextUtils.cs15
-rw-r--r--VPNAuth.Server/Migrations/20250418102156_DbInit.Designer.cs91
-rw-r--r--VPNAuth.Server/Migrations/20250418102156_DbInit.cs61
-rw-r--r--VPNAuth.Server/Migrations/DatabaseModelSnapshot.cs88
-rw-r--r--VPNAuth.Server/Pages/Auth.cshtml31
-rw-r--r--VPNAuth.Server/Pages/Auth.cshtml.cs52
-rw-r--r--VPNAuth.Server/Pages/Dashboard.cshtml41
-rw-r--r--VPNAuth.Server/PkceUtils.cs17
-rw-r--r--VPNAuth.Server/Program.cs98
-rw-r--r--VPNAuth.Server/Responses/Token.cs11
-rw-r--r--VPNAuth.Server/Responses/UserInfo.cs28
-rw-r--r--VPNAuth.Server/VPNAuth.Server.csproj87
16 files changed, 707 insertions, 0 deletions
diff --git a/VPNAuth.Server/Config.cs b/VPNAuth.Server/Config.cs
new file mode 100644
index 0000000..84b01ef
--- /dev/null
+++ b/VPNAuth.Server/Config.cs
@@ -0,0 +1,49 @@
+using System.Text.Json;
+
+namespace VPNAuth.Server;
+
+public class ConfigUser
+{
+ public string? Username { get; set; }
+ public List<string>? Ips { get; set; }
+
+ public string? Sub { get; set; }
+ public string? Name { get; set; }
+ public string? GivenName { get; set; }
+ public string? FamilyName { get; set; }
+ public string? PreferredUsername { get; set; }
+ public string? Email { get; set; }
+ public string? Picture { get; set; }
+}
+
+public class ConfigApp
+{
+ public string? ClientId { get; set; }
+ public string? RedirectUri { get; set; }
+ public string? Secret { get; set; }
+}
+
+public class Config
+{
+ public List<ConfigUser>? Users { get; set; }
+ public List<ConfigApp>? Apps { get; set; }
+
+ public ConfigApp? FindApp(string clientId)
+ => Apps?.Find(app => app.ClientId == clientId);
+
+ private static string _filePath = "./config.json";
+
+ public static void CreateIfNotExists()
+ {
+ if (File.Exists(_filePath)) return;
+
+ File.Create(_filePath);
+ File.WriteAllText(_filePath, JsonSerializer.Serialize(new Config
+ {
+ Users = []
+ }));
+ }
+
+ public static Config Read()
+ => JsonSerializer.Deserialize<Config>(File.ReadAllText(_filePath))!;
+}
diff --git a/VPNAuth.Server/Database/AccessToken.cs b/VPNAuth.Server/Database/AccessToken.cs
new file mode 100644
index 0000000..3cdc3ba
--- /dev/null
+++ b/VPNAuth.Server/Database/AccessToken.cs
@@ -0,0 +1,10 @@
+namespace VPNAuth.Server.Database;
+
+public class AccessToken
+{
+ public int Id { get; set; }
+ public string Token { get; set; }
+ public string ClientId { get; set; }
+ public DateTime CreationTime { get; set; }
+ public List<string> Scopes { get; set; }
+}
diff --git a/VPNAuth.Server/Database/AuthRequest.cs b/VPNAuth.Server/Database/AuthRequest.cs
new file mode 100644
index 0000000..98fe001
--- /dev/null
+++ b/VPNAuth.Server/Database/AuthRequest.cs
@@ -0,0 +1,14 @@
+namespace VPNAuth.Server.Database;
+
+public class AuthRequest
+{
+ public int Id { get; set; }
+ public DateTime InitTime { get; set; }
+ public string Code { get; set; }
+ public string? State { get; set; }
+ public string ClientId { get; set; }
+ public List<string> Scopes { get; set; }
+ public string CodeChallenge { get; set; }
+ public string CodeChallengeMethod { get; set; }
+ public bool Accepted { get; set; }
+}
diff --git a/VPNAuth.Server/Database/Database.cs b/VPNAuth.Server/Database/Database.cs
new file mode 100644
index 0000000..a8cf8eb
--- /dev/null
+++ b/VPNAuth.Server/Database/Database.cs
@@ -0,0 +1,14 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace VPNAuth.Server.Database;
+
+public class Database : DbContext
+{
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder.UseSqlite("Data Source=vpnauth.db");
+ }
+
+ public DbSet<AuthRequest> AuthRequests { get; set; }
+ public DbSet<AccessToken> AccessTokens { get; set; }
+}
diff --git a/VPNAuth.Server/HttpContextUtils.cs b/VPNAuth.Server/HttpContextUtils.cs
new file mode 100644
index 0000000..5bba471
--- /dev/null
+++ b/VPNAuth.Server/HttpContextUtils.cs
@@ -0,0 +1,15 @@
+namespace VPNAuth.Server;
+
+public static class HttpContextUtils
+{
+ public static string GetRemoteIpAddress(this HttpContext context)
+ => context.Request.Headers["X-Forwarded-For"].DefaultIfEmpty(context.Connection.RemoteIpAddress!.ToString())
+ .First()!;
+
+ public static ConfigUser? GetUser(this HttpContext context)
+ {
+ var config = Config.Read();
+ if (config.Users == null || config.Users.Count == 0) return null;
+ return config.Users!.Find(user => user.Ips!.Contains(context.GetRemoteIpAddress()));
+ }
+}
diff --git a/VPNAuth.Server/Migrations/20250418102156_DbInit.Designer.cs b/VPNAuth.Server/Migrations/20250418102156_DbInit.Designer.cs
new file mode 100644
index 0000000..57eda44
--- /dev/null
+++ b/VPNAuth.Server/Migrations/20250418102156_DbInit.Designer.cs
@@ -0,0 +1,91 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using VPNAuth.Server.Database;
+
+#nullable disable
+
+namespace VPNAuth.Server.Migrations
+{
+ [DbContext(typeof(Database.Database))]
+ [Migration("20250418102156_DbInit")]
+ partial class DbInit
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.4");
+
+ modelBuilder.Entity("VPNAuth.Server.Database.AccessToken", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ClientId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("CreationTime")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("Scopes")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Token")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AccessTokens");
+ });
+
+ modelBuilder.Entity("VPNAuth.Server.Database.AuthRequest", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Accepted")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ClientId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Code")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodeChallenge")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodeChallengeMethod")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("InitTime")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("Scopes")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("State")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AuthRequests");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/VPNAuth.Server/Migrations/20250418102156_DbInit.cs b/VPNAuth.Server/Migrations/20250418102156_DbInit.cs
new file mode 100644
index 0000000..70967ca
--- /dev/null
+++ b/VPNAuth.Server/Migrations/20250418102156_DbInit.cs
@@ -0,0 +1,61 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace VPNAuth.Server.Migrations
+{
+ /// <inheritdoc />
+ public partial class DbInit : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AccessTokens",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Token = table.Column<string>(type: "TEXT", nullable: false),
+ ClientId = table.Column<string>(type: "TEXT", nullable: false),
+ CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
+ Scopes = table.Column<string>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AccessTokens", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AuthRequests",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ InitTime = table.Column<DateTime>(type: "TEXT", nullable: false),
+ Code = table.Column<string>(type: "TEXT", nullable: false),
+ State = table.Column<string>(type: "TEXT", nullable: true),
+ ClientId = table.Column<string>(type: "TEXT", nullable: false),
+ Scopes = table.Column<string>(type: "TEXT", nullable: false),
+ CodeChallenge = table.Column<string>(type: "TEXT", nullable: false),
+ CodeChallengeMethod = table.Column<string>(type: "TEXT", nullable: false),
+ Accepted = table.Column<bool>(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AuthRequests", x => x.Id);
+ });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AccessTokens");
+
+ migrationBuilder.DropTable(
+ name: "AuthRequests");
+ }
+ }
+}
diff --git a/VPNAuth.Server/Migrations/DatabaseModelSnapshot.cs b/VPNAuth.Server/Migrations/DatabaseModelSnapshot.cs
new file mode 100644
index 0000000..d735267
--- /dev/null
+++ b/VPNAuth.Server/Migrations/DatabaseModelSnapshot.cs
@@ -0,0 +1,88 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using VPNAuth.Server.Database;
+
+#nullable disable
+
+namespace VPNAuth.Server.Migrations
+{
+ [DbContext(typeof(Database.Database))]
+ partial class DatabaseModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.4");
+
+ modelBuilder.Entity("VPNAuth.Server.Database.AccessToken", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ClientId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("CreationTime")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("Scopes")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Token")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AccessTokens");
+ });
+
+ modelBuilder.Entity("VPNAuth.Server.Database.AuthRequest", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Accepted")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ClientId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Code")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodeChallenge")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodeChallengeMethod")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("InitTime")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("Scopes")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("State")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AuthRequests");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/VPNAuth.Server/Pages/Auth.cshtml b/VPNAuth.Server/Pages/Auth.cshtml
new file mode 100644
index 0000000..5ac8efe
--- /dev/null
+++ b/VPNAuth.Server/Pages/Auth.cshtml
@@ -0,0 +1,31 @@
+@page "/auth"
+@model VPNAuth.Server.Pages.Auth
+
+@{
+ Layout = null;
+}
+
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>VPNAuth - Auth</title>
+</head>
+<body style="text-align: center;">
+ <h1>Authorization</h1>
+ <h2>VPNAuth</h2>
+ @if (Model.ValidRequest)
+ {
+ <div>
+ <p>Do you want to log into <i>@Request.Query["client_id"]</i>?</p>
+ <button onclick="window.location = '/accept-auth/@Model.RequestEntry?.Entity.Id'">Yes</button>
+ <br/>
+ <p>You are logged in as <i>@Model.User?.Username</i>.</p>
+ </div>
+ }
+ else
+ {
+ <b>Invalid request.</b>
+ }
+</body>
+</html>
diff --git a/VPNAuth.Server/Pages/Auth.cshtml.cs b/VPNAuth.Server/Pages/Auth.cshtml.cs
new file mode 100644
index 0000000..bdcbc59
--- /dev/null
+++ b/VPNAuth.Server/Pages/Auth.cshtml.cs
@@ -0,0 +1,52 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using VPNAuth.Server.Database;
+
+namespace VPNAuth.Server.Pages;
+
+public class Auth : PageModel
+{
+ public Config Config;
+ public ConfigUser? User;
+ public bool ValidRequest;
+ public EntityEntry<AuthRequest>? RequestEntry;
+
+ public readonly List<string> RequiredQueryParams =
+ [
+ "response_type",
+ "client_id",
+ "scope",
+ "code_challenge_method",
+ "code_challenge"
+ ];
+
+ public void OnGet()
+ {
+ Config = Config.Read();
+ User = HttpContext.GetUser();
+
+ ValidRequest = RequiredQueryParams.All(key => Request.Query.ContainsKey(key))
+ && Config.FindApp(Request.Query["client_id"]!) != null
+ && Request.Query["code_challenge_method"] == "S256"
+ && User != null;
+
+ RequestEntry = null;
+
+ if (ValidRequest)
+ {
+ using var db = new Database.Database();
+ RequestEntry = db.Add(new AuthRequest
+ {
+ InitTime = DateTime.Now,
+ ClientId = Request.Query["client_id"]!,
+ Code = PkceUtils.GenerateCode(),
+ State = Request.Query["state"],
+ Scopes = Request.Query["scope"].ToString().Split(" ").ToList(),
+ CodeChallenge = Request.Query["code_challenge"]!,
+ CodeChallengeMethod = Request.Query["code_challenge_method"]!,
+ Accepted = false
+ });
+ db.SaveChanges();
+ }
+ }
+}
diff --git a/VPNAuth.Server/Pages/Dashboard.cshtml b/VPNAuth.Server/Pages/Dashboard.cshtml
new file mode 100644
index 0000000..cb00d9f
--- /dev/null
+++ b/VPNAuth.Server/Pages/Dashboard.cshtml
@@ -0,0 +1,41 @@
+@page "/"
+@using Microsoft.EntityFrameworkCore.ChangeTracking
+@using VPNAuth.Server
+@using VPNAuth.Server.Database
+
+@{
+ Layout = null;
+
+ string remoteIp = Request.HttpContext.GetRemoteIpAddress();
+ ConfigUser? user = Request.HttpContext.GetUser();
+}
+
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>VPNAuth - Dashboard</title>
+</head>
+<body style="text-align: center;">
+ @if (user == null)
+ {
+ <p>No user detected</p>
+ }
+ else
+ {
+ <div>
+ <h1>Dashboard</h1>
+ <h2>VPNAuth</h2>
+ <p>Hey, @user.Username!</p>
+ <i>User settings coming soon...</i>
+ <h3>Your IPs</h3>
+ <ul style="list-style-position: inside;">
+ @foreach (var ip in user.Ips!)
+ {
+ <li>@ip</li>
+ }
+ </ul>
+ </div>
+ }
+</body>
+</html>
diff --git a/VPNAuth.Server/PkceUtils.cs b/VPNAuth.Server/PkceUtils.cs
new file mode 100644
index 0000000..a11926e
--- /dev/null
+++ b/VPNAuth.Server/PkceUtils.cs
@@ -0,0 +1,17 @@
+namespace VPNAuth.Server;
+
+public static class PkceUtils
+{
+ private static string _codeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
+
+ public static string GenerateCode(int length = 10)
+ {
+ string code = "";
+ for (int i = 0; i < length; i++)
+ code += _codeChars[new Random().Next(_codeChars.Length)]; // TODO: Is that function random enough?
+ return code;
+ }
+
+ public static string GenerateToken(int length = 20)
+ => GenerateCode(length); // TODO: maybe add more possible chars then for GenerateCode
+}
diff --git a/VPNAuth.Server/Program.cs b/VPNAuth.Server/Program.cs
new file mode 100644
index 0000000..6ea0b40
--- /dev/null
+++ b/VPNAuth.Server/Program.cs
@@ -0,0 +1,98 @@
+using VPNAuth.Server;
+using VPNAuth.Server.Database;
+using VPNAuth.Server.Responses;
+
+Config.CreateIfNotExists();
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+builder.Services.AddRazorPages();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (!app.Environment.IsDevelopment())
+{
+ app.UseExceptionHandler("/Error");
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+ app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+
+app.UseRouting();
+
+app.UseAuthorization();
+
+app.MapGet("/accept-auth/{id}", async (HttpContext context, int id) =>
+{
+ using var db = new Database();
+ var authRequest = db.AuthRequests.Find(id);
+ if (authRequest == null || authRequest.Accepted)
+ {
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
+ return;
+ }
+
+ authRequest.Accepted = true;
+ db.SaveChanges();
+
+ var config = Config.Read();
+ context.Response.StatusCode = StatusCodes.Status302Found;
+ context.Response.Headers["Location"] = config.FindApp(authRequest.ClientId)!.RedirectUri!
+ + "?code=" + authRequest.Code
+ + "&state=" + authRequest.State;
+});
+
+app.MapPost("/access-token", async (HttpContext context) =>
+{
+ var config = Config.Read();
+ if (context.Request.Form["grant_type"] != "authorization_code")
+ {
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
+ return;
+ }
+
+ var clientSecret = config.FindApp(context.Request.Form["client_id"]!)!.Secret; // FIXME: null pointer
+ if (clientSecret != null && clientSecret != context.Request.Form["client_secret"])
+ {
+ context.Response.StatusCode = StatusCodes.Status403Forbidden;
+ return;
+ }
+
+ using var db = new Database();
+ var authRequest = db.AuthRequests
+ .Where(request => request.Code == context.Request.Form["code"].ToString())
+ .ToList()
+ .FirstOrDefault();
+ if (authRequest == null)
+ {
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
+ return;
+ }
+
+ // TODO: validate code verifier -> context.Request.Form["code_verifier"]
+
+ var accessTokenEntry = db.AccessTokens.Add(new AccessToken
+ {
+ ClientId = authRequest.ClientId,
+ Scopes = authRequest.Scopes,
+ CreationTime = DateTime.Now,
+ Token = PkceUtils.GenerateToken()
+ });
+ db.SaveChanges();
+
+ await context.Response.WriteAsJsonAsync(new Token
+ {
+ AccessToken = accessTokenEntry.Entity.Token,
+ TokenType = "Bearer",
+ Expires = 0 // TODO: change to actual value
+ });
+});
+
+app.MapStaticAssets();
+app.MapRazorPages()
+ .WithStaticAssets();
+
+app.Run("http://localhost:8080");
diff --git a/VPNAuth.Server/Responses/Token.cs b/VPNAuth.Server/Responses/Token.cs
new file mode 100644
index 0000000..9d8a374
--- /dev/null
+++ b/VPNAuth.Server/Responses/Token.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+
+namespace VPNAuth.Server.Responses;
+
+public class Token
+{
+ [JsonPropertyName("access_token")] public string? AccessToken { get; set; }
+ [JsonPropertyName("refresh_token")] public string? RefreshToken { get; set; }
+ [JsonPropertyName("token_type")] public string? TokenType { get; set; }
+ [JsonPropertyName("expires")] public int? Expires { get; set; }
+}
diff --git a/VPNAuth.Server/Responses/UserInfo.cs b/VPNAuth.Server/Responses/UserInfo.cs
new file mode 100644
index 0000000..bc4deee
--- /dev/null
+++ b/VPNAuth.Server/Responses/UserInfo.cs
@@ -0,0 +1,28 @@
+using System.Text.Json.Serialization;
+
+namespace VPNAuth.Server.Responses;
+
+public class UserInfo
+{
+ [JsonPropertyName("sub")] public string? Sub { get; set; }
+ [JsonPropertyName("name")] public string? Name { get; set; }
+ [JsonPropertyName("given_name")] public string? GivenName { get; set; }
+ [JsonPropertyName("family_name")] public string? FamilyName { get; set; }
+
+ [JsonPropertyName("preferred_username")]
+ public string? PreferredUsername { get; set; }
+
+ [JsonPropertyName("email")] public string? Email { get; set; }
+ [JsonPropertyName("picture")] public string? Picture { get; set; }
+
+ public UserInfo(ConfigUser configUser)
+ {
+ Sub = configUser.Sub;
+ Name = configUser.Name;
+ GivenName = configUser.GivenName;
+ FamilyName = configUser.FamilyName;
+ PreferredUsername = configUser.PreferredUsername;
+ Email = configUser.Email;
+ Picture = configUser.Picture;
+ }
+}
diff --git a/VPNAuth.Server/VPNAuth.Server.csproj b/VPNAuth.Server/VPNAuth.Server.csproj
new file mode 100644
index 0000000..8bbec60
--- /dev/null
+++ b/VPNAuth.Server/VPNAuth.Server.csproj
@@ -0,0 +1,87 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <_ContentIncludedByDefault Remove="wwwroot\css\site.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.min.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.min.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.min.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.min.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.min.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.min.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.min.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.min.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.min.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.min.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.min.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.min.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.min.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.min.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.min.css" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.min.css.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.js.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.min.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.min.js.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.js.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.min.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.min.js.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.js.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.min.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.min.js.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\LICENSE" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation-unobtrusive\dist\jquery.validate.unobtrusive.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation-unobtrusive\dist\jquery.validate.unobtrusive.min.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation-unobtrusive\LICENSE.txt" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\dist\additional-methods.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\dist\additional-methods.min.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\dist\jquery.validate.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\dist\jquery.validate.min.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery-validation\LICENSE.md" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery\dist\jquery.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery\dist\jquery.min.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery\dist\jquery.min.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery\dist\jquery.slim.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery\dist\jquery.slim.min.js" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery\dist\jquery.slim.min.map" />
+ <_ContentIncludedByDefault Remove="wwwroot\lib\jquery\LICENSE.txt" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="wwwroot\" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
+ </ItemGroup>
+
+</Project>