Skip to content

Commit eb2bb52

Browse files
committed
v2 token endpoints
1 parent c1012c1 commit eb2bb52

9 files changed

Lines changed: 1663 additions & 44 deletions
Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
using Microsoft.AspNetCore.Authorization;
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Authorization;
23
using Microsoft.AspNetCore.Mvc;
34
using OpenShock.API.Models.Response;
45
using OpenShock.Common.Authentication;
56
using OpenShock.Common.Authentication.ControllerBase;
67
using OpenShock.Common.Authentication.Services;
8+
using OpenShock.Common.OpenShockDb;
79

810
namespace OpenShock.API.Controller.Tokens;
911

1012
[ApiController]
1113
[Tags("API Tokens")]
1214
[Route("/{version:apiVersion}/tokens")]
1315
[Authorize(AuthenticationSchemes = OpenShockAuthSchemes.ApiToken)]
16+
[ApiVersion("1"), ApiVersion("2")]
1417
public sealed partial class TokensSelfController : AuthenticatedSessionControllerBase
1518
{
1619
/// <summary>
@@ -20,16 +23,35 @@ public sealed partial class TokensSelfController : AuthenticatedSessionControlle
2023
/// <returns></returns>
2124
/// <exception cref="Exception"></exception>
2225
[HttpGet("self")]
26+
[MapToApiVersion("1")]
2327
public TokenResponse GetSelfToken([FromServices] IUserReferenceService userReferenceService)
2428
{
25-
var x = userReferenceService.AuthReference;
26-
27-
if (x is null) throw new Exception("This should not be reachable due to AuthenticatedSession requirement");
28-
if (!x.Value.IsT1) throw new Exception("This should not be reachable due to the [TokenOnly] attribute");
29-
30-
var token = x.Value.AsT1;
31-
29+
var token = GetSelfTokenDto(userReferenceService);
30+
3231
return new TokenResponse
32+
{
33+
CreatedOn = token.CreatedAt,
34+
ValidUntil = token.ValidUntil,
35+
LastUsed = token.LastUsed ?? default,
36+
Permissions = token.Permissions,
37+
Name = token.Name,
38+
Id = token.Id
39+
};
40+
}
41+
42+
/// <summary>
43+
/// Gets information about the current token used to access this endpoint
44+
/// </summary>
45+
/// <param name="userReferenceService"></param>
46+
/// <returns></returns>
47+
/// <exception cref="Exception"></exception>
48+
[HttpGet("self")]
49+
[MapToApiVersion("2")]
50+
public TokenResponseV2 GetSelfTokenV2([FromServices] IUserReferenceService userReferenceService)
51+
{
52+
var token = GetSelfTokenDto(userReferenceService);
53+
54+
return new TokenResponseV2
3355
{
3456
CreatedOn = token.CreatedAt,
3557
ValidUntil = token.ValidUntil,
@@ -39,4 +61,14 @@ public TokenResponse GetSelfToken([FromServices] IUserReferenceService userRefer
3961
Id = token.Id
4062
};
4163
}
64+
65+
private static ApiToken GetSelfTokenDto(IUserReferenceService userReferenceService)
66+
{
67+
var x = userReferenceService.AuthReference;
68+
69+
if (x is null) throw new Exception("This should not be reachable due to AuthenticatedSession requirement");
70+
if (!x.Value.IsT1) throw new Exception("This should not be reachable due to the [TokenOnly] attribute");
71+
72+
return x.Value.AsT1;
73+
}
4274
}

API/Controller/Tokens/Tokens.cs

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System.Linq.Expressions;
12
using System.Net.Mime;
3+
using Asp.Versioning;
24
using Microsoft.AspNetCore.Mvc;
35
using Microsoft.EntityFrameworkCore;
46
using OpenShock.API.Models.Requests;
@@ -13,25 +15,57 @@ namespace OpenShock.API.Controller.Tokens;
1315

1416
public sealed partial class TokensController
1517
{
18+
private static readonly Expression<Func<ApiToken, TokenResponse>> ToTokenResponse = x => new TokenResponse
19+
{
20+
CreatedOn = x.CreatedAt,
21+
ValidUntil = x.ValidUntil,
22+
LastUsed = x.LastUsed ?? default,
23+
Permissions = x.Permissions,
24+
Name = x.Name,
25+
Id = x.Id
26+
};
27+
28+
private static readonly Expression<Func<ApiToken, TokenResponseV2>> ToTokenResponseV2 = x => new TokenResponseV2
29+
{
30+
CreatedOn = x.CreatedAt,
31+
ValidUntil = x.ValidUntil,
32+
LastUsed = x.LastUsed,
33+
Permissions = x.Permissions,
34+
Name = x.Name,
35+
Id = x.Id
36+
};
37+
38+
/// <summary>
39+
/// Tokens belonging to the current user that have not expired.
40+
/// </summary>
41+
private IQueryable<ApiToken> CurrentUserValidTokens => _db.ApiTokens
42+
.Where(x => x.UserId == CurrentUser.Id && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow));
43+
1644
/// <summary>
1745
/// List all tokens for the current user
1846
/// </summary>
1947
/// <response code="200">All tokens for the current user</response>
2048
[HttpGet]
49+
[MapToApiVersion("1")]
2150
public IAsyncEnumerable<TokenResponse> ListTokens()
2251
{
23-
return _db.ApiTokens
24-
.Where(x => x.UserId == CurrentUser.Id && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow))
52+
return CurrentUserValidTokens
53+
.OrderBy(x => x.CreatedAt)
54+
.Select(ToTokenResponse)
55+
.AsAsyncEnumerable();
56+
}
57+
58+
/// <summary>
59+
/// List all tokens for the current user
60+
/// </summary>
61+
/// <response code="200">All tokens for the current user</response>
62+
[HttpGet]
63+
[MapToApiVersion("2")]
64+
public IAsyncEnumerable<TokenResponseV2> ListTokensV2()
65+
{
66+
return CurrentUserValidTokens
2567
.OrderBy(x => x.CreatedAt)
26-
.Select(x => new TokenResponse
27-
{
28-
CreatedOn = x.CreatedAt,
29-
ValidUntil = x.ValidUntil,
30-
LastUsed = x.LastUsed,
31-
Permissions = x.Permissions,
32-
Name = x.Name,
33-
Id = x.Id
34-
})
68+
.Select(ToTokenResponseV2)
3569
.AsAsyncEnumerable();
3670
}
3771

@@ -43,23 +77,39 @@ public IAsyncEnumerable<TokenResponse> ListTokens()
4377
/// <response code="404">The token does not exist or you do not have access to it.</response>
4478
[HttpGet("{tokenId}")]
4579
[ProducesResponseType<TokenResponse>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
46-
[ProducesResponseType<OpenShockProblem>(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound
80+
[ProducesResponseType<OpenShockProblem>(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound
81+
[MapToApiVersion("1")]
4782
public async Task<IActionResult> GetTokenById([FromRoute] Guid tokenId)
4883
{
49-
var apiToken = await _db.ApiTokens
50-
.Where(x => x.UserId == CurrentUser.Id && x.Id == tokenId && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow))
51-
.Select(x => new TokenResponse
52-
{
53-
CreatedOn = x.CreatedAt,
54-
ValidUntil = x.ValidUntil,
55-
Permissions = x.Permissions,
56-
LastUsed = x.LastUsed,
57-
Name = x.Name,
58-
Id = x.Id
59-
}).FirstOrDefaultAsync();
60-
84+
var apiToken = await CurrentUserValidTokens
85+
.Where(x => x.Id == tokenId)
86+
.Select(ToTokenResponse)
87+
.FirstOrDefaultAsync();
88+
6189
if (apiToken is null) return Problem(ApiTokenError.ApiTokenNotFound);
62-
90+
91+
return Ok(apiToken);
92+
}
93+
94+
/// <summary>
95+
/// Get a token by id
96+
/// </summary>
97+
/// <param name="tokenId"></param>
98+
/// <response code="200">The token</response>
99+
/// <response code="404">The token does not exist or you do not have access to it.</response>
100+
[HttpGet("{tokenId}")]
101+
[ProducesResponseType<TokenResponseV2>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
102+
[ProducesResponseType<OpenShockProblem>(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound
103+
[MapToApiVersion("2")]
104+
public async Task<IActionResult> GetTokenByIdV2([FromRoute] Guid tokenId)
105+
{
106+
var apiToken = await CurrentUserValidTokens
107+
.Where(x => x.Id == tokenId)
108+
.Select(ToTokenResponseV2)
109+
.FirstOrDefaultAsync();
110+
111+
if (apiToken is null) return Problem(ApiTokenError.ApiTokenNotFound);
112+
63113
return Ok(apiToken);
64114
}
65115

@@ -71,6 +121,7 @@ public async Task<IActionResult> GetTokenById([FromRoute] Guid tokenId)
71121
[HttpPost]
72122
[Consumes(MediaTypeNames.Application.Json)]
73123
[Produces(MediaTypeNames.Application.Json)]
124+
[MapToApiVersion("1")]
74125
public async Task<TokenCreatedResponse> CreateToken([FromBody] CreateTokenRequest body)
75126
{
76127
var token = CryptoUtils.RandomAlphaNumericString(AuthConstants.ApiTokenLength);
@@ -95,7 +146,7 @@ public async Task<TokenCreatedResponse> CreateToken([FromBody] CreateTokenReques
95146
Token = token,
96147
CreatedAt = tokenDto.CreatedAt,
97148
ValidUntil = tokenDto.ValidUntil,
98-
LastUsed = tokenDto.LastUsed,
149+
LastUsed = tokenDto.LastUsed ?? default,
99150
Permissions = tokenDto.Permissions
100151
};
101152
}
@@ -111,10 +162,10 @@ public async Task<TokenCreatedResponse> CreateToken([FromBody] CreateTokenReques
111162
[Consumes(MediaTypeNames.Application.Json)]
112163
[ProducesResponseType(StatusCodes.Status200OK)]
113164
[ProducesResponseType<OpenShockProblem>(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound
165+
[MapToApiVersion("1")]
114166
public async Task<IActionResult> EditToken([FromRoute] Guid tokenId, [FromBody] EditTokenRequest body)
115167
{
116-
var token = await _db.ApiTokens
117-
.FirstOrDefaultAsync(x => x.UserId == CurrentUser.Id && x.Id == tokenId && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow));
168+
var token = await CurrentUserValidTokens.FirstOrDefaultAsync(x => x.Id == tokenId);
118169
if (token is null) return Problem(ApiTokenError.ApiTokenNotFound);
119170

120171
token.Name = body.Name;

API/Controller/Tokens/_ApiController.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.AspNetCore.Authorization;
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Authorization;
23
using Microsoft.AspNetCore.Mvc;
34
using OpenShock.Common.Authentication;
45
using OpenShock.Common.Authentication.ControllerBase;
@@ -10,6 +11,7 @@ namespace OpenShock.API.Controller.Tokens;
1011
[Tags("API Tokens")]
1112
[Route("/{version:apiVersion}/tokens")]
1213
[Authorize(AuthenticationSchemes = OpenShockAuthSchemes.UserSessionCookie)]
14+
[ApiVersion("1"), ApiVersion("2")]
1315
public sealed partial class TokensController : AuthenticatedSessionControllerBase
1416
{
1517
private readonly OpenShockContext _db;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using OpenShock.Common.Models;
2+
3+
namespace OpenShock.API.Models.Response;
4+
5+
public sealed class TokenResponseV2
6+
{
7+
public required Guid Id { get; init; }
8+
9+
public required string Name { get; init; }
10+
11+
public required DateTime CreatedOn { get; init; }
12+
13+
public required DateTime? ValidUntil { get; init; }
14+
15+
public required DateTime? LastUsed { get; init; }
16+
17+
public required List<PermissionType> Permissions { get; init; }
18+
}

0 commit comments

Comments
 (0)