Initial Commit
This commit is contained in:
commit
4e51424c8e
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
obj
|
||||
bin
|
||||
appsettings.local.json
|
||||
node_modules
|
||||
Web/dist
|
||||
.vscode/settings.json
|
||||
wwwroot
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
269
Controllers/IndieauthController.cs
Normal file
269
Controllers/IndieauthController.cs
Normal file
@ -0,0 +1,269 @@
|
||||
namespace Lunar.Exchange.Lapis.Controllers;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using Lunar.Exchange.Lapis.Data;
|
||||
using Lunar.Exchange.Lapis.Data.Models;
|
||||
using Lunar.Exchange.Lapis.Models;
|
||||
using Lunar.Exchange.Lapis.Utilities;
|
||||
//using Lunar.Exchange.Lapis.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/indieauth")]
|
||||
public class IndieauthController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<IndieauthController> Logger;
|
||||
private readonly SignInManager<IdentityUser> SignInManager;
|
||||
private readonly UserManager<IdentityUser> UserManager;
|
||||
private readonly IUserStore<IdentityUser> UserStore;
|
||||
private readonly LapisContext DbContext;
|
||||
|
||||
public IndieauthController(
|
||||
ILogger<IndieauthController> logger,
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
UserManager<IdentityUser> userManager,
|
||||
IUserStore<IdentityUser> userStore,
|
||||
LapisContext dbContext
|
||||
)
|
||||
{
|
||||
Logger = logger;
|
||||
SignInManager = signInManager;
|
||||
UserManager = userManager;
|
||||
UserStore = userStore;
|
||||
DbContext = dbContext;
|
||||
}
|
||||
|
||||
[HttpGet("metadata")]
|
||||
[SnakeCaseConverterAttribute]
|
||||
public IActionResult Metadata()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
Issuer = "https://auth.translunar.systems",
|
||||
AuthorizationEndpoint = "https://auth.translunar.systems/indieauth",
|
||||
TokenEndpoint = "https://auth.translunar.systems/api/indieauth/token",
|
||||
IntrospectionEndpoint = "https://auth.translunar.systems/api/indieauth/introspect",
|
||||
RevocationEndpoint = "https://auth.translunar.systems/api/indieauth/revoke",
|
||||
UserinfoEndpoint = "https://auth.translunar.systems/api/indieauth/userinfo",
|
||||
ResponseTypesSupported = new string[] {
|
||||
"code"
|
||||
},
|
||||
GrantTypesSupported = new string[] {
|
||||
"authorization_code"
|
||||
},
|
||||
CodeChallengeMethodsSupported = new string[] {
|
||||
"S256"
|
||||
},
|
||||
AuthorizationResponseIssParameterSupported = true,
|
||||
ServiceDocumentation = "https://auth.translunar.systems"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/indieauth")]
|
||||
public IActionResult RedirectToConsentPage()
|
||||
{
|
||||
var url = new UriBuilder("https://auth.translunar.systems/authorize");
|
||||
url.Query = HttpContext.Request.QueryString.ToString();
|
||||
|
||||
return new RedirectResult(url.ToString(), false);
|
||||
}
|
||||
|
||||
[HttpPost("authorize")]
|
||||
[HttpPost("/indieauth")]
|
||||
[SnakeCaseConverterAttribute]
|
||||
[Consumes("application/x-www-form-urlencoded")]
|
||||
public async Task<IActionResult> ClientGetProfile([FromForm] IndieauthAuthorizeModel model)
|
||||
{
|
||||
var result = await AuthorizeClient(model);
|
||||
if (result is not OkResult)
|
||||
return result;
|
||||
|
||||
// Nasty but eh
|
||||
var requestCode = await DbContext.IndieauthCodes
|
||||
.Include(c => c.Client)
|
||||
.ThenInclude(l => l.Profile)
|
||||
.SingleAsync(
|
||||
c => c.Value == model.Code
|
||||
&& c.Client.ClientId == model.ClientId
|
||||
&& c.Client.RedirectUri == model.RedirectUri
|
||||
&& c.Claimed != null
|
||||
);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Me = requestCode.Client.Profile.ProfileUrl
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public async Task<IActionResult> AuthorizeClient(IndieauthAuthorizeModel model)
|
||||
{
|
||||
if (model.GrantType is null)
|
||||
return BadRequest(new { Error = "invalid_request", ErrorMessage = "Missing grant_type argument" });
|
||||
|
||||
if (model.GrantType != "authorization_code")
|
||||
return BadRequest(new { Error = "unsupported_grant_type", ErrorMessage = "Supported grant types: authorization_code" });
|
||||
|
||||
if (model.Code is null || model.ClientId is null || model.RedirectUri is null)
|
||||
return BadRequest(new { Error = "invalid_request", ErrorMessage = "Missing required parameters" });
|
||||
|
||||
var requestCode = await DbContext.IndieauthCodes.SingleOrDefaultAsync(
|
||||
c => c.Value == model.Code
|
||||
&& c.Client.ClientId == model.ClientId
|
||||
&& c.Client.RedirectUri == model.RedirectUri
|
||||
&& c.Claimed == null
|
||||
);
|
||||
|
||||
if (requestCode is null)
|
||||
{
|
||||
Logger.LogInformation("IndieAuth client token authorization failure: no such unclaimed code for given client info");
|
||||
return BadRequest(new { Error = "invalid_grant", ErrorDescription = "Failed to verify client code" });
|
||||
}
|
||||
|
||||
if (requestCode.Created.AddMinutes(3) < DateTime.UtcNow)
|
||||
{
|
||||
if (requestCode.Created.AddHours(1) < DateTime.UtcNow)
|
||||
{
|
||||
Logger.LogInformation("IndieAuth client token authorization failure: expired code");
|
||||
return BadRequest(new { Error = "invalid_grant", ErrorDescription = "Failed to verify client code" });
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation("IndieAuth client token authorization failure: recently expired code");
|
||||
return BadRequest(new { Error = "invalid_grant", ErrorDescription = "Code expired" });
|
||||
}
|
||||
}
|
||||
|
||||
if (model.CodeVerifier is null != requestCode.CodeChallenge is null)
|
||||
{
|
||||
Logger.LogInformation("IndieAuth client token authorization failure: presence mismatch for PKCE \"code_verifier\" and \"code_challenge\"");
|
||||
return BadRequest(new { Error = "invalid_grant", ErrorDescription = "Failed to verify client code" });
|
||||
}
|
||||
|
||||
|
||||
if (model.CodeVerifier is not null)
|
||||
{
|
||||
using (var sha256 = SHA256.Create())
|
||||
{
|
||||
var shaBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(model.CodeVerifier));
|
||||
var challengeBytes = Base64UrlTextEncoder.Decode(requestCode.CodeChallenge!);
|
||||
if (shaBytes is null || !shaBytes.Any())
|
||||
{
|
||||
Logger.LogError("IndieAuth client token authorization failure: hash output is null or empty");
|
||||
return BadRequest(new { Error = "invalid_grant", ErrorDescription = "Failed to verify client code" });
|
||||
}
|
||||
if (!CryptographicOperations.FixedTimeEquals(shaBytes, challengeBytes))
|
||||
{
|
||||
Logger.LogInformation("IndieAuth client token authorization failure: PKCE verifier does not match the challenge");
|
||||
return BadRequest(new { Error = "invalid_grant", ErrorDescription = "Failed to verify client code" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestCode.Claimed = DateTime.UtcNow;
|
||||
await DbContext.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("request")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> AuthRequest(IndieauthRequestModel model)
|
||||
{
|
||||
var user = await UserManager.FindByNameAsync(User.Identity!.Name);
|
||||
if (user is null)
|
||||
// Shoudn't really happen, but just in case
|
||||
return Unauthorized(new { Error = "Unauthorized" });
|
||||
|
||||
if (model.ProfileId is null)
|
||||
return BadRequest(new { Error = "No profile selected" });
|
||||
var profile = await DbContext.UserProfiles.SingleOrDefaultAsync(p => p.UserId == user.Id && p.Id == model.ProfileId);
|
||||
if (profile is null)
|
||||
return NotFound(new { Error = "No such profile for the current user" });
|
||||
|
||||
|
||||
var client = new Uri(model.ClientId);
|
||||
var redirect = new Uri(model.RedirectUri);
|
||||
|
||||
if (redirect.Host != client.Host || redirect.Scheme != client.Scheme || redirect.Port != client.Port)
|
||||
return BadRequest(new { Error = "Redirect does not match the basic client data, checking not implemented yet, bailing" });
|
||||
|
||||
if (model.CodeChallenge is not null && model.CodeChallengeMethod is not null)
|
||||
{
|
||||
if (model.CodeChallengeMethod != "S256")
|
||||
return BadRequest(new { Error = "Unsupported PKCE challenge method" });
|
||||
if (Base64UrlTextEncoder.Decode(model.CodeChallenge).Length != 32)
|
||||
return BadRequest(new { Error = "Invalid PKCE challenge data" });
|
||||
}
|
||||
|
||||
if (model.Me is not null && profile.ProfileUrl != model.Me)
|
||||
return BadRequest(new { Error = "Mismatched profile urls" });
|
||||
|
||||
var existing = await DbContext.IndieauthLinks.FirstOrDefaultAsync(l => l.Profile.UserId == user.Id && l.ClientId == model.ClientId);
|
||||
|
||||
// Probably not the best...
|
||||
string? randomCode = null;
|
||||
const string codeAlphabet = "abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRTUVWXYZ1234567890!@#$%^&*()[]{}\\|-_;:'\",.`~ ";
|
||||
const int size = 48;
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
byte[] randomBytes = new byte[4 * size];
|
||||
rng.GetBytes(randomBytes);
|
||||
StringBuilder result = new StringBuilder(size);
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
var rnd = BitConverter.ToUInt32(randomBytes, i * 4);
|
||||
var idx = rnd % codeAlphabet.Length;
|
||||
|
||||
result.Append(codeAlphabet[((int)idx)]);
|
||||
}
|
||||
|
||||
randomCode = result.ToString();
|
||||
}
|
||||
|
||||
if (randomCode is null || randomCode.Length != size)
|
||||
throw new Exception("Code creation error");
|
||||
|
||||
var linkEntity = existing ?? DbContext.IndieauthLinks.Add(new IndieauthClient
|
||||
{
|
||||
ProfileId = profile.Id,
|
||||
|
||||
ClientId = model.ClientId,
|
||||
RedirectUri = model.RedirectUri,
|
||||
|
||||
FirstSeen = DateTime.UtcNow,
|
||||
Scope = model.Scope
|
||||
}).Entity;
|
||||
|
||||
linkEntity.LastSeen = DateTime.UtcNow;
|
||||
|
||||
DbContext.IndieauthCodes.Add(new IndieauthRequest
|
||||
{
|
||||
Client = linkEntity,
|
||||
|
||||
Value = randomCode,
|
||||
CodeChallenge = model.CodeChallenge,
|
||||
CodeChallengeMethod = model.CodeChallengeMethod,
|
||||
|
||||
Created = DateTime.UtcNow,
|
||||
});
|
||||
await DbContext.SaveChangesAsync();
|
||||
|
||||
var redirectTo = new UriBuilder(model.RedirectUri);
|
||||
var query = HttpUtility.ParseQueryString(redirectTo.Query);
|
||||
query.Set("code", randomCode);
|
||||
query.Set("state", model.State);
|
||||
query.Set("iss", "https://auth.translunar.systems/");
|
||||
redirectTo.Query = query.ToString();
|
||||
|
||||
return Ok(new { Message = "Successfully authorized", RedirectUri = redirectTo.ToString() });
|
||||
}
|
||||
}
|
132
Controllers/UserController.cs
Normal file
132
Controllers/UserController.cs
Normal file
@ -0,0 +1,132 @@
|
||||
namespace Lunar.Exchange.Lapis.Controllers;
|
||||
|
||||
using System.Security.Claims;
|
||||
using Lunar.Exchange.Lapis.Data;
|
||||
using Lunar.Exchange.Lapis.Models;
|
||||
//using Lunar.Exchange.Lapis.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("/api/user")]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<UserController> Logger;
|
||||
private readonly SignInManager<IdentityUser> SignInManager;
|
||||
private readonly UserManager<IdentityUser> UserManager;
|
||||
private readonly IUserStore<IdentityUser> UserStore;
|
||||
private readonly LapisContext DbContext;
|
||||
|
||||
public UserController(
|
||||
ILogger<UserController> logger,
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
UserManager<IdentityUser> userManager,
|
||||
IUserStore<IdentityUser> userStore,
|
||||
LapisContext dbContext
|
||||
)
|
||||
{
|
||||
Logger = logger;
|
||||
SignInManager = signInManager;
|
||||
UserManager = userManager;
|
||||
UserStore = userStore;
|
||||
DbContext = dbContext;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(LoginModel loginModel)
|
||||
{
|
||||
var user = await UserManager.FindByNameAsync(loginModel.Username);
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized(new { error = "Invalid email or password" });
|
||||
}
|
||||
|
||||
var result = await SignInManager.PasswordSignInAsync(
|
||||
user,
|
||||
loginModel.Password,
|
||||
isPersistent: true,
|
||||
lockoutOnFailure: false
|
||||
);
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
return Unauthorized(new { error = "Continue with two-factor auth" });
|
||||
|
||||
if (result.IsLockedOut || result.IsNotAllowed)
|
||||
return Unauthorized(new { error = "Account locked" });
|
||||
|
||||
if (!result.Succeeded)
|
||||
return Unauthorized(new { error = "Invalid email or password" });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Message = "Logged in successfully"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
if (User.Identity is null)
|
||||
return Ok(new { });
|
||||
var user = await UserManager.FindByNameAsync(User.Identity!.Name);
|
||||
if (user is null)
|
||||
return Ok(new { });
|
||||
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
|
||||
|
||||
return Ok(new { });
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Register(LoginModel loginModel)
|
||||
{
|
||||
var user = new IdentityUser();
|
||||
|
||||
await UserStore.SetUserNameAsync(user, loginModel.Username, default);
|
||||
|
||||
var result = await UserManager.CreateAsync(user, loginModel.Password);
|
||||
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
result.Succeeded,
|
||||
result.Errors
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> UserInfo()
|
||||
{
|
||||
var user = await UserManager.FindByNameAsync(User.Identity!.Name);
|
||||
var profiles = await DbContext.UserProfiles
|
||||
.Where(u => u.UserId == user.Id)
|
||||
.Select(p => new
|
||||
{
|
||||
p.Id,
|
||||
p.ProfileUrl,
|
||||
Clients = p.Clients.Select(c => new
|
||||
{
|
||||
c.Id,
|
||||
c.ClientId,
|
||||
c.RedirectUri,
|
||||
c.Scope,
|
||||
c.FirstSeen,
|
||||
c.LastSeen
|
||||
})
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Username = user.UserName,
|
||||
Profiles = profiles
|
||||
});
|
||||
}
|
||||
}
|
22
Data/LapisContext.cs
Normal file
22
Data/LapisContext.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace Lunar.Exchange.Lapis.Data;
|
||||
|
||||
using System.Reflection;
|
||||
using Lunar.Exchange.Lapis.Data.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class LapisContext : IdentityDbContext<IdentityUser>
|
||||
{
|
||||
public DbSet<IndieauthClient> IndieauthLinks { get; set; } = null!;
|
||||
public DbSet<IndieauthRequest> IndieauthCodes { get; set; } = null!;
|
||||
public DbSet<UserProfile> UserProfiles { get; set; } = null!;
|
||||
|
||||
public LapisContext(DbContextOptions<LapisContext> options) : base(options) { }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
}
|
||||
}
|
21
Data/Models/IndieauthClient.cs
Normal file
21
Data/Models/IndieauthClient.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace Lunar.Exchange.Lapis.Data.Models;
|
||||
|
||||
|
||||
public class IndieauthClient
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int ProfileId { get; set; }
|
||||
public UserProfile Profile { get; set; } = null!;
|
||||
|
||||
public string ClientId { get; set; } = null!;
|
||||
public string RedirectUri { get; set; } = null!;
|
||||
|
||||
// Maybe store it as several entries elsewhere? Good enough for MVP
|
||||
public string? Scope { get; set; }
|
||||
|
||||
public DateTime FirstSeen { get; set; }
|
||||
public DateTime LastSeen { get; set; }
|
||||
|
||||
public IEnumerable<IndieauthRequest> Codes { get; set; } = null!;
|
||||
}
|
16
Data/Models/IndieauthRequest.cs
Normal file
16
Data/Models/IndieauthRequest.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Lunar.Exchange.Lapis.Data.Models;
|
||||
|
||||
public class IndieauthRequest
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int ClientId { get; set; }
|
||||
public IndieauthClient Client { get; set; } = null!;
|
||||
|
||||
public string Value { get; set; } = null!;
|
||||
public string CodeChallenge { get; set; } = null!;
|
||||
public string CodeChallengeMethod { get; set; } = null!;
|
||||
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime? Claimed { get; set; }
|
||||
}
|
15
Data/Models/UserProfile.cs
Normal file
15
Data/Models/UserProfile.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Lunar.Exchange.Lapis.Data.Models;
|
||||
|
||||
|
||||
public class UserProfile
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserId { get; set; } = null!;
|
||||
public IdentityUser User { get; set; } = null!;
|
||||
|
||||
public string ProfileUrl { get; set; } = null!;
|
||||
|
||||
public IEnumerable<IndieauthClient> Clients { get; set; } = null!;
|
||||
}
|
402
Migrations/20221024165358_InitialMigration.Designer.cs
generated
Normal file
402
Migrations/20221024165358_InitialMigration.Designer.cs
generated
Normal file
@ -0,0 +1,402 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Lunar.Exchange.Lapis.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lunar.Exchange.Lapis.Migrations
|
||||
{
|
||||
[DbContext(typeof(LapisContext))]
|
||||
[Migration("20221024165358_InitialMigration")]
|
||||
partial class InitialMigration
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.4");
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthClient", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("FirstSeen")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastSeen")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RedirectUri")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileId");
|
||||
|
||||
b.ToTable("IndieauthLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthRequest", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("Claimed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CodeChallenge")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CodeChallengeMethod")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.ToTable("IndieauthCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.UserProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ProfileUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserProfiles");
|
||||
});
|
||||
|
||||
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("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
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");
|
||||
|
||||
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.IdentityUser", 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("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
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")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasMaxLength(128)
|
||||
.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")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthClient", b =>
|
||||
{
|
||||
b.HasOne("Lunar.Exchange.Lapis.Data.Models.UserProfile", "Profile")
|
||||
.WithMany("Clients")
|
||||
.HasForeignKey("ProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthRequest", b =>
|
||||
{
|
||||
b.HasOne("Lunar.Exchange.Lapis.Data.Models.IndieauthClient", "Client")
|
||||
.WithMany("Codes")
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Client");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.UserProfile", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
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("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", 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("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthClient", b =>
|
||||
{
|
||||
b.Navigation("Codes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.UserProfile", b =>
|
||||
{
|
||||
b.Navigation("Clients");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
311
Migrations/20221024165358_InitialMigration.cs
Normal file
311
Migrations/20221024165358_InitialMigration.cs
Normal file
@ -0,0 +1,311 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lunar.Exchange.Lapis.Migrations
|
||||
{
|
||||
public partial class InitialMigration : Migration
|
||||
{
|
||||
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: "TEXT", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "TEXT", 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: "TEXT", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "INTEGER", 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: "INTEGER", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "INTEGER", 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("Sqlite:Autoincrement", true),
|
||||
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("Sqlite:Autoincrement", true),
|
||||
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", maxLength: 128, nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "TEXT", maxLength: 128, 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", maxLength: 128, nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 128, 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.CreateTable(
|
||||
name: "UserProfiles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ProfileUrl = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserProfiles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserProfiles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IndieauthLinks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ProfileId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ClientId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
RedirectUri = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Scope = table.Column<string>(type: "TEXT", nullable: true),
|
||||
FirstSeen = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
LastSeen = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IndieauthLinks", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_IndieauthLinks_UserProfiles_ProfileId",
|
||||
column: x => x.ProfileId,
|
||||
principalTable: "UserProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IndieauthCodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CodeChallenge = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CodeChallengeMethod = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Claimed = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IndieauthCodes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_IndieauthCodes_IndieauthLinks_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "IndieauthLinks",
|
||||
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);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IndieauthCodes_ClientId",
|
||||
table: "IndieauthCodes",
|
||||
column: "ClientId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IndieauthLinks_ProfileId",
|
||||
table: "IndieauthLinks",
|
||||
column: "ProfileId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserProfiles_UserId",
|
||||
table: "UserProfiles",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
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: "IndieauthCodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "IndieauthLinks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserProfiles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
400
Migrations/LapisContextModelSnapshot.cs
Normal file
400
Migrations/LapisContextModelSnapshot.cs
Normal file
@ -0,0 +1,400 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Lunar.Exchange.Lapis.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lunar.Exchange.Lapis.Migrations
|
||||
{
|
||||
[DbContext(typeof(LapisContext))]
|
||||
partial class LapisContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.4");
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthClient", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("FirstSeen")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastSeen")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RedirectUri")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileId");
|
||||
|
||||
b.ToTable("IndieauthLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthRequest", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("Claimed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CodeChallenge")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CodeChallengeMethod")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.ToTable("IndieauthCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.UserProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ProfileUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserProfiles");
|
||||
});
|
||||
|
||||
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("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
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");
|
||||
|
||||
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.IdentityUser", 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("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
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")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasMaxLength(128)
|
||||
.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")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthClient", b =>
|
||||
{
|
||||
b.HasOne("Lunar.Exchange.Lapis.Data.Models.UserProfile", "Profile")
|
||||
.WithMany("Clients")
|
||||
.HasForeignKey("ProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthRequest", b =>
|
||||
{
|
||||
b.HasOne("Lunar.Exchange.Lapis.Data.Models.IndieauthClient", "Client")
|
||||
.WithMany("Codes")
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Client");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.UserProfile", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
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("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", 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("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.IndieauthClient", b =>
|
||||
{
|
||||
b.Navigation("Codes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lunar.Exchange.Lapis.Data.Models.UserProfile", b =>
|
||||
{
|
||||
b.Navigation("Clients");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
21
Models/IndieauthAuthorizeModel.cs
Normal file
21
Models/IndieauthAuthorizeModel.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Lunar.Exchange.Lapis.Models;
|
||||
|
||||
public class IndieauthAuthorizeModel
|
||||
{
|
||||
[FromForm(Name = "code")]
|
||||
public string? Code { get; set; } = null!;
|
||||
|
||||
[FromForm(Name = "client_id")]
|
||||
public string? ClientId { get; set; } = null!;
|
||||
|
||||
[FromForm(Name = "redirect_uri")]
|
||||
public string? RedirectUri { get; set; } = null!;
|
||||
|
||||
[FromForm(Name = "grant_type")]
|
||||
public string? GrantType { get; set; } = null!;
|
||||
|
||||
[FromForm(Name = "code_verifier")]
|
||||
public string? CodeVerifier { get; set; }
|
||||
}
|
18
Models/IndieauthRequestModel.cs
Normal file
18
Models/IndieauthRequestModel.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Lunar.Exchange.Lapis.Models;
|
||||
|
||||
public class IndieauthRequestModel
|
||||
{
|
||||
public string ResponseType { get; set; } = null!;
|
||||
public string ClientId { get; set; } = null!;
|
||||
public string RedirectUri { get; set; } = null!;
|
||||
public string State { get; set; } = null!;
|
||||
public string CodeChallenge { get; set; } = null!;
|
||||
public string CodeChallengeMethod { get; set; } = null!;
|
||||
|
||||
public string? Scope { get; set; }
|
||||
public string? Me { get; set; }
|
||||
|
||||
public int? ProfileId { get; set; }
|
||||
}
|
6
Models/LapisOptions.cs
Normal file
6
Models/LapisOptions.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Lunar.Exchange.Lapis.Models;
|
||||
|
||||
class LapisOptions
|
||||
{
|
||||
|
||||
}
|
7
Models/LoginModel.cs
Normal file
7
Models/LoginModel.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Lunar.Exchange.Lapis.Models;
|
||||
|
||||
public class LoginModel
|
||||
{
|
||||
public string Username { get; set; } = null!;
|
||||
public string Password { get; set; } = null!;
|
||||
}
|
97
Program.cs
Normal file
97
Program.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using Lunar.Exchange.Lapis.Data;
|
||||
using Lunar.Exchange.Lapis.Models;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var lapisConfiguration = new LapisOptions();
|
||||
builder.Configuration.Bind("Lapis", lapisConfiguration);
|
||||
|
||||
builder.Services.AddDbContext<LapisContext>(
|
||||
options => options.UseSqlite(
|
||||
builder.Configuration.GetConnectionString("LapisContext")
|
||||
)
|
||||
);
|
||||
|
||||
builder.Services
|
||||
.AddAntiforgery(options =>
|
||||
{
|
||||
options.HeaderName = "X-LAPIS-XSRF-TOKEN";
|
||||
options.Cookie.Name = "__Host-X-LAPIS-XSRF-TOKEN";
|
||||
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
|
||||
options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
|
||||
})
|
||||
.AddIdentityCore<IdentityUser>(o =>
|
||||
{
|
||||
o.Stores.MaxLengthForKeys = 128;
|
||||
})
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders()
|
||||
.AddEntityFrameworkStores<LapisContext>();
|
||||
|
||||
builder.Services
|
||||
.AddAuthentication("Identity.Application")
|
||||
.AddCookie("Identity.Application", options =>
|
||||
{
|
||||
options.AccessDeniedPath = null;
|
||||
options.LoginPath = null;
|
||||
options.LogoutPath = null;
|
||||
options.Events.OnRedirectToAccessDenied = redirectContext =>
|
||||
{
|
||||
redirectContext.Response.StatusCode = 403;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
options.Events.OnRedirectToLogin = redirectContext =>
|
||||
{
|
||||
Console.WriteLine($"{redirectContext}");
|
||||
redirectContext.Response.StatusCode = 401;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddControllers();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
using var context = scope.ServiceProvider.GetRequiredService<LapisContext>();
|
||||
await context.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Method != "GET")
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var path = context.Request.Path;
|
||||
var redirect = path.Value switch
|
||||
{
|
||||
"/forgot" or "/login" or "/logout" or "/register" => true,
|
||||
"/forgot/" or "/login/" or "/logout/" or "/register/" => true,
|
||||
"/authorize" or "/indieauth" => true,
|
||||
"/authorize/" or "/indieauth/" => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
if (redirect)
|
||||
context.Request.Path = "/";
|
||||
|
||||
await next(context);
|
||||
});
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapDefaultControllerRoute();
|
||||
|
||||
app.Run();
|
12
Properties/launchSettings.json
Normal file
12
Properties/launchSettings.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"lapis": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7050;http://localhost:7049",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
Services/IndieauthResolver.cs
Normal file
15
Services/IndieauthResolver.cs
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
class IndieauthResolver
|
||||
{
|
||||
|
||||
IndieauthResolver()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Discover(Uri userProfile)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
37
Utilities/SnakeCaseConverterAttribute.cs
Normal file
37
Utilities/SnakeCaseConverterAttribute.cs
Normal file
@ -0,0 +1,37 @@
|
||||
namespace Lunar.Exchange.Lapis.Utilities;
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
public class SnakeCaseConverterAttribute : ActionFilterAttribute
|
||||
{
|
||||
JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = new SnakeCaseNamingPolicy()
|
||||
};
|
||||
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
{
|
||||
if (context.Result is ObjectResult objectResult)
|
||||
{
|
||||
var jsonFormatter = new SystemTextJsonOutputFormatter(SerializerOptions);
|
||||
|
||||
objectResult.Formatters.Add(jsonFormatter);
|
||||
}
|
||||
|
||||
base.OnResultExecuting(context);
|
||||
}
|
||||
}
|
||||
|
||||
public class SnakeCaseNamingPolicy : JsonNamingPolicy
|
||||
{
|
||||
public static SnakeCaseNamingPolicy Instance { get; } = new SnakeCaseNamingPolicy();
|
||||
|
||||
public override string ConvertName(string name)
|
||||
{
|
||||
// Conversion to other naming convention goes here. Like SnakeCase, KebabCase etc.
|
||||
return string.Concat(name.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower();
|
||||
}
|
||||
}
|
4
Web/env.d.ts
vendored
Normal file
4
Web/env.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
const __COMMIT_HASH__: string | null;
|
||||
const __APP_VERSION__: string;
|
23
Web/index.html
Normal file
23
Web/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lapis Auth</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="modals"></div>
|
||||
<noscript>
|
||||
<main>
|
||||
This application requires JavaScript enabled. It's a personal
|
||||
thing made quickly without wanting to use the server-side
|
||||
templates only to then also spend time to figure out out how to
|
||||
get the input in those bound properly. Sorry!
|
||||
</main>
|
||||
</noscript>
|
||||
<footer id="footer" class="relative w-full"></footer>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
4414
Web/package-lock.json
generated
Normal file
4414
Web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
Web/package.json
Normal file
35
Web/package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "lapis-frontend",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.0.10",
|
||||
"pinia": "^2.0.21",
|
||||
"vue": "^3.2.38",
|
||||
"vue-router": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.56",
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.16",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^3.0.9",
|
||||
"vue-tsc": "^0.40.7"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
6
Web/postcss.config.js
Normal file
6
Web/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
Web/public/favicon.ico
Normal file
BIN
Web/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
60
Web/src/App.vue
Normal file
60
Web/src/App.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "@vue/reactivity";
|
||||
import { nextTick, onMounted, watch } from "vue";
|
||||
import { RouterView, useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const body = document.getElementsByTagName("body")[0];
|
||||
body.classList.add("loaded");
|
||||
|
||||
function footerUpdate(appearing: boolean) {
|
||||
body.classList.toggle("footer-shown", appearing);
|
||||
}
|
||||
|
||||
const showFooter = computed(
|
||||
() =>
|
||||
(route.meta?.showFooter as boolean) ??
|
||||
[...route.matched].reverse().find((m) => m.meta?.showFooter)?.meta
|
||||
?.showFooter ??
|
||||
false
|
||||
);
|
||||
|
||||
const version =
|
||||
__APP_VERSION__ + (__COMMIT_HASH__ ? "-" + __COMMIT_HASH__ : "");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
<teleport to="#footer">
|
||||
<transition
|
||||
@after-enter="footerUpdate(true)"
|
||||
@after-leave="footerUpdate(false)"
|
||||
name="fade"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
v-if="showFooter"
|
||||
class="absolute bottom-0 left-0 right-0 flex justify-center py-1"
|
||||
>
|
||||
<span>lapis v{{ version }}</span>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
18
Web/src/assets/base.css
Normal file
18
Web/src/assets/base.css
Normal file
@ -0,0 +1,18 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
--bg-color: #181818;
|
||||
min-height: 100vh;
|
||||
color: rgba(235, 235, 235, 0.64);
|
||||
background: var(--bg-color);
|
||||
line-height: 1.6;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
1
Web/src/assets/logo.svg
Normal file
1
Web/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 308 B |
23
Web/src/assets/main.css
Normal file
23
Web/src/assets/main.css
Normal file
@ -0,0 +1,23 @@
|
||||
@import "./base.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
font-weight: normal;
|
||||
margin: 0 auto;
|
||||
flex: 1 0 auto;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.footer-shown > #app {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
place-items: center center;
|
||||
}
|
76
Web/src/components/Collapsible.vue
Normal file
76
Web/src/components/Collapsible.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import { nextTick, onMounted, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string | null): void;
|
||||
}>();
|
||||
|
||||
const finishedChanging = ref<number | null>(null);
|
||||
const innerElement = ref<HTMLDivElement | null>(null);
|
||||
const style = ref<string | undefined>("height: 0px");
|
||||
const cachedValue = ref<string | null>(props.modelValue ?? null);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (newValue, oldValue) => {
|
||||
style.value = `height: ${
|
||||
oldValue ? innerElement.value?.clientHeight ?? 0 : 0
|
||||
}`;
|
||||
if (finishedChanging.value) clearTimeout(finishedChanging.value);
|
||||
|
||||
if (newValue) cachedValue.value = newValue;
|
||||
await nextTick();
|
||||
|
||||
style.value = `height: ${
|
||||
newValue ? innerElement.value?.clientHeight ?? 0 : 0
|
||||
}px`;
|
||||
|
||||
finishedChanging.value = setTimeout(() => {
|
||||
finishedChanging.value = null;
|
||||
cachedValue.value = newValue;
|
||||
}, 200);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
if (finishedChanging.value) clearTimeout(finishedChanging.value);
|
||||
|
||||
style.value = `height: ${
|
||||
props.modelValue ? innerElement.value?.clientHeight ?? 0 : 0
|
||||
}px`;
|
||||
|
||||
finishedChanging.value = setTimeout(
|
||||
() => (finishedChanging.value = null),
|
||||
205
|
||||
);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-if="modelValue || cachedValue"
|
||||
class="collapsible mx-1 bg-red-900 text-gray-50 rounded relative overflow-hidden"
|
||||
:style="style"
|
||||
>
|
||||
<div ref="innerElement" class="px-3 py-2 whitespace-pre-wrap">
|
||||
<button
|
||||
title="Close Collapsible"
|
||||
type="button"
|
||||
@click="emit('update:modelValue', null)"
|
||||
class="float-right ml-2"
|
||||
>
|
||||
<XCircleIcon class="h-6 w-6 text-gray-50" /></button
|
||||
>{{ cachedValue }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.collapsible {
|
||||
transition: height 0.2s ease-out;
|
||||
}
|
||||
</style>
|
185
Web/src/components/Modal.vue
Normal file
185
Web/src/components/Modal.vue
Normal file
@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
const closeButton = ref<HTMLButtonElement>();
|
||||
const modalBody = ref<HTMLDivElement>();
|
||||
const ariaClose = ref<HTMLDivElement>();
|
||||
|
||||
let previousFocus: HTMLElement | null = null;
|
||||
let lastFocusable: HTMLElement;
|
||||
let escListener: (event: KeyboardEvent) => void;
|
||||
let closing = false;
|
||||
|
||||
function close() {
|
||||
if (!props.modelValue) return;
|
||||
|
||||
closing = true;
|
||||
|
||||
emit("update:modelValue", false);
|
||||
emit("closed");
|
||||
previousFocus?.focus();
|
||||
previousFocus = null;
|
||||
}
|
||||
|
||||
function closeEsc() {
|
||||
const defaultElement = getDefaultFocus();
|
||||
if (document.activeElement == document.body) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (document.activeElement != defaultElement) {
|
||||
(defaultElement as HTMLElement).focus();
|
||||
return;
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
function getDefaultFocus() {
|
||||
return (
|
||||
(modalBody.value?.getElementsByClassName(
|
||||
"modal-default-element"
|
||||
)[0] as HTMLElement) ??
|
||||
closeButton.value ??
|
||||
ariaClose.value!
|
||||
);
|
||||
}
|
||||
|
||||
function tabCapture(event: KeyboardEvent) {
|
||||
const active = document.activeElement;
|
||||
const close = closeButton.value ?? ariaClose.value!;
|
||||
|
||||
if (event.shiftKey && active == close) {
|
||||
event.preventDefault();
|
||||
lastFocusable.focus();
|
||||
}
|
||||
if (!event.shiftKey && active == lastFocusable) {
|
||||
event.preventDefault();
|
||||
close.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
escListener = (event) => {
|
||||
if (event.code != "Escape") return;
|
||||
if (props.modelValue == false) return;
|
||||
closeEsc();
|
||||
};
|
||||
document.addEventListener("keydown", escListener);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("keydown", escListener);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (value, old) => {
|
||||
previousFocus =
|
||||
document.activeElement == document.body
|
||||
? null
|
||||
: (document.activeElement as HTMLElement);
|
||||
|
||||
console.log(value, old);
|
||||
if (!value || old) return;
|
||||
|
||||
await nextTick();
|
||||
|
||||
const defaultElement = getDefaultFocus();
|
||||
console.log(defaultElement);
|
||||
defaultElement.focus();
|
||||
|
||||
lastFocusable = ariaClose.value!;
|
||||
|
||||
const focusable = [
|
||||
...(modalBody.value?.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
) ?? []),
|
||||
] as HTMLElement[];
|
||||
|
||||
lastFocusable =
|
||||
focusable.length > 1
|
||||
? focusable[focusable.length - 1]
|
||||
: lastFocusable;
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
(e: "closed"): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<teleport to="#modals">
|
||||
<transition appear name="fade">
|
||||
<div
|
||||
id="modal-backdrop"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
v-if="modelValue"
|
||||
@click="close()"
|
||||
@keydown.tab="tabCapture"
|
||||
class="absolute inset-0 bg-gray-900/50 flex h-screen items-center justify-center"
|
||||
>
|
||||
<div
|
||||
v-if="!closeButton"
|
||||
ref="ariaClose"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="Close modal"
|
||||
@click.stop="close()"
|
||||
@keydown.enter="close()"
|
||||
@keydown.space="close()"
|
||||
></div>
|
||||
<div
|
||||
ref="modalBody"
|
||||
@click.stop=""
|
||||
class="w-96 max-h-[80%] bg-slate-700 rounded divide-y divide-gray-600"
|
||||
>
|
||||
<div>
|
||||
<slot name="header">
|
||||
<h3 id="modal-title" class="p-2">
|
||||
{{ title ?? "Modal" }}
|
||||
|
||||
<button
|
||||
ref="closeButton"
|
||||
aria-label="Close"
|
||||
@click="close()"
|
||||
class="float-right ml-2 text-red-400 active:text-red-500 focus:outline outline-2 outline-offset-2 rounded transition-color transition-100 ease-in"
|
||||
>
|
||||
<x-circle-icon class="w-6 h-6" />
|
||||
</button>
|
||||
</h3>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="p-2 flex flex-col">
|
||||
<slot><p>Empty modal</p></slot>
|
||||
</div>
|
||||
<div v-if="$slots.footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
43
Web/src/components/TailwindButton.vue
Normal file
43
Web/src/components/TailwindButton.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
disabled?: boolean;
|
||||
text?: string;
|
||||
to?: RouteLocationRaw;
|
||||
customClass?: string;
|
||||
type?: string;
|
||||
}>();
|
||||
|
||||
const button = ref(null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click", value: MouseEvent): void;
|
||||
}>();
|
||||
|
||||
const eventHandler = (event: MouseEvent) => {
|
||||
emit("click", event);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
ref="button"
|
||||
:is="to ? 'RouterLink' : 'button'"
|
||||
@click="eventHandler"
|
||||
:to="to ? to : undefined"
|
||||
:disabled="disabled || false"
|
||||
class="px-1 py-2 border transition-colors transition-200 ease-in"
|
||||
:class="[
|
||||
to ? 'text-center' : '',
|
||||
customClass ??
|
||||
'bg-slate-800 border-slate-700 disabled:bg-zinc-800 disabled:border-zinc-700 hover:bg-slate-700 hover:border-slate-600',
|
||||
]"
|
||||
:type="type"
|
||||
>
|
||||
<slot>
|
||||
{{ text || "" }}
|
||||
</slot>
|
||||
</component>
|
||||
</template>
|
215
Web/src/components/VaultTree.vue
Normal file
215
Web/src/components/VaultTree.vue
Normal file
@ -0,0 +1,215 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
FileEntry,
|
||||
FilesystemEntry,
|
||||
FileTree,
|
||||
FolderEntry,
|
||||
} from "@/stores/files";
|
||||
import { computed } from "@vue/reactivity";
|
||||
import { nextTick, ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
tree: FileTree;
|
||||
level?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "file-click", file: FileEntry): void;
|
||||
|
||||
(e: "focus-up", file: FileTree): void;
|
||||
(e: "focus-previous", file: FileTree): void;
|
||||
(e: "focus-next", file: FileTree): void;
|
||||
(e: "focused", file: FileTree): void;
|
||||
}>();
|
||||
|
||||
const focusedEntry = ref<number | null>(null);
|
||||
const innerEntries = ref<
|
||||
(
|
||||
| HTMLDivElement
|
||||
| {
|
||||
innerPrevious: () => void;
|
||||
innerNext: () => void;
|
||||
outerFocusSelf: (next: boolean | null) => void;
|
||||
}
|
||||
)[]
|
||||
>([]);
|
||||
|
||||
const root = props.tree.type == "filesystem";
|
||||
const open = ref(root);
|
||||
const treeLabel = ref<HTMLDivElement>();
|
||||
|
||||
async function toggleFolder() {
|
||||
open.value = !open.value;
|
||||
|
||||
await nextTick();
|
||||
treeLabel.value?.focus();
|
||||
emit("focused", props.tree);
|
||||
}
|
||||
|
||||
function innerPrevious() {
|
||||
console.log("prev from", focusedEntry.value);
|
||||
if (focusedEntry.value === null) {
|
||||
focusedEntry.value = props.tree.files.length - 1;
|
||||
emit("focused", props.tree);
|
||||
} else focusedEntry.value--;
|
||||
|
||||
if (focusedEntry.value >= 0) {
|
||||
focusEntry(focusedEntry.value, false);
|
||||
} else {
|
||||
focusSelf();
|
||||
}
|
||||
}
|
||||
|
||||
function innerNext() {
|
||||
if (focusedEntry.value === null) {
|
||||
focusedEntry.value = 0;
|
||||
emit("focused", props.tree);
|
||||
} else focusedEntry.value++;
|
||||
|
||||
if (focusedEntry.value <= props.tree.files.length - 1) {
|
||||
focusEntry(focusedEntry.value, true);
|
||||
} else {
|
||||
outerNext();
|
||||
}
|
||||
}
|
||||
|
||||
function outerUp() {
|
||||
focusedEntry.value = null;
|
||||
emit("focus-up", props.tree);
|
||||
}
|
||||
|
||||
function outerPrevious() {
|
||||
focusedEntry.value = null;
|
||||
emit("focus-previous", props.tree);
|
||||
}
|
||||
|
||||
function outerNext() {
|
||||
focusedEntry.value = null;
|
||||
emit("focus-next", props.tree);
|
||||
}
|
||||
|
||||
function focusEntry(index: number, next: boolean | null) {
|
||||
const entryElem = innerEntries.value[index];
|
||||
|
||||
console.log(index, entryElem, props.tree.files[index]);
|
||||
|
||||
if (entryElem instanceof HTMLElement) {
|
||||
if (document.activeElement != entryElem) entryElem.focus();
|
||||
} else {
|
||||
entryElem.outerFocusSelf(next);
|
||||
}
|
||||
}
|
||||
|
||||
function focusFsEntry(tree: FilesystemEntry) {
|
||||
const idx = props.tree.files.indexOf(tree);
|
||||
if (idx == -1) throw "Corrupt file tree or file explorer";
|
||||
focusedEntry.value = idx;
|
||||
return idx;
|
||||
}
|
||||
|
||||
function focusSelf() {
|
||||
focusedEntry.value = null;
|
||||
treeLabel.value?.focus();
|
||||
}
|
||||
|
||||
function outerFocusSelf(next: boolean | null) {
|
||||
if (next || !open.value || (next === null && focusedEntry.value === null)) {
|
||||
if (!root) focusSelf();
|
||||
else {
|
||||
focusedEntry.value = 0;
|
||||
focusEntry(0, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (next !== null) {
|
||||
focusedEntry.value = null;
|
||||
innerPrevious();
|
||||
return;
|
||||
}
|
||||
focusEntry(focusedEntry.value!, null);
|
||||
}
|
||||
|
||||
function openFile(file: FileEntry) {
|
||||
emit("focused", props.tree);
|
||||
focusedEntry.value = focusFsEntry(file);
|
||||
emit("file-click", file as FileEntry);
|
||||
}
|
||||
|
||||
const itemId = computed(() => {
|
||||
return root ? "root" : "" + (props.tree as FolderEntry).sessionUid;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
innerPrevious,
|
||||
innerNext,
|
||||
outerFocusSelf,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:id="`folder-${itemId}`"
|
||||
:role="root ? 'tree' : 'treeitem'"
|
||||
:aria-setsize="tree.files.length"
|
||||
:aria-labelledby="root ? undefined : `folder-label-${itemId}`"
|
||||
:aria-level="root ? 1 : level"
|
||||
:aria-expanded="root || tree.files.length > 0 ? open : undefined"
|
||||
>
|
||||
<div
|
||||
v-if="tree.type == 'folder'"
|
||||
ref="treeLabel"
|
||||
@click="toggleFolder()"
|
||||
@keydown.stop.up="emit('focus-previous', tree)"
|
||||
@keydown.stop.down="open ? innerNext() : emit('focus-next', tree)"
|
||||
@keydown.stop.left="open ? toggleFolder() : outerUp()"
|
||||
@keydown.stop.right="open ? innerNext() : toggleFolder()"
|
||||
:id="`folder-label-${itemId}`"
|
||||
class="p-1 rounded whitespace-nowrap text-ellipsis overflow-hidden focus:outline outline-2 outline-offset-2"
|
||||
:class="{
|
||||
'bg-cyan-900/40': !open,
|
||||
'bg-cyan-900/20': open,
|
||||
}"
|
||||
tabindex="-1"
|
||||
>
|
||||
{{ (tree as FolderEntry).name }}
|
||||
</div>
|
||||
<div :class="{ 'ml-4': !root }" v-if="open">
|
||||
<template v-for="(file, idx) in tree.files">
|
||||
<a
|
||||
v-if="file.type == 'file'"
|
||||
ref="innerEntries"
|
||||
@click="openFile(file as FileEntry)"
|
||||
@keydown.stop.up="innerPrevious()"
|
||||
@keydown.stop.down="innerNext()"
|
||||
@keydown.stop.left="focusSelf()"
|
||||
@keydown.stop.right="innerNext()"
|
||||
class="block my-1 p-1 whitespace-nowrap text-ellipsis overflow-hidden focus:outline outline-2 outline-offset-2"
|
||||
tabindex="-1"
|
||||
role="treeitem"
|
||||
:aria-posinset="idx"
|
||||
>
|
||||
{{ file.name }}
|
||||
</a>
|
||||
<vault-tree
|
||||
v-else
|
||||
ref="innerEntries"
|
||||
@file-click="(file) => emit('file-click', file)"
|
||||
@focus-up="focusSelf()"
|
||||
@focused="
|
||||
(innerTree) => (
|
||||
focusFsEntry(innerTree as FolderEntry), emit('focused', tree)
|
||||
)
|
||||
"
|
||||
@focus-previous="
|
||||
(tree) => (focusFsEntry(tree as FolderEntry), innerPrevious())
|
||||
"
|
||||
@focus-next="(tree) => (focusFsEntry(tree as FolderEntry), innerNext())"
|
||||
class="my-1"
|
||||
:tree="(file as FolderEntry)"
|
||||
:level="(level ?? 1) + 1"
|
||||
:aria-posinset="idx"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
15
Web/src/main.ts
Normal file
15
Web/src/main.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from '@/App.vue'
|
||||
import router from '@/router'
|
||||
|
||||
import '@/assets/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
112
Web/src/router.ts
Normal file
112
Web/src/router.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
import LoginView from "@/views/LoginView.vue";
|
||||
import AuthorizeView from "@/views/AuthorizeView.vue";
|
||||
import useAuthStore from "./stores/auth";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: HomeView,
|
||||
meta: {
|
||||
showFooter: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/authorize",
|
||||
name: "authorize",
|
||||
component: AuthorizeView,
|
||||
meta: {
|
||||
showFooter: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: LoginView,
|
||||
meta: {
|
||||
guest: true,
|
||||
guestOnly: true,
|
||||
showFooter: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
name: "register",
|
||||
component: () => import("@/views/RegisterView.vue"),
|
||||
meta: {
|
||||
guest: true,
|
||||
guestOnly: true,
|
||||
showFooter: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/logout",
|
||||
name: "logout",
|
||||
component: () => import("@/views/LogoutView.vue"),
|
||||
meta: {
|
||||
showFooter: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/offline",
|
||||
name: "offline",
|
||||
component: () => import("@/views/OfflineView.vue"),
|
||||
meta: {
|
||||
showFooter: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "notfound",
|
||||
component: () => import("@/views/NotFoundView.vue"),
|
||||
meta: {
|
||||
showFooter: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from) => {
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (!from.name) {
|
||||
const showFooter =
|
||||
(to.meta?.showFooter as boolean) ??
|
||||
[...to.matched].reverse().find((m) => m.meta?.showFooter)?.meta
|
||||
?.showFooter ??
|
||||
false;
|
||||
|
||||
document
|
||||
.getElementsByTagName("body")[0]
|
||||
.classList.toggle("footer-shown", showFooter);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!auth.userInfo) await auth.ready;
|
||||
} catch (error) {
|
||||
if (to.name == "offline") return;
|
||||
|
||||
auth.savePath(to.fullPath);
|
||||
console.log(auth.savedPath);
|
||||
|
||||
console.error(error);
|
||||
|
||||
return "/offline";
|
||||
}
|
||||
|
||||
if (auth.userInfo) {
|
||||
if (to.meta?.guestOnly) {
|
||||
return "/";
|
||||
}
|
||||
} else {
|
||||
if (!to.meta?.guest) {
|
||||
return "/login";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
63
Web/src/stores/auth.ts
Normal file
63
Web/src/stores/auth.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { get, send } from "@/utils";
|
||||
|
||||
interface UserProfiles {
|
||||
id: number;
|
||||
profileUrl: string;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
username: string;
|
||||
profiles: UserProfiles[];
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
const userInfo = ref<UserInfo | null>(null);
|
||||
const savedPath = ref<string | null>(null);
|
||||
|
||||
savedPath.value = window.localStorage.getItem("savedPath");
|
||||
|
||||
async function updateUserInfo() {
|
||||
try {
|
||||
userInfo.value = await get<UserInfo>("/api/user");
|
||||
} catch (response: Response | TypeError | any) {
|
||||
if (response instanceof Response && response.status == 401) {
|
||||
userInfo.value = null;
|
||||
return true;
|
||||
}
|
||||
if (response instanceof TypeError) return Promise.reject(response);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let ready = updateUserInfo();
|
||||
|
||||
async function login(username: string, password: string) {
|
||||
const response = await send("/api/user/login", {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
ready = updateUserInfo();
|
||||
await ready;
|
||||
return response;
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await send("/api/user/logout");
|
||||
ready = updateUserInfo();
|
||||
await ready;
|
||||
}
|
||||
|
||||
async function savePath(value: string) {
|
||||
savedPath.value = value;
|
||||
|
||||
if (savedPath.value)
|
||||
window.localStorage.setItem("savedPath", savedPath.value);
|
||||
else window.localStorage.removeItem("savedPath");
|
||||
}
|
||||
|
||||
return { userInfo, login, logout, ready, savedPath, savePath };
|
||||
});
|
||||
|
||||
export default useAuthStore;
|
72
Web/src/utils.ts
Normal file
72
Web/src/utils.ts
Normal file
@ -0,0 +1,72 @@
|
||||
export function toHex(buffer: ArrayBuffer | Uint8Array) {
|
||||
const view = new Uint8Array(buffer);
|
||||
const out: string[] = [];
|
||||
view.forEach((n) => out.push(n.toString(16).padStart(2, "0")));
|
||||
return out.join("");
|
||||
}
|
||||
|
||||
export function fromHex(input: string) {
|
||||
const numbers = input
|
||||
.match(/[0-9a-zA-Z]{2,2}/g)
|
||||
?.map((v) => parseInt(v, 16));
|
||||
if (!numbers) throw new Error("Input cannot be parsed as hex string");
|
||||
return new Uint8Array(numbers);
|
||||
}
|
||||
|
||||
export async function get<T>(
|
||||
url: string,
|
||||
headers?: Record<string, string>
|
||||
): Promise<T> {
|
||||
return send(url, undefined, "GET", headers);
|
||||
}
|
||||
|
||||
export async function send<Treceived, Tsent = any | undefined>(
|
||||
url: string,
|
||||
data?: Tsent,
|
||||
method: "POST" | "PUT" | "PATCH" | "DELETE" | "GET" = "POST",
|
||||
headers?: Record<string, string>
|
||||
): Promise<Treceived> {
|
||||
if (!url.startsWith("htt") && window.location.port == "5173")
|
||||
url =
|
||||
"http://localhost:3000" +
|
||||
(url.startsWith("/") ? "" : window.location.pathname + "/") +
|
||||
url;
|
||||
if (fetch) {
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: data
|
||||
? {
|
||||
"Content-Type": "application/json",
|
||||
...headers,
|
||||
}
|
||||
: headers,
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
}).then((response) => {
|
||||
if (!response.ok) return Promise.reject(response);
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
for (const header in headers ?? {}) {
|
||||
if (
|
||||
!headers ||
|
||||
!Object.prototype.hasOwnProperty.call(headers, header)
|
||||
)
|
||||
continue;
|
||||
const value = headers[header];
|
||||
xhr.setRequestHeader(header, value);
|
||||
}
|
||||
|
||||
xhr.open(method, url, true);
|
||||
xhr.addEventListener("load", function (e) {
|
||||
resolve(JSON.parse(this.response));
|
||||
});
|
||||
xhr.addEventListener("error", function (e) {
|
||||
reject(e);
|
||||
});
|
||||
if (data) xhr.send(JSON.stringify(data));
|
||||
else xhr.send();
|
||||
});
|
||||
}
|
131
Web/src/views/AuthorizeView.vue
Normal file
131
Web/src/views/AuthorizeView.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<main>
|
||||
<h2>
|
||||
IndieAuth login request for: {{ state.request?.clientId }} ({{
|
||||
state.request?.responseType
|
||||
}})
|
||||
</h2>
|
||||
<form @submit.prevent="sendAuthorize">
|
||||
<div>
|
||||
<div v-if="state.request?.scope">
|
||||
Scopes requested:
|
||||
<ul>
|
||||
<li v-for="scope in state.request.scope.split(' ')">
|
||||
{{ scope }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>No scopes requested</div>
|
||||
</div>
|
||||
<label>
|
||||
<span>Profile ID</span>
|
||||
<input
|
||||
class="border border-slate-500"
|
||||
placeholder="Profile ID"
|
||||
v-model="state.profileId"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="!hasPkce">
|
||||
<input v-model="state.ignoreMissingPkce" type="checkbox" />
|
||||
Ignore missing PKCE
|
||||
</label>
|
||||
<div v-if="state.errorText">{{ state.errorText }}</div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="!hasPkce && !state.ignoreMissingPkce"
|
||||
>
|
||||
Authorize
|
||||
</button>
|
||||
<a v-if="state.redirectUri" :href="state.redirectUri">
|
||||
Client redirect page
|
||||
</a>
|
||||
</form>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { send } from "@/utils";
|
||||
import { computed } from "@vue/reactivity";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const state = ref({
|
||||
profileId: "",
|
||||
errorText: "",
|
||||
request: null as AuthRequest | null,
|
||||
redirectUri: "",
|
||||
ignoreMissingPkce: false,
|
||||
});
|
||||
|
||||
interface AuthRequest {
|
||||
responseType: string;
|
||||
clientId: string;
|
||||
redirectUri: string;
|
||||
state?: string;
|
||||
codeChallenge?: string;
|
||||
codeChallengeMethod?: string;
|
||||
scope?: string;
|
||||
me?: string;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const paramData = {} as { [idx: string]: string };
|
||||
for (const iterator of new URLSearchParams(window.location.search)) {
|
||||
paramData[
|
||||
iterator[0].replace(/_(\w)/g, (_, letter) => letter.toUpperCase())
|
||||
] = iterator[1];
|
||||
}
|
||||
state.value.request = paramData as any as AuthRequest;
|
||||
});
|
||||
|
||||
const hasPkce = computed(
|
||||
() =>
|
||||
!!state.value.request?.codeChallenge &&
|
||||
!!state.value.request.codeChallengeMethod
|
||||
);
|
||||
|
||||
async function sendAuthorize() {
|
||||
if (!hasPkce.value && !state.value.ignoreMissingPkce) return;
|
||||
|
||||
const request = state.value.request;
|
||||
|
||||
if (!request) return;
|
||||
|
||||
if (
|
||||
!request.clientId ||
|
||||
!request.redirectUri ||
|
||||
request.responseType != "code"
|
||||
) {
|
||||
state.value.errorText = "Invalid IndieAuth request";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await send<{ redirectUri: string }>(
|
||||
"/api/indieauth/request",
|
||||
{
|
||||
...request,
|
||||
profileId: +state.value.profileId,
|
||||
}
|
||||
);
|
||||
state.value.redirectUri = result.redirectUri;
|
||||
console.log(result);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
let text = e as string;
|
||||
if (e instanceof Response) {
|
||||
if (e.bodyUsed) {
|
||||
// Shouldn't happen but, just in case
|
||||
text = `${e.status} - ${e.statusText} - "Unable to get error, body already used"`;
|
||||
} else {
|
||||
const responseText = await e.text();
|
||||
if (responseText.trim() && responseText.startsWith("{"))
|
||||
text =
|
||||
JSON.parse(responseText)?.error ||
|
||||
`${e.status} - ${e.statusText} - No detailed error information given`;
|
||||
else text = `${e.status} - ${e.statusText} - No body present`;
|
||||
}
|
||||
}
|
||||
state.value.errorText = "Error while authorizing the request:\n" + text;
|
||||
}
|
||||
}
|
||||
</script>
|
18
Web/src/views/HomeView.vue
Normal file
18
Web/src/views/HomeView.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import TailwindButton from "@/components/TailwindButton.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="flex flex-col w-80">
|
||||
<h2 class="mb-2 text-center text-xl">Home</h2>
|
||||
<div>TODO</div>
|
||||
<tailwind-button
|
||||
title="This will erase all local data!"
|
||||
to="logout"
|
||||
class="mx-2"
|
||||
custom-class="bg-red-800/20 border-red-800/10 text-zinc-400 hover:bg-red-800/60 hover:border-red-800/50"
|
||||
>
|
||||
Logout
|
||||
</tailwind-button>
|
||||
</main>
|
||||
</template>
|
135
Web/src/views/LoginView.vue
Normal file
135
Web/src/views/LoginView.vue
Normal file
@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import useAuthStore from "@/stores/auth";
|
||||
import { computed } from "@vue/reactivity";
|
||||
import { ref } from "vue";
|
||||
import Collapsible from "../components/Collapsible.vue";
|
||||
import TailwindButton from "../components/TailwindButton.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const loggingIn = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
const disableButton = computed(
|
||||
() => loggingIn.value || !email.value || !password.value
|
||||
);
|
||||
|
||||
async function login() {
|
||||
loggingIn.value = true;
|
||||
try {
|
||||
const loginInfo = await auth.login(email.value, password.value);
|
||||
console.log("Logged in as user", loginInfo, auth.userInfo);
|
||||
router.push("/");
|
||||
} catch (response) {
|
||||
if (response instanceof Error) {
|
||||
console.error(response);
|
||||
loggingIn.value = false;
|
||||
error.value = response.message;
|
||||
return;
|
||||
}
|
||||
if (response instanceof Response) {
|
||||
const buffer = await response.arrayBuffer();
|
||||
await new Promise<void>((resolve) =>
|
||||
setTimeout(() => resolve(), 400)
|
||||
);
|
||||
loggingIn.value = false;
|
||||
if (buffer.byteLength > 0) {
|
||||
const json = JSON.parse(new TextDecoder().decode(buffer));
|
||||
error.value = json.error ?? response.statusText;
|
||||
} else {
|
||||
error.value = `Error ${response.status}: ${response.statusText}`;
|
||||
}
|
||||
} else {
|
||||
error.value = `Unknown error: ${response}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<form
|
||||
id="login-form"
|
||||
@submit.prevent="login"
|
||||
class="flex flex-col w-80"
|
||||
>
|
||||
<h2 class="mb-2 text-center text-2xl">Lapis Auth</h2>
|
||||
<hr class="mt-1 mb-2 border-zinc-800" />
|
||||
|
||||
<label class="flex flex-col mb-2">
|
||||
<span class="block mb-1 px-1" for="email"> Email </span>
|
||||
<input
|
||||
required
|
||||
v-model="email"
|
||||
id="email"
|
||||
class="bg-slate-700 px-2 mx-2 py-1 rounded border border-slate-600 caret-slate-400"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
form="login-form"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="flex flex-col mb-2">
|
||||
<span class="block mb-1 px-1" for="password"> Password </span>
|
||||
<input
|
||||
required
|
||||
v-model="password"
|
||||
id="password"
|
||||
class="bg-slate-700 px-2 mx-2 py-1 rounded border border-slate-600 caret-slate-400"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
form="login-form"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<router-link
|
||||
class="mx-1 mb-2 py-1 text-center rounded text-zinc-500"
|
||||
to="register"
|
||||
>
|
||||
Forgot password
|
||||
</router-link>
|
||||
|
||||
<Collapsible class="" v-model="error" />
|
||||
<hr class="mt-2 mb-3 border-zinc-800" />
|
||||
<TailwindButton
|
||||
type="submit"
|
||||
:disabled="disableButton"
|
||||
class="mx-3 mb-3"
|
||||
>
|
||||
<template v-if="!loggingIn">Login</template>
|
||||
<div v-else>
|
||||
<span
|
||||
class="relative animate-pulse inline-flex rounded-full h-2 mx-1 w-2 bg-sky-500"
|
||||
></span>
|
||||
<span
|
||||
class="relative animate-pulse pulse-2 inline-flex rounded-full h-2 mx-1 w-2 bg-sky-500"
|
||||
></span>
|
||||
<span
|
||||
class="relative animate-pulse pulse-3 inline-flex rounded-full h-2 mx-1 w-2 bg-sky-500"
|
||||
></span>
|
||||
</div>
|
||||
</TailwindButton>
|
||||
<TailwindButton
|
||||
type="button"
|
||||
custom-class="bg-stone-900 border-stone-900"
|
||||
class="mx-3"
|
||||
to="register"
|
||||
>
|
||||
Register
|
||||
</TailwindButton>
|
||||
</form>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.pulse-2 {
|
||||
animation-delay: calc(2s / 6) !important;
|
||||
}
|
||||
.pulse-3 {
|
||||
animation-delay: calc(2s / 6 * 2) !important;
|
||||
}
|
||||
</style>
|
10
Web/src/views/LogoutView.vue
Normal file
10
Web/src/views/LogoutView.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import useAuthStore from "@/stores/auth";
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
auth.logout().then(() => router.push("/login"));
|
||||
</script>
|
||||
|
||||
<template><main>Logging out</main></template>
|
9
Web/src/views/NotFoundView.vue
Normal file
9
Web/src/views/NotFoundView.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import TailwindButton from "../components/TailwindButton.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="mb-2 text-center text-2xl">Page not found</h2>
|
||||
</div>
|
||||
</template>
|
10
Web/src/views/OfflineView.vue
Normal file
10
Web/src/views/OfflineView.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import TailwindButton from "../components/TailwindButton.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="mb-2 text-center text-2xl">Offline</h2>
|
||||
<tailwind-button>Refresh</tailwind-button>
|
||||
</div>
|
||||
</template>
|
5
Web/src/views/RegisterView.vue
Normal file
5
Web/src/views/RegisterView.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main>Registration page</main>
|
||||
</template>
|
9
Web/tailwind.config.js
Normal file
9
Web/tailwind.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
8
Web/tsconfig.config.json
Normal file
8
Web/tsconfig.config.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
21
Web/tsconfig.json
Normal file
21
Web/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"lib": [
|
||||
"ES2017",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
]
|
||||
},
|
||||
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.config.json"
|
||||
}
|
||||
]
|
||||
}
|
31
Web/vite.config.ts
Normal file
31
Web/vite.config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const commitHash = fs.existsSync(path.join("..", ".git"))
|
||||
? require("child_process")
|
||||
.execSync(
|
||||
"git describe --long --always --dirty --exclude=* --abbrev=8"
|
||||
)
|
||||
.toString()
|
||||
: process.env.VCS_DESCRIBE || null;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: ["es2017"],
|
||||
},
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
|
||||
__COMMIT_HASH__: commitHash ? JSON.stringify(commitHash) : null,
|
||||
},
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
});
|
13
appsettings.json
Normal file
13
appsettings.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"LapisContext": "Data Source=lapis.db"
|
||||
},
|
||||
"Lapis": {}
|
||||
}
|
41
lapis.csproj
Normal file
41
lapis.csproj
Normal file
@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Lunar.Exchange.Lapis</RootNamespace>
|
||||
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>0.1.0</VersionPrefix>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
|
||||
<Exec
|
||||
Command="which git > /dev/null 2>&1 && git describe --long --always --dirty --exclude=* --abbrev=8 2>/dev/null || echo -n $VCS_DESCRIBE"
|
||||
EchoOff="True"
|
||||
ConsoleToMSBuild="True"
|
||||
IgnoreExitCode="False"
|
||||
>
|
||||
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput"/>
|
||||
</Exec>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Sqlite" Version="6.0.2" />
|
||||
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.1.1" PrivateAssets="All"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
Loading…
Reference in New Issue
Block a user