✨ More auth factors, sessions api
This commit is contained in:
parent
db9b04ef47
commit
2f9df8009b
@ -350,21 +350,21 @@ public class AccountCurrentController(
|
|||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
|
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
|
||||||
return BadRequest($"Auth factor with type {request.Type} is already exists.");
|
return BadRequest($"Auth factor with type {request.Type} is already exists.");
|
||||||
|
|
||||||
var factor = await accounts.CreateAuthFactor(currentUser, request.Type, request.Secret);
|
var factor = await accounts.CreateAuthFactor(currentUser, request.Type, request.Secret);
|
||||||
return Ok(factor);
|
return Ok(factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("factors/{id:guid}")]
|
[HttpPost("factors/{id:guid}/enable")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor(Guid id, [FromBody] string code)
|
public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string code)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var factor = await db.AccountAuthFactors
|
var factor = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == id && f.Id == id)
|
.Where(f => f.AccountId == id && f.Id == id)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if(factor is null) return NotFound();
|
if (factor is null) return NotFound();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -377,7 +377,52 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("factors/{id:guid}/disable")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<AccountAuthFactor>> DisableAuthFactor(Guid id)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var factor = await db.AccountAuthFactors
|
||||||
|
.Where(f => f.AccountId == id && f.Id == id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (factor is null) return NotFound();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
factor = await accounts.DisableAuthFactor(factor);
|
||||||
|
return Ok(factor);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("factors/{id:guid}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<AccountAuthFactor>> DeleteAuthFactor(Guid id)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var factor = await db.AccountAuthFactors
|
||||||
|
.Where(f => f.AccountId == id && f.Id == id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (factor is null) return NotFound();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await accounts.DeleteAuthFactor(factor);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("sessions")]
|
[HttpGet("sessions")]
|
||||||
|
[Authorize]
|
||||||
public async Task<ActionResult<List<Session>>> GetSessions(
|
public async Task<ActionResult<List<Session>>> GetSessions(
|
||||||
[FromQuery] int take = 20,
|
[FromQuery] int take = 20,
|
||||||
[FromQuery] int offset = 0
|
[FromQuery] int offset = 0
|
||||||
@ -401,4 +446,39 @@ public class AccountCurrentController(
|
|||||||
|
|
||||||
return Ok(sessions);
|
return Ok(sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("sessions/{id:guid}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<Session>> DeleteSession(Guid id)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await accounts.DeleteSession(currentUser, id);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("sessions/current")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<Session>> DeleteCurrentSession()
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||||
|
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await accounts.DeleteSession(currentUser, currentSession.Id);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Sphere.Auth;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -9,6 +10,7 @@ namespace DysonNetwork.Sphere.Account;
|
|||||||
public class AccountService(
|
public class AccountService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
MagicSpellService spells,
|
MagicSpellService spells,
|
||||||
|
NotificationService nty,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@ -28,9 +30,7 @@ public class AccountService(
|
|||||||
.Where(c => c.Content == probe)
|
.Where(c => c.Content == probe)
|
||||||
.Include(c => c.Account)
|
.Include(c => c.Account)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (contact is not null) return contact.Account;
|
return contact?.Account;
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int?> GetAccountLevel(Guid accountId)
|
public async Task<int?> GetAccountLevel(Guid accountId)
|
||||||
@ -142,7 +142,7 @@ public class AccountService(
|
|||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Invalid code, you need to enter the correct code to enable the factor."
|
"Invalid code, you need to enter the correct code to enable the factor."
|
||||||
);
|
);
|
||||||
|
|
||||||
factor.EnabledAt = SystemClock.Instance.GetCurrentInstant();
|
factor.EnabledAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
db.Update(factor);
|
db.Update(factor);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@ -150,6 +150,57 @@ public class AccountService(
|
|||||||
return factor;
|
return factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<AccountAuthFactor> DisableAuthFactor(AccountAuthFactor factor)
|
||||||
|
{
|
||||||
|
if (factor.EnabledAt is null) throw new ArgumentException("The factor has been disabled.");
|
||||||
|
|
||||||
|
var count = await db.AccountAuthFactors
|
||||||
|
.Where(f => f.AccountId == factor.Id && f.EnabledAt != null)
|
||||||
|
.CountAsync();
|
||||||
|
if (count <= 1)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Disabling this auth factor will cause you have no active auth factors.");
|
||||||
|
|
||||||
|
factor.EnabledAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
return factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAuthFactor(AccountAuthFactor factor)
|
||||||
|
{
|
||||||
|
var count = await db.AccountAuthFactors
|
||||||
|
.Where(f => f.AccountId == factor.Id)
|
||||||
|
.CountAsync();
|
||||||
|
if (count <= 1)
|
||||||
|
throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor.");
|
||||||
|
|
||||||
|
db.AccountAuthFactors.Remove(factor);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteSession(Account account, Guid sessionId)
|
||||||
|
{
|
||||||
|
var session = await db.AuthSessions
|
||||||
|
.Include(s => s.Challenge)
|
||||||
|
.Where(s => s.Id == sessionId && s.AccountId == account.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (session is null) throw new InvalidOperationException("Session was not found.");
|
||||||
|
|
||||||
|
var sessions = await db.AuthSessions
|
||||||
|
.Include(s => s.Challenge)
|
||||||
|
.Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if(session.Challenge.DeviceId is not null)
|
||||||
|
await nty.UnsubscribePushNotifications(session.Challenge.DeviceId);
|
||||||
|
|
||||||
|
// The current session should be included in the sessions' list
|
||||||
|
db.AuthSessions.RemoveRange(sessions);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
foreach (var item in sessions)
|
||||||
|
await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Maintenance methods for server administrator
|
/// Maintenance methods for server administrator
|
||||||
public async Task EnsureAccountProfileCreated()
|
public async Task EnsureAccountProfileCreated()
|
||||||
{
|
{
|
||||||
|
@ -17,7 +17,12 @@ public class NotificationService(
|
|||||||
private readonly string _notifyTopic = config["Notifications:Topic"]!;
|
private readonly string _notifyTopic = config["Notifications:Topic"]!;
|
||||||
private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
|
private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
|
||||||
|
|
||||||
// TODO remove all push notification with this device id when this device is logged out
|
public async Task UnsubscribePushNotifications(string deviceId)
|
||||||
|
{
|
||||||
|
await db.NotificationPushSubscriptions
|
||||||
|
.Where(s => s.DeviceId == deviceId)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<NotificationPushSubscription> SubscribePushNotification(
|
public async Task<NotificationPushSubscription> SubscribePushNotification(
|
||||||
Account account,
|
Account account,
|
||||||
|
@ -41,7 +41,7 @@ public class DysonTokenAuthHandler(
|
|||||||
)
|
)
|
||||||
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
|
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
|
||||||
{
|
{
|
||||||
private const string AuthCachePrefix = "auth:";
|
public const string AuthCachePrefix = "auth:";
|
||||||
|
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,9 @@ public class Session : ModelBase
|
|||||||
public Instant? LastGrantedAt { get; set; }
|
public Instant? LastGrantedAt { get; set; }
|
||||||
public Instant? ExpiredAt { get; set; }
|
public Instant? ExpiredAt { get; set; }
|
||||||
|
|
||||||
|
public Guid AccountId { get; set; }
|
||||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||||
|
public Guid ChallengeId { get; set; }
|
||||||
[JsonIgnore] public Challenge Challenge { get; set; } = null!;
|
[JsonIgnore] public Challenge Challenge { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user