mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2026-05-16 04:48:12 +02:00
(refactor): LetsEncrypt cache entities (primary ctor, cert loader, null guard)
This commit is contained in:
parent
f70742cf18
commit
685a174806
11
CHANGELOG.md
11
CHANGELOG.md
@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.3.5] - 2026-04-12
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `CachedHostname` now uses a C# 12 primary constructor (same public construction as before).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `RegistrationCache` loads cached PEM certificates via `X509CertificateLoader.LoadCertificate` and disposes them with `using` where certificates are parsed for expiry and host listing.
|
||||||
|
- `RegistrationCache.TryGetCachedCertificate` returns `false` when the cached entry has no private key blob, avoiding a null argument when importing key material.
|
||||||
|
|
||||||
## [3.3.4] - 2026-04-01
|
## [3.3.4] - 2026-04-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
namespace MaksIT.LetsEncrypt.Entities;
|
namespace MaksIT.LetsEncrypt.Entities;
|
||||||
|
|
||||||
public class CachedHostname {
|
public class CachedHostname(
|
||||||
public string Hostname { get; set; }
|
string hostname,
|
||||||
public DateTime Expires { get; set; }
|
DateTime expires,
|
||||||
public bool IsUpcomingExpire { get; set; }
|
bool isUpcomingExpire,
|
||||||
|
bool isDisabled
|
||||||
public bool IsDisabled { get; set; }
|
) {
|
||||||
|
public string Hostname { get; set; } = hostname;
|
||||||
public CachedHostname(string hostname, DateTime expires, bool isUpcomingExpire, bool isDisabled) {
|
public DateTime Expires { get; set; } = expires;
|
||||||
Hostname = hostname;
|
public bool IsUpcomingExpire { get; set; } = isUpcomingExpire;
|
||||||
Expires = expires;
|
public bool IsDisabled { get; set; } = isDisabled;
|
||||||
IsUpcomingExpire = isUpcomingExpire;
|
|
||||||
IsDisabled = isDisabled;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ public class RegistrationCache {
|
|||||||
var (subject, cachedChert) = result;
|
var (subject, cachedChert) = result;
|
||||||
|
|
||||||
if (cachedChert.Cert != null && !cachedChert.IsDisabled) {
|
if (cachedChert.Cert != null && !cachedChert.IsDisabled) {
|
||||||
var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cachedChert.Cert));
|
using var cert = X509CertificateLoader.LoadCertificate(Encoding.ASCII.GetBytes(cachedChert.Cert));
|
||||||
|
|
||||||
// if it is about to expire, we need to refresh
|
// if it is about to expire, we need to refresh
|
||||||
if ((cert.NotAfter - DateTime.UtcNow).TotalDays < days)
|
if ((cert.NotAfter - DateTime.UtcNow).TotalDays < days)
|
||||||
@ -63,7 +63,7 @@ public class RegistrationCache {
|
|||||||
var (subject, cachedChert) = result;
|
var (subject, cachedChert) = result;
|
||||||
|
|
||||||
if (cachedChert.Cert != null) {
|
if (cachedChert.Cert != null) {
|
||||||
var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cachedChert.Cert));
|
using var cert = X509CertificateLoader.LoadCertificate(Encoding.ASCII.GetBytes(cachedChert.Cert));
|
||||||
|
|
||||||
hosts.Add(new CachedHostname(
|
hosts.Add(new CachedHostname(
|
||||||
subject,
|
subject,
|
||||||
@ -93,11 +93,14 @@ public class RegistrationCache {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cache.Cert));
|
using var cert = X509CertificateLoader.LoadCertificate(Encoding.ASCII.GetBytes(cache.Cert));
|
||||||
|
|
||||||
if ((cert.NotAfter - DateTime.UtcNow).TotalDays < 30)
|
if ((cert.NotAfter - DateTime.UtcNow).TotalDays < 30)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (cache.Private is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
var rsa = new RSACryptoServiceProvider(4096);
|
var rsa = new RSACryptoServiceProvider(4096);
|
||||||
rsa.ImportCspBlob(cache.Private);
|
rsa.ImportCspBlob(cache.Private);
|
||||||
|
|
||||||
@ -106,6 +109,7 @@ public class RegistrationCache {
|
|||||||
Private = rsa.ExportCspBlob(true),
|
Private = rsa.ExportCspBlob(true),
|
||||||
PrivatePem = rsa.ExportRSAPrivateKeyPem()
|
PrivatePem = rsa.ExportRSAPrivateKeyPem()
|
||||||
};
|
};
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,13 +132,15 @@ public class RegistrationCache {
|
|||||||
if (CachedCerts == null)
|
if (CachedCerts == null)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
foreach (var kvp in CachedCerts) {
|
foreach (var kvp in CachedCerts)
|
||||||
|
{
|
||||||
var hostname = kvp.Key;
|
var hostname = kvp.Key;
|
||||||
var cert = kvp.Value;
|
var cert = kvp.Value;
|
||||||
if (!string.IsNullOrEmpty(cert.Cert) && !string.IsNullOrEmpty(cert.PrivatePem)) {
|
if (!string.IsNullOrEmpty(cert.Cert) && !string.IsNullOrEmpty(cert.PrivatePem)) {
|
||||||
result[hostname] = $"{cert.Cert}\n{cert.PrivatePem}";
|
result[hostname] = $"{cert.Cert}\n{cert.PrivatePem}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using MaksIT.Core.Security.JWK;
|
using MaksIT.Core.Security.JWK;
|
||||||
using MaksIT.LetsEncrypt.Models.Responses;
|
using MaksIT.LetsEncrypt.Models.Responses;
|
||||||
using MaksIT.LetsEncrypt.Services;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,4 +9,7 @@ public class LoginResponse : ResponseModelBase {
|
|||||||
public required DateTime ExpiresAt { get; set; }
|
public required DateTime ExpiresAt { get; set; }
|
||||||
public required string RefreshToken { get; set; }
|
public required string RefreshToken { get; set; }
|
||||||
public required DateTime RefreshTokenExpiresAt { get; set; }
|
public required DateTime RefreshTokenExpiresAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Actual login username; use this for display so it is not replaced by a display name (e.g. "Organization Admin") from the JWT name claim.</summary>
|
||||||
|
public string? Username { get; set; }
|
||||||
}
|
}
|
||||||
@ -1,19 +1,51 @@
|
|||||||
import { LoginResponse } from '../models/identity/login/LoginResponse'
|
import { LoginResponse } from '../models/identity/login/LoginResponse'
|
||||||
|
|
||||||
const readIdentity = () => {
|
/**
|
||||||
const json = localStorage.getItem('identity')
|
* Maps API/localStorage payloads to a single camelCase LoginResponse so stored JSON matches across apps.
|
||||||
|
*/
|
||||||
|
const normalizeLoginResponse = (raw: unknown): LoginResponse | undefined => {
|
||||||
|
if (raw == null || typeof raw !== 'object') return undefined
|
||||||
|
const r = raw as Record<string, unknown>
|
||||||
|
const str = (camel: keyof LoginResponse, pascal: string): string => {
|
||||||
|
const v = r[camel as string] ?? r[pascal]
|
||||||
|
if (v == null) return ''
|
||||||
|
return typeof v === 'string' ? v : String(v)
|
||||||
|
}
|
||||||
|
const token = str('token', 'Token')
|
||||||
|
const refreshToken = str('refreshToken', 'RefreshToken')
|
||||||
|
if (!token || !refreshToken) return undefined
|
||||||
|
|
||||||
if (!json) return undefined
|
const out: LoginResponse = {
|
||||||
|
tokenType: str('tokenType', 'TokenType') || 'Bearer',
|
||||||
|
token,
|
||||||
|
expiresAt: str('expiresAt', 'ExpiresAt'),
|
||||||
|
refreshToken,
|
||||||
|
refreshTokenExpiresAt: str('refreshTokenExpiresAt', 'RefreshTokenExpiresAt'),
|
||||||
|
}
|
||||||
|
const u = r.username ?? r.Username
|
||||||
|
if (typeof u === 'string' && u.length > 0)
|
||||||
|
out.username = u
|
||||||
|
|
||||||
return JSON.parse(json) as LoginResponse
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeIdentity = (identity: LoginResponse) => {
|
const readIdentity = (): LoginResponse | undefined => {
|
||||||
localStorage.setItem('identity', JSON.stringify(identity))
|
const json = localStorage.getItem('identity')
|
||||||
|
if (!json) return undefined
|
||||||
|
try {
|
||||||
|
return normalizeLoginResponse(JSON.parse(json) as unknown)
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeIdentity = (identity: LoginResponse | unknown) => {
|
||||||
|
const n = normalizeLoginResponse(identity)
|
||||||
|
if (n) localStorage.setItem('identity', JSON.stringify(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeIdentity = () => {
|
const removeIdentity = () => {
|
||||||
localStorage.removeItem('identity')
|
localStorage.removeItem('identity')
|
||||||
}
|
}
|
||||||
|
|
||||||
export { readIdentity, writeIdentity, removeIdentity }
|
export { readIdentity, writeIdentity, removeIdentity, normalizeLoginResponse }
|
||||||
|
|||||||
@ -4,4 +4,6 @@ export interface LoginResponse {
|
|||||||
expiresAt: string
|
expiresAt: string
|
||||||
refreshToken: string
|
refreshToken: string
|
||||||
refreshTokenExpiresAt: string
|
refreshTokenExpiresAt: string
|
||||||
|
/** Actual login username from the server; use for display so it is not replaced by a display name (e.g. "Organization Admin") from the JWT. */
|
||||||
|
username?: string
|
||||||
}
|
}
|
||||||
@ -7,25 +7,14 @@ import { LoginRequest } from '../../models/identity/login/LoginRequest'
|
|||||||
import { ApiRoutes, GetApiRoute } from '../../AppMap'
|
import { ApiRoutes, GetApiRoute } from '../../AppMap'
|
||||||
import { LogoutRequest } from '../../models/identity/logout/LogoutRequest'
|
import { LogoutRequest } from '../../models/identity/logout/LogoutRequest'
|
||||||
import { LogoutResponse } from '../../models/identity/logout/LogoutResponse'
|
import { LogoutResponse } from '../../models/identity/logout/LogoutResponse'
|
||||||
import { readIdentity, removeIdentity, writeIdentity } from '../../localStorage/identity'
|
import { readIdentity, removeIdentity, writeIdentity, normalizeLoginResponse } from '../../localStorage/identity'
|
||||||
import { RefreshTokenRequest } from '../../models/identity/login/RefreshTokenRequest'
|
import { RefreshTokenRequest } from '../../models/identity/login/RefreshTokenRequest'
|
||||||
import { jwtDecode } from 'jwt-decode'
|
import { jwtDecode } from 'jwt-decode'
|
||||||
import { Claims } from '../../models/identity/Claims'
|
import { Claims } from '../../models/identity/Claims'
|
||||||
import { enumToArr, parseAclEntries } from '../../functions'
|
|
||||||
import { Role } from '../../models/identity/Role'
|
|
||||||
import { AclEntry } from '../../functions/acl/parseAclEntry'
|
|
||||||
|
|
||||||
interface IdentityRole {
|
|
||||||
value: string | number,
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Identity extends LoginResponse {
|
interface Identity extends LoginResponse {
|
||||||
userId?: string,
|
userId?: string,
|
||||||
username?: string
|
username?: string
|
||||||
roles?: IdentityRole []
|
|
||||||
isGlobalAdmin: boolean
|
|
||||||
acls?: AclEntry []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IdentityState {
|
interface IdentityState {
|
||||||
@ -36,24 +25,6 @@ interface IdentityState {
|
|||||||
hydrated: boolean
|
hydrated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** API JSON may be camelCase or PascalCase; normalize so Authorization and axios see stable keys. */
|
|
||||||
const normalizeLoginResponse = (raw: LoginResponse | undefined): LoginResponse | undefined => {
|
|
||||||
if (!raw) return undefined
|
|
||||||
const r = raw as Record<string, unknown>
|
|
||||||
const str = (camel: keyof LoginResponse, pascal: string) => {
|
|
||||||
const v = r[camel] ?? r[pascal]
|
|
||||||
if (v == null) return ''
|
|
||||||
return typeof v === 'string' ? v : String(v)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
tokenType: str('tokenType', 'TokenType'),
|
|
||||||
token: str('token', 'Token'),
|
|
||||||
expiresAt: str('expiresAt', 'ExpiresAt'),
|
|
||||||
refreshToken: str('refreshToken', 'RefreshToken'),
|
|
||||||
refreshTokenExpiresAt: str('refreshTokenExpiresAt', 'RefreshTokenExpiresAt'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: IdentityState = {
|
const initialState: IdentityState = {
|
||||||
identity: null,
|
identity: null,
|
||||||
showUserOffcanvas: false,
|
showUserOffcanvas: false,
|
||||||
@ -104,62 +75,26 @@ const refreshJwt = createAsyncThunk(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const enrichStateWithJwtContent = (token: string, identity: Identity) => {
|
const enrichStateWithJwtContent = (token: string, identity: Identity) => {
|
||||||
const jwtContent = jwtDecode(token) as Record<string, unknown>
|
let jwtContent: Record<string, unknown>
|
||||||
|
try {
|
||||||
|
jwtContent = jwtDecode(token) as Record<string, unknown>
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (jwtContent) {
|
if (jwtContent) {
|
||||||
if (jwtContent[Claims.NameIdentifier])
|
if (jwtContent[Claims.NameIdentifier])
|
||||||
identity.userId = jwtContent[Claims.NameIdentifier] as string
|
identity.userId = jwtContent[Claims.NameIdentifier] as string
|
||||||
|
|
||||||
|
// Keep the original username: prefer username from login/refresh response, then JWT claims (do not replace with a display name like "Organization Admin" from the name claim)
|
||||||
if (identity.username == null || identity.username?.trim() === '') {
|
if (identity.username == null || identity.username?.trim() === '') {
|
||||||
const nameClaim = jwtContent[Claims.Name] as string | undefined
|
const nameClaim = jwtContent[Claims.Name] as string | undefined
|
||||||
const usernameClaim = (jwtContent['username'] ?? jwtContent['preferred_username']) as string | undefined
|
const usernameClaim = (jwtContent['username'] ?? jwtContent['preferred_username']) as string | undefined
|
||||||
identity.username = (usernameClaim?.trim()) ? usernameClaim : nameClaim
|
identity.username = (usernameClaim?.trim()) ? usernameClaim : nameClaim
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jwtContent[Claims.Role]) {
|
|
||||||
|
|
||||||
const appKnownRoles = enumToArr(Role)?.map(item => {
|
|
||||||
return {
|
|
||||||
value: item.value,
|
|
||||||
label: item.displayValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const jwtRoles: string[] = Array.isArray(jwtContent[Claims.Role])
|
|
||||||
? jwtContent[Claims.Role] as string[]
|
|
||||||
: jwtContent[Claims.Role]
|
|
||||||
? [jwtContent[Claims.Role] as string]
|
|
||||||
: []
|
|
||||||
|
|
||||||
const identityRoles: IdentityRole [] = []
|
|
||||||
jwtRoles.forEach(identityRole => {
|
|
||||||
const foundRole = appKnownRoles.find(role => role.label === identityRole)
|
|
||||||
if (foundRole) {
|
|
||||||
identityRoles.push(foundRole)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
identity.roles = identityRoles
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jwtContent[Claims.AclEntry]) {
|
|
||||||
const jwtAcls: string[] = Array.isArray(jwtContent[Claims.AclEntry])
|
|
||||||
? jwtContent[Claims.AclEntry] as string[]
|
|
||||||
: jwtContent[Claims.AclEntry]
|
|
||||||
? [jwtContent[Claims.AclEntry] as string]
|
|
||||||
: []
|
|
||||||
|
|
||||||
if (jwtAcls?.includes('global:admin') ?? false) {
|
|
||||||
jwtAcls.splice(jwtAcls.indexOf('global:admin'), 1)
|
|
||||||
identity.isGlobalAdmin = true
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
identity.isGlobalAdmin = false
|
|
||||||
}
|
|
||||||
|
|
||||||
identity.acls = parseAclEntries(jwtAcls)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Enriched identity:', identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
const identitySlice = createSlice({
|
const identitySlice = createSlice({
|
||||||
@ -167,13 +102,10 @@ const identitySlice = createSlice({
|
|||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setIdentityFromLocalStorage: (state) => {
|
setIdentityFromLocalStorage: (state) => {
|
||||||
const raw = readIdentity()
|
const identity = readIdentity()
|
||||||
const identity = normalizeLoginResponse(raw)
|
|
||||||
|
|
||||||
if (identity?.token && identity.refreshTokenExpiresAt) {
|
if (identity) {
|
||||||
writeIdentity(identity)
|
|
||||||
state.identity = {
|
state.identity = {
|
||||||
isGlobalAdmin: false,
|
|
||||||
...identity
|
...identity
|
||||||
}
|
}
|
||||||
enrichStateWithJwtContent(identity.token, state.identity)
|
enrichStateWithJwtContent(identity.token, state.identity)
|
||||||
@ -204,15 +136,14 @@ const identitySlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(login.fulfilled, (state, action: PayloadAction<LoginResponse | undefined>) => {
|
.addCase(login.fulfilled, (state, action: PayloadAction<LoginResponse | undefined>) => {
|
||||||
state.status = 'idle'
|
state.status = 'idle'
|
||||||
const payload = normalizeLoginResponse(action.payload)
|
const normalized = normalizeLoginResponse(action.payload)
|
||||||
if (payload?.token && payload.refreshTokenExpiresAt) {
|
if (normalized) {
|
||||||
state.identity = {
|
state.identity = {
|
||||||
isGlobalAdmin: false,
|
...normalized
|
||||||
...payload
|
|
||||||
}
|
}
|
||||||
writeIdentity(payload)
|
writeIdentity(normalized)
|
||||||
|
|
||||||
enrichStateWithJwtContent(payload.token, state.identity)
|
enrichStateWithJwtContent(normalized.token, state.identity)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addCase(login.rejected, (state) => {
|
.addCase(login.rejected, (state) => {
|
||||||
@ -241,17 +172,17 @@ const identitySlice = createSlice({
|
|||||||
.addCase(refreshJwt.fulfilled, (state, action: PayloadAction<LoginResponse | undefined>) => {
|
.addCase(refreshJwt.fulfilled, (state, action: PayloadAction<LoginResponse | undefined>) => {
|
||||||
state.status = 'idle'
|
state.status = 'idle'
|
||||||
|
|
||||||
const payload = normalizeLoginResponse(action.payload)
|
const normalized = normalizeLoginResponse(action.payload)
|
||||||
if (payload?.token && payload.refreshTokenExpiresAt) {
|
if (normalized) {
|
||||||
state.identity = {
|
state.identity = {
|
||||||
isGlobalAdmin: false,
|
...normalized
|
||||||
...payload
|
|
||||||
}
|
}
|
||||||
writeIdentity(payload)
|
writeIdentity(normalized)
|
||||||
|
|
||||||
enrichStateWithJwtContent(payload.token, state.identity)
|
enrichStateWithJwtContent(normalized.token, state.identity)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
// Refresh API returned error (e.g. 401 Invalid refresh token); treat as logged out
|
||||||
state.identity = null
|
state.identity = null
|
||||||
state.showUserOffcanvas = false
|
state.showUserOffcanvas = false
|
||||||
removeIdentity()
|
removeIdentity()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>3.3.4</Version>
|
<Version>3.3.5</Version>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|||||||
@ -130,7 +130,8 @@ public class IdentityService(
|
|||||||
Token = tokenDomain.Token,
|
Token = tokenDomain.Token,
|
||||||
ExpiresAt = claims.ExpiresAt.Value,
|
ExpiresAt = claims.ExpiresAt.Value,
|
||||||
RefreshToken = tokenDomain.RefreshToken,
|
RefreshToken = tokenDomain.RefreshToken,
|
||||||
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
|
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt,
|
||||||
|
Username = user.Name
|
||||||
};
|
};
|
||||||
|
|
||||||
return Result<LoginResponse?>.Ok(response);
|
return Result<LoginResponse?>.Ok(response);
|
||||||
@ -166,7 +167,8 @@ public class IdentityService(
|
|||||||
Token = tokenDomain.Token,
|
Token = tokenDomain.Token,
|
||||||
ExpiresAt = tokenDomain.ExpiresAt,
|
ExpiresAt = tokenDomain.ExpiresAt,
|
||||||
RefreshToken = tokenDomain.RefreshToken,
|
RefreshToken = tokenDomain.RefreshToken,
|
||||||
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
|
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt,
|
||||||
|
Username = user.Name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +214,8 @@ public class IdentityService(
|
|||||||
Token = tokenDomain.Token,
|
Token = tokenDomain.Token,
|
||||||
ExpiresAt = claims.ExpiresAt.Value,
|
ExpiresAt = claims.ExpiresAt.Value,
|
||||||
RefreshToken = tokenDomain.RefreshToken,
|
RefreshToken = tokenDomain.RefreshToken,
|
||||||
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
|
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt,
|
||||||
|
Username = user.Name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,7 @@
|
|||||||
<DockerPublishLocally>False</DockerPublishLocally>
|
<DockerPublishLocally>False</DockerPublishLocally>
|
||||||
<ProjectGuid>0233e43f-435d-4309-b20c-ecd4bfbd2e63</ProjectGuid>
|
<ProjectGuid>0233e43f-435d-4309-b20c-ecd4bfbd2e63</ProjectGuid>
|
||||||
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
|
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
|
||||||
<!-- reverseproxy is the only service with host port mappings; server/client are internal-only. Swagger is served via YARP at /swagger. -->
|
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}</DockerServiceUrl>
|
||||||
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}/swagger</DockerServiceUrl>
|
|
||||||
<DockerServiceName>reverseproxy</DockerServiceName>
|
<DockerServiceName>reverseproxy</DockerServiceName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user