first commit
This commit is contained in:
2
libs/web/identity/domain/src/index.ts
Normal file
2
libs/web/identity/domain/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lib/session';
|
||||
export * from './lib/ports';
|
||||
41
libs/web/identity/domain/src/lib/ports.ts
Normal file
41
libs/web/identity/domain/src/lib/ports.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthConfig, CurrentUser, Session } from './session';
|
||||
|
||||
/** The API's sign-in endpoints (/api/auth/*). */
|
||||
export abstract class AuthGateway {
|
||||
abstract config(): Observable<AuthConfig | null>;
|
||||
abstract exchangeCode(
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
redirectUri: string,
|
||||
): Observable<Session>;
|
||||
abstract password(username: string, password: string): Observable<Session>;
|
||||
abstract refresh(refreshToken: string): Observable<Session>;
|
||||
}
|
||||
|
||||
/** Who the current token belongs to (GraphQL `me`). */
|
||||
export abstract class ProfileGateway {
|
||||
abstract me(accessToken: string): Observable<CurrentUser>;
|
||||
}
|
||||
|
||||
export abstract class SessionStorage {
|
||||
abstract load(): Session | null;
|
||||
abstract save(session: Session): void;
|
||||
abstract clear(): void;
|
||||
}
|
||||
|
||||
export interface AuthorizationResponse {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
redirectUri: string;
|
||||
/** Where the user wanted to go before signing in. */
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
/** Authorization Code + PKCE in the browser (redirect out and back). */
|
||||
export abstract class AuthorizationFlow {
|
||||
/** Leaves the app towards the identity server's authorize page. */
|
||||
abstract start(config: AuthConfig, returnUrl: string): Promise<void>;
|
||||
/** Validates `state` and returns what is needed to exchange the code. */
|
||||
abstract complete(params: URLSearchParams): AuthorizationResponse;
|
||||
}
|
||||
23
libs/web/identity/domain/src/lib/session.spec.ts
Normal file
23
libs/web/identity/domain/src/lib/session.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { isExpired, sessionFrom } from './session';
|
||||
|
||||
describe('session', () => {
|
||||
it('computes the expiry from expires_in', () => {
|
||||
const session = sessionFrom(
|
||||
{ accessToken: 't', refreshToken: 'r', expiresIn: 3600 },
|
||||
1_000,
|
||||
);
|
||||
expect(session).toEqual({
|
||||
accessToken: 't',
|
||||
refreshToken: 'r',
|
||||
expiresAt: 3_601_000,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats tokens about to expire as expired', () => {
|
||||
const session = sessionFrom({ accessToken: 't', expiresIn: 30 }, 0);
|
||||
expect(isExpired(session, 0)).toBe(true);
|
||||
expect(
|
||||
isExpired(sessionFrom({ accessToken: 't', expiresIn: 3600 }, 0), 0),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
46
libs/web/identity/domain/src/lib/session.ts
Normal file
46
libs/web/identity/domain/src/lib/session.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/** Tokens issued by Pireon (through the API's /api/auth/token proxy). */
|
||||
export interface Session {
|
||||
accessToken: string;
|
||||
refreshToken: string | null;
|
||||
/** Epoch milliseconds. */
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export interface CurrentUser {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
appRoles: string[];
|
||||
}
|
||||
|
||||
/** How the browser can sign in (null config = the API has no Pireon client yet). */
|
||||
export interface AuthConfig {
|
||||
authorizeUrl: string;
|
||||
clientId: string;
|
||||
scopes: string[];
|
||||
passwordGrant: boolean;
|
||||
}
|
||||
|
||||
/** Refresh a little before the token actually expires. */
|
||||
export const EXPIRY_MARGIN_MS = 60_000;
|
||||
|
||||
export function isExpired(session: Session, now = Date.now()): boolean {
|
||||
return session.expiresAt - EXPIRY_MARGIN_MS <= now;
|
||||
}
|
||||
|
||||
export function sessionFrom(
|
||||
token: {
|
||||
accessToken: string;
|
||||
refreshToken?: string | null;
|
||||
expiresIn?: number | null;
|
||||
},
|
||||
now = Date.now(),
|
||||
): Session {
|
||||
// Personal access tokens are long-lived and come without expiry: one year.
|
||||
const expiresIn = token.expiresIn ?? 365 * 24 * 3600;
|
||||
return {
|
||||
accessToken: token.accessToken,
|
||||
refreshToken: token.refreshToken ?? null,
|
||||
expiresAt: now + expiresIn * 1000,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user