mirror of https://github.com/abpframework/abp.git
committed by
GitHub
87 changed files with 1177 additions and 348 deletions
@ -0,0 +1,29 @@ |
|||
import { |
|||
mergeApplicationConfig, |
|||
ApplicationConfig, |
|||
provideAppInitializer, |
|||
inject, |
|||
PLATFORM_ID, |
|||
TransferState |
|||
} from '@angular/core'; |
|||
import { isPlatformServer } from '@angular/common'; |
|||
import { provideServerRendering, withRoutes } from '@angular/ssr'; |
|||
|
|||
import { appConfig } from './app.config'; |
|||
import { appServerRoutes } from './app.routes.server'; |
|||
import { SSR_FLAG } from '@abp/ng.core'; |
|||
|
|||
const serverConfig: ApplicationConfig = { |
|||
providers: [ |
|||
provideAppInitializer(() => { |
|||
const platformId = inject(PLATFORM_ID); |
|||
const transferState = inject<TransferState>(TransferState); |
|||
if (isPlatformServer(platformId)) { |
|||
transferState.set(SSR_FLAG, true); |
|||
} |
|||
}), |
|||
provideServerRendering(withRoutes(appServerRoutes)), |
|||
], |
|||
}; |
|||
|
|||
export const config = mergeApplicationConfig(appConfig, serverConfig); |
|||
@ -0,0 +1,8 @@ |
|||
import { RenderMode, ServerRoute } from '@angular/ssr'; |
|||
|
|||
export const appServerRoutes: ServerRoute[] = [ |
|||
{ |
|||
path: '**', |
|||
renderMode: RenderMode.Server, |
|||
} |
|||
]; |
|||
@ -0,0 +1,7 @@ |
|||
import { bootstrapApplication } from '@angular/platform-browser'; |
|||
import { AppComponent } from './app/app.component'; |
|||
import { config } from './app/app.config.server'; |
|||
|
|||
const bootstrap = () => bootstrapApplication(AppComponent, config); |
|||
|
|||
export default bootstrap; |
|||
@ -0,0 +1,187 @@ |
|||
import { |
|||
AngularNodeAppEngine, |
|||
createNodeRequestHandler, |
|||
isMainModule, |
|||
writeResponseToNodeResponse, |
|||
} from '@angular/ssr/node'; |
|||
import express from 'express'; |
|||
import { dirname, resolve } from 'node:path'; |
|||
import { fileURLToPath } from 'node:url'; |
|||
import {environment} from './environments/environment'; |
|||
import * as oidc from 'openid-client'; |
|||
import { ServerCookieParser } from '@abp/ng.core'; |
|||
|
|||
if (environment.production === false) { |
|||
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; |
|||
} |
|||
|
|||
const serverDistFolder = dirname(fileURLToPath(import.meta.url)); |
|||
const browserDistFolder = resolve(serverDistFolder, '../browser'); |
|||
|
|||
const app = express(); |
|||
const angularApp = new AngularNodeAppEngine(); |
|||
|
|||
const ISSUER = new URL(environment.oAuthConfig.issuer); |
|||
const CLIENT_ID = environment.oAuthConfig.clientId; |
|||
const REDIRECT_URI = environment.oAuthConfig.redirectUri; |
|||
const SCOPE = environment.oAuthConfig.scope; |
|||
|
|||
const config = await oidc.discovery(ISSUER, CLIENT_ID, /* client_secret */ undefined); |
|||
const secureCookie = { httpOnly: true, sameSite: 'lax' as const, secure: environment.production, path: '/' }; |
|||
const tokenCookie = { ...secureCookie, httpOnly: false }; |
|||
|
|||
app.use(ServerCookieParser.middleware()); |
|||
|
|||
const sessions = new Map<string, { pkce?: string; state?: string; refresh?: string; at?: string, returnUrl?: string }>(); |
|||
|
|||
app.get('/authorize', async (_req, res) => { |
|||
const code_verifier = oidc.randomPKCECodeVerifier(); |
|||
const code_challenge = await oidc.calculatePKCECodeChallenge(code_verifier); |
|||
const state = oidc.randomState(); |
|||
|
|||
if (_req.query.returnUrl) { |
|||
const returnUrl = String(_req.query.returnUrl || null); |
|||
res.cookie('returnUrl', returnUrl, { ...secureCookie, maxAge: 5 * 60 * 1000 }); |
|||
} |
|||
|
|||
const sid = crypto.randomUUID(); |
|||
sessions.set(sid, { pkce: code_verifier, state }); |
|||
res.cookie('sid', sid, secureCookie); |
|||
|
|||
const url = oidc.buildAuthorizationUrl(config, { |
|||
redirect_uri: REDIRECT_URI, |
|||
scope: SCOPE, |
|||
code_challenge, |
|||
code_challenge_method: 'S256', |
|||
state, |
|||
}); |
|||
res.redirect(url.toString()); |
|||
}); |
|||
|
|||
app.get('/logout', async (req, res) => { |
|||
try { |
|||
const sid = req.cookies.sid; |
|||
|
|||
if (sid && sessions.has(sid)) { |
|||
sessions.delete(sid); |
|||
} |
|||
|
|||
res.clearCookie('sid', secureCookie); |
|||
res.clearCookie('access_token', tokenCookie); |
|||
res.clearCookie('refresh_token', secureCookie); |
|||
res.clearCookie('expires_at', tokenCookie); |
|||
res.clearCookie('returnUrl', secureCookie); |
|||
|
|||
const endSessionEndpoint = config.serverMetadata().end_session_endpoint; |
|||
if (endSessionEndpoint) { |
|||
const logoutUrl = new URL(endSessionEndpoint); |
|||
logoutUrl.searchParams.set('post_logout_redirect_uri', REDIRECT_URI); |
|||
logoutUrl.searchParams.set('client_id', CLIENT_ID); |
|||
|
|||
return res.redirect(logoutUrl.toString()); |
|||
} |
|||
res.redirect('/'); |
|||
|
|||
} catch (error) { |
|||
console.error('Logout error:', error); |
|||
res.status(500).send('Logout error'); |
|||
} |
|||
}); |
|||
|
|||
app.get('/', async (req, res, next) => { |
|||
try { |
|||
const { code, state } = req.query as any; |
|||
if (!code || !state) return next(); |
|||
|
|||
const sid = req.cookies.sid; |
|||
const sess = sid && sessions.get(sid); |
|||
if (!sess || state !== sess.state) return res.status(400).send('invalid state'); |
|||
|
|||
const tokenEndpoint = config.serverMetadata().token_endpoint!; |
|||
const body = new URLSearchParams({ |
|||
grant_type: 'authorization_code', |
|||
code: String(code), |
|||
redirect_uri: environment.oAuthConfig.redirectUri, |
|||
code_verifier: sess.pkce!, |
|||
client_id: CLIENT_ID |
|||
}); |
|||
|
|||
const resp = await fetch(tokenEndpoint, { |
|||
method: 'POST', |
|||
headers: { 'content-type': 'application/x-www-form-urlencoded' }, |
|||
body, |
|||
}); |
|||
|
|||
if (!resp.ok) { |
|||
const errTxt = await resp.text(); |
|||
console.error('token error:', resp.status, errTxt); |
|||
return res.status(500).send('token error'); |
|||
} |
|||
|
|||
const tokens = await resp.json(); |
|||
|
|||
const expiresInSec = |
|||
Number(tokens.expires_in ?? tokens.expiresIn ?? 3600); |
|||
const skewSec = 60; |
|||
const accessExpiresAt = new Date( |
|||
Date.now() + Math.max(0, expiresInSec - skewSec) * 1000 |
|||
); |
|||
|
|||
sessions.set(sid, { ...sess, at: tokens.access_token, refresh: tokens.refresh_token }); |
|||
res.cookie('access_token', tokens.access_token, {...tokenCookie, maxAge: accessExpiresAt.getTime()}); |
|||
res.cookie('refresh_token', tokens.refresh_token, secureCookie); |
|||
res.cookie('expires_at', String(accessExpiresAt.getTime()), tokenCookie); |
|||
|
|||
const returnUrl = req.cookies?.returnUrl ?? '/'; |
|||
res.clearCookie('returnUrl', secureCookie); |
|||
|
|||
return res.redirect(returnUrl); |
|||
} catch (e) { |
|||
console.error('OIDC error:', e); |
|||
return res.status(500).send('oidc error'); |
|||
} |
|||
}); |
|||
|
|||
/** |
|||
* Serve static files from /browser |
|||
*/ |
|||
app.use( |
|||
express.static(browserDistFolder, { |
|||
maxAge: '1y', |
|||
index: false, |
|||
redirect: false, |
|||
}), |
|||
); |
|||
|
|||
/** |
|||
* Handle all other requests by rendering the Angular application. |
|||
*/ |
|||
app.use((req, res, next) => { |
|||
angularApp |
|||
.handle(req) |
|||
.then(response => { |
|||
if (response) { |
|||
res.cookie('ssr-init', 'true', {...secureCookie, httpOnly: false}); |
|||
return writeResponseToNodeResponse(response, res); |
|||
} else { |
|||
return next() |
|||
} |
|||
}) |
|||
.catch(next); |
|||
}); |
|||
|
|||
/** |
|||
* Start the server if this module is the main entry point. |
|||
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. |
|||
*/ |
|||
if (isMainModule(import.meta.url)) { |
|||
const port = process.env['PORT'] || 4200; |
|||
app.listen(port, () => { |
|||
console.log(`Node Express server listening on http://localhost:${port}`); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions. |
|||
*/ |
|||
export const reqHandler = createNodeRequestHandler(app); |
|||
@ -1,2 +1,3 @@ |
|||
export * from './api.interceptor'; |
|||
export * from './timezone.interceptor'; |
|||
export * from './transfer-state.interceptor'; |
|||
|
|||
@ -1,26 +1,23 @@ |
|||
import { inject, Injectable } from '@angular/core'; |
|||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; |
|||
import { inject } from '@angular/core'; |
|||
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http'; |
|||
import { TimezoneService } from '../services'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class TimezoneInterceptor implements HttpInterceptor { |
|||
protected readonly timezoneService = inject(TimezoneService); |
|||
export const timezoneInterceptor: HttpInterceptorFn = ( |
|||
req: HttpRequest<any>, |
|||
next: HttpHandlerFn, |
|||
) => { |
|||
const timezoneService = inject(TimezoneService); |
|||
|
|||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { |
|||
if (!this.timezoneService.isUtcClockEnabled) { |
|||
return next.handle(req); |
|||
} |
|||
const timezone = this.timezoneService.timezone; |
|||
if (timezone) { |
|||
req = req.clone({ |
|||
setHeaders: { |
|||
__timezone: timezone, |
|||
}, |
|||
}); |
|||
} |
|||
return next.handle(req); |
|||
if (!timezoneService.isUtcClockEnabled) { |
|||
return next(req); |
|||
} |
|||
} |
|||
const timezone = timezoneService.timezone; |
|||
if (timezone) { |
|||
req = req.clone({ |
|||
setHeaders: { |
|||
__timezone: timezone, |
|||
}, |
|||
}); |
|||
} |
|||
return next(req); |
|||
}; |
|||
|
|||
@ -0,0 +1,42 @@ |
|||
import { inject, makeStateKey, PLATFORM_ID, TransferState } from '@angular/core'; |
|||
import { |
|||
HttpEvent, |
|||
HttpHandlerFn, |
|||
HttpInterceptorFn, |
|||
HttpRequest, |
|||
HttpResponse, |
|||
} from '@angular/common/http'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { isPlatformBrowser, isPlatformServer } from '@angular/common'; |
|||
import { tap } from 'rxjs/operators'; |
|||
|
|||
export const transferStateInterceptor: HttpInterceptorFn = ( |
|||
req: HttpRequest<any>, |
|||
next: HttpHandlerFn, |
|||
): Observable<HttpEvent<any>> => { |
|||
const transferState = inject(TransferState); |
|||
const platformId = inject(PLATFORM_ID); |
|||
|
|||
if (req.method !== 'GET') { |
|||
return next(req); |
|||
} |
|||
|
|||
const stateKey = makeStateKey<HttpResponse<any>>(req.urlWithParams); |
|||
|
|||
if (isPlatformBrowser(platformId)) { |
|||
const storedResponse = transferState.get<HttpResponse<any>>(stateKey, null); |
|||
if (storedResponse) { |
|||
transferState.remove(stateKey); |
|||
return of(new HttpResponse<any>({ body: storedResponse, status: 200 })); |
|||
} |
|||
} |
|||
|
|||
return next(req).pipe( |
|||
tap(event => { |
|||
if (isPlatformServer(platformId) && event instanceof HttpResponse) { |
|||
transferState.set(stateKey, event.body); |
|||
console.log(`Interceptor: ${req.urlWithParams} is stored in TransferState.`); |
|||
} |
|||
}), |
|||
); |
|||
}; |
|||
@ -0,0 +1,99 @@ |
|||
import { Injectable, PLATFORM_ID, inject, REQUEST } from '@angular/core'; |
|||
import { DOCUMENT, isPlatformBrowser } from '@angular/common'; |
|||
|
|||
@Injectable({ providedIn: 'root' }) |
|||
export class AbpCookieStorageService implements Storage { |
|||
private platformId = inject(PLATFORM_ID); |
|||
private document = inject(DOCUMENT); |
|||
private request = inject(REQUEST); |
|||
|
|||
get length(): number { |
|||
return isPlatformBrowser(this.platformId) ? this.keys().length : this.getCookiesFromRequest()?.size ?? 0; |
|||
} |
|||
|
|||
clear(): void { |
|||
if (!isPlatformBrowser(this.platformId)) return; |
|||
this.keys().forEach(k => this.removeItem(k)); |
|||
} |
|||
|
|||
getItem(key: string): string | null { |
|||
if (!isPlatformBrowser(this.platformId)) { |
|||
return this.getCookiesFromRequest()?.get(key) ?? null; |
|||
} |
|||
const name = key + '='; |
|||
const parts = (this.document.cookie || '').split('; '); |
|||
for (const p of parts) { |
|||
if (p.startsWith(name)) { |
|||
return decodeURIComponent(p.slice(name.length)); |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
key(index: number): string | null { |
|||
if (!isPlatformBrowser(this.platformId)) return null; |
|||
return this.keys()[index] ?? null; |
|||
} |
|||
|
|||
removeItem(key: string): void { |
|||
if (!isPlatformBrowser(this.platformId)) return; |
|||
this.setCookie(key, '', { 'max-age': -1, path: '/' }); |
|||
} |
|||
|
|||
setItem(key: string, value: string): void { |
|||
if (!isPlatformBrowser(this.platformId)) return; |
|||
this.setCookie(key, encodeURIComponent(value), { |
|||
path: '/', |
|||
sameSite: 'Lax', |
|||
secure: true, |
|||
}); |
|||
} |
|||
|
|||
setItemWithExpiry(key: string, value: string, seconds: number): void { |
|||
if (!isPlatformBrowser(this.platformId)) return; |
|||
this.setCookie(key, encodeURIComponent(value), { |
|||
path: '/', |
|||
sameSite: 'Lax', |
|||
secure: true, |
|||
'max-age': Math.max(0, Math.floor(seconds)), |
|||
}); |
|||
} |
|||
|
|||
private keys(): string[] { |
|||
const raw = (this.document.cookie || '').split('; ').filter(Boolean); |
|||
return raw |
|||
.map(c => decodeURIComponent(c.split('=')[0])); |
|||
} |
|||
|
|||
private setCookie(name: string, value: string, opts: { |
|||
path?: string; |
|||
domain?: string; |
|||
secure?: boolean; |
|||
sameSite?: 'Lax' | 'Strict' | 'None'; |
|||
expires?: Date; |
|||
'max-age'?: number; |
|||
}) { |
|||
let s = `${name}=${value}`; |
|||
if (opts.path) s += `; Path=${opts.path}`; |
|||
if (opts.domain) s += `; Domain=${opts.domain}`; |
|||
if (opts.sameSite) s += `; SameSite=${opts.sameSite}`; |
|||
if (opts.secure) s += `; Secure`; |
|||
if (opts.expires) s += `; Expires=${opts.expires.toUTCString()}`; |
|||
if (typeof opts['max-age'] === 'number') s += `; Max-Age=${opts['max-age']}`; |
|||
this.document.cookie = s; |
|||
} |
|||
|
|||
private getCookiesFromRequest(): Map<string, string> { |
|||
const cookies = new Map<string, string>(); |
|||
const cookieHeader = this.request?.headers.get('cookie') ?? ''; |
|||
for (const part of cookieHeader.split(';')) { |
|||
const i = part.indexOf('='); |
|||
if (i > -1) { |
|||
const k = part.slice(0, i).trim(); |
|||
const v = decodeURIComponent(part.slice(i + 1).trim()); |
|||
cookies.set(k, v); |
|||
} |
|||
} |
|||
return cookies; |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
import { inject, Injectable } from '@angular/core'; |
|||
import { DOCUMENT } from '@angular/common'; |
|||
import { DomStrategy } from '../strategies'; |
|||
|
|||
@Injectable({ providedIn: 'root' }) |
|||
export class DomStrategyService { |
|||
private document = inject(DOCUMENT); |
|||
|
|||
afterElement(el: HTMLElement) { |
|||
return new DomStrategy(() => el, 'afterend'); |
|||
} |
|||
|
|||
beforeElement(el: HTMLElement) { |
|||
return new DomStrategy(() => el, 'beforebegin'); |
|||
} |
|||
|
|||
appendToBody() { |
|||
return new DomStrategy(() => this.document.body, 'beforeend'); |
|||
} |
|||
|
|||
appendToHead() { |
|||
return new DomStrategy(() => this.document.head, 'beforeend'); |
|||
} |
|||
|
|||
prependToHead() { |
|||
return new DomStrategy(() => this.document.head, 'afterbegin'); |
|||
} |
|||
} |
|||
@ -1,28 +1,44 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { inject, Injectable, PLATFORM_ID } from '@angular/core'; |
|||
import { isPlatformBrowser } from '@angular/common'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class AbpLocalStorageService implements Storage { |
|||
constructor() {} |
|||
private platformId = inject(PLATFORM_ID); |
|||
|
|||
constructor() { |
|||
} |
|||
[name: string]: any; |
|||
get length(): number { |
|||
return localStorage.length; |
|||
return isPlatformBrowser(this.platformId) ? localStorage.length : 0; |
|||
} |
|||
|
|||
clear(): void { |
|||
localStorage.clear(); |
|||
if (isPlatformBrowser(this.platformId)) { |
|||
localStorage.clear(); |
|||
} |
|||
} |
|||
getItem(key: string): string { |
|||
getItem(key: string): string | null { |
|||
if (!isPlatformBrowser(this.platformId)) { |
|||
return null; |
|||
} |
|||
return localStorage.getItem(key); |
|||
} |
|||
key(index: number): string { |
|||
key(index: number): string | null { |
|||
if (!isPlatformBrowser(this.platformId)) { |
|||
return null; |
|||
} |
|||
return localStorage.key(index); |
|||
} |
|||
removeItem(key: string): void { |
|||
localStorage.removeItem(key); |
|||
if (isPlatformBrowser(this.platformId)) { |
|||
localStorage.removeItem(key); |
|||
} |
|||
} |
|||
setItem(key: string, value: string): void { |
|||
localStorage.setItem(key, value); |
|||
if (isPlatformBrowser(this.platformId)) { |
|||
localStorage.setItem(key, value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,28 +1,31 @@ |
|||
export class DomStrategy { |
|||
constructor( |
|||
public target: HTMLElement = document.head, |
|||
private getTarget: () => HTMLElement, |
|||
public position: InsertPosition = 'beforeend', |
|||
) {} |
|||
|
|||
insertElement<T extends HTMLElement>(element: T) { |
|||
this.target.insertAdjacentElement(this.position, element); |
|||
if (typeof document !== 'undefined') { |
|||
const target = this.getTarget(); |
|||
target.insertAdjacentElement(this.position, element); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export const DOM_STRATEGY = { |
|||
AfterElement(element: HTMLElement) { |
|||
return new DomStrategy(element, 'afterend'); |
|||
return new DomStrategy(() => element, 'afterend'); |
|||
}, |
|||
AppendToBody() { |
|||
return new DomStrategy(document.body, 'beforeend'); |
|||
return new DomStrategy(() => document?.body, 'beforeend'); |
|||
}, |
|||
AppendToHead() { |
|||
return new DomStrategy(document.head, 'beforeend'); |
|||
return new DomStrategy(() => document?.head, 'beforeend'); |
|||
}, |
|||
BeforeElement(element: HTMLElement) { |
|||
return new DomStrategy(element, 'beforebegin'); |
|||
return new DomStrategy(() => element, 'beforebegin'); |
|||
}, |
|||
PrependToHead() { |
|||
return new DomStrategy(document.head, 'afterbegin'); |
|||
return new DomStrategy(() => document?.head, 'afterbegin'); |
|||
}, |
|||
}; |
|||
|
|||
@ -0,0 +1,19 @@ |
|||
import { inject, InjectionToken, makeStateKey, PLATFORM_ID, TransferState } from '@angular/core'; |
|||
import { isPlatformBrowser } from '@angular/common'; |
|||
import { AbpCookieStorageService } from '../services'; |
|||
|
|||
export const SSR_FLAG = makeStateKey<boolean>('SSR_FLAG'); |
|||
|
|||
export const APP_STARTED_WITH_SSR = new InjectionToken<boolean>('APP_STARTED_WITH_SSR', { |
|||
providedIn: 'root', |
|||
factory: () => { |
|||
const platformId = inject(PLATFORM_ID); |
|||
const cookieService = inject(AbpCookieStorageService); |
|||
if (!isPlatformBrowser(platformId)) return true; |
|||
const ts = inject(TransferState); |
|||
const ssrEnabled = cookieService.getItem('ssr-init'); |
|||
// Remove the cookie after reading its value because it's only needed once
|
|||
cookieService.removeItem('ssr-init'); |
|||
return ts.get(SSR_FLAG, false) || ssrEnabled === 'true'; |
|||
}, |
|||
}); |
|||
@ -0,0 +1,44 @@ |
|||
export class ServerCookieParser { |
|||
static parse(cookieHeader: string): { [key: string]: string } { |
|||
const cookies: { [key: string]: string } = {}; |
|||
|
|||
if (!cookieHeader) return cookies; |
|||
|
|||
try { |
|||
cookieHeader.split(';').forEach(cookie => { |
|||
const parts = cookie.trim().split('='); |
|||
if (parts.length >= 2) { |
|||
const name = parts[0].trim(); |
|||
const value = parts.slice(1).join('='); |
|||
|
|||
if (name) { |
|||
try { |
|||
cookies[name] = decodeURIComponent(value); |
|||
} catch (e) { |
|||
cookies[name] = value; |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
} catch (error) { |
|||
console.error('Error parsing cookies:', error); |
|||
} |
|||
|
|||
return cookies; |
|||
} |
|||
|
|||
static middleware() { |
|||
return (req: any, res: any, next: any) => { |
|||
req.cookies = ServerCookieParser.parse(req.headers.cookie || ''); |
|||
next(); |
|||
}; |
|||
} |
|||
|
|||
static getCookie(req: any, name: string): string | undefined { |
|||
const cookieHeader = req.headers.cookie; |
|||
if (!cookieHeader) return undefined; |
|||
|
|||
const cookies = ServerCookieParser.parse(cookieHeader); |
|||
return cookies[name]; |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { OAuthStorage } from 'angular-oauth2-oidc'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class BrowserTokenStorageService implements OAuthStorage { |
|||
getItem(key: string): string { |
|||
return this.readCookie(key); |
|||
} |
|||
|
|||
removeItem(key: string): void { |
|||
this.removeCookie(key); |
|||
} |
|||
|
|||
setItem(key: string, data: string): void { |
|||
this.writeCookie(key, data); |
|||
} |
|||
|
|||
readCookie(name: string): string | null { |
|||
if (typeof document === 'undefined') return null; |
|||
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); |
|||
return match ? decodeURIComponent(match[2]) : null; |
|||
} |
|||
|
|||
writeCookie(name: string, value: string, days = 7): void { |
|||
if (typeof document === 'undefined') return; |
|||
const expires = new Date(Date.now() + days * 86400000).toUTCString(); |
|||
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; Secure; SameSite=Lax`; |
|||
} |
|||
|
|||
removeCookie(name: string): void { |
|||
if (typeof document === 'undefined') return; |
|||
document.cookie = `${name}=; Max-Age=0; path=/;`; |
|||
} |
|||
} |
|||
@ -1,3 +1,5 @@ |
|||
export * from './oauth.service'; |
|||
export * from './oauth-error-filter.service'; |
|||
export * from './remember-me.service' |
|||
export * from './remember-me.service'; |
|||
export * from './browser-token-storage.service'; |
|||
export * from './server-token-storage.service'; |
|||
|
|||
@ -1,32 +1,49 @@ |
|||
import { AbpLocalStorageService } from '@abp/ng.core'; |
|||
import { Injectable, inject } from '@angular/core'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class RememberMeService { |
|||
readonly #rememberMe = 'remember_me'; |
|||
protected readonly localStorageService = inject(AbpLocalStorageService); |
|||
|
|||
set(remember: boolean) { |
|||
this.localStorageService.setItem(this.#rememberMe, JSON.stringify(remember)); |
|||
} |
|||
|
|||
remove() { |
|||
this.localStorageService.removeItem(this.#rememberMe); |
|||
} |
|||
|
|||
get() { |
|||
return Boolean(JSON.parse(this.localStorageService.getItem(this.#rememberMe) || 'false')); |
|||
} |
|||
|
|||
getFromToken(accessToken: string) { |
|||
const tokenBody = accessToken.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); |
|||
try { |
|||
const parsedToken = JSON.parse(atob(tokenBody)); |
|||
return Boolean(parsedToken[this.#rememberMe]); |
|||
} catch { |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
import { |
|||
AbpCookieStorageService, |
|||
AbpLocalStorageService, |
|||
APP_STARTED_WITH_SSR, |
|||
} from '@abp/ng.core'; |
|||
import { Injectable, inject } from '@angular/core'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class RememberMeService { |
|||
readonly #rememberMe = 'remember_me'; |
|||
protected readonly localStorageService = inject(AbpLocalStorageService); |
|||
protected readonly cookieStorageService = inject(AbpCookieStorageService); |
|||
private appStartedWithSsr = inject(APP_STARTED_WITH_SSR, { optional: true }); |
|||
|
|||
set(remember: boolean) { |
|||
if (this.appStartedWithSsr) { |
|||
this.cookieStorageService.setItem(this.#rememberMe, JSON.stringify(remember)); |
|||
return; |
|||
} |
|||
this.localStorageService.setItem(this.#rememberMe, JSON.stringify(remember)); |
|||
} |
|||
|
|||
remove() { |
|||
if (this.appStartedWithSsr) { |
|||
this.cookieStorageService.removeItem(this.#rememberMe); |
|||
return; |
|||
} |
|||
this.localStorageService.removeItem(this.#rememberMe); |
|||
} |
|||
|
|||
get() { |
|||
if (this.appStartedWithSsr) { |
|||
return Boolean(JSON.parse(this.cookieStorageService.getItem(this.#rememberMe) || 'false')); |
|||
} |
|||
return Boolean(JSON.parse(this.localStorageService.getItem(this.#rememberMe) || 'false')); |
|||
} |
|||
|
|||
getFromToken(accessToken: string) { |
|||
const tokenBody = accessToken.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); |
|||
try { |
|||
const parsedToken = JSON.parse(atob(tokenBody)); |
|||
return Boolean(parsedToken[this.#rememberMe]); |
|||
} catch { |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,32 @@ |
|||
import { Inject, Injectable, Optional } from '@angular/core'; |
|||
import { OAuthStorage } from 'angular-oauth2-oidc'; |
|||
import { REQUEST } from '@angular/core'; |
|||
|
|||
@Injectable({ providedIn: null }) |
|||
export class ServerTokenStorageService implements OAuthStorage { |
|||
private cookies = new Map<string, string>(); |
|||
|
|||
constructor(@Optional() @Inject(REQUEST) private req: Request | null) { |
|||
const cookieHeader = this.req?.headers.get('cookie') ?? ''; |
|||
for (const part of cookieHeader.split(';')) { |
|||
const i = part.indexOf('='); |
|||
if (i > -1) { |
|||
const k = part.slice(0, i).trim(); |
|||
const v = decodeURIComponent(part.slice(i + 1).trim()); |
|||
this.cookies.set(k, v); |
|||
} |
|||
} |
|||
} |
|||
|
|||
getItem(key: string): string { |
|||
const fromCookie = this.cookies.get(key); |
|||
if (fromCookie) { |
|||
return fromCookie; |
|||
} |
|||
|
|||
return ''; |
|||
} |
|||
|
|||
setItem(_k: string, _v: string): void {} |
|||
removeItem(_k: string): void {} |
|||
} |
|||
@ -1,6 +1,39 @@ |
|||
import { inject, PLATFORM_ID } from '@angular/core'; |
|||
import { isPlatformBrowser } from '@angular/common'; |
|||
import { ServerTokenStorageService } from '../services/server-token-storage.service'; |
|||
import { BrowserTokenStorageService } from '../services'; |
|||
import { OAuthStorage } from 'angular-oauth2-oidc'; |
|||
import { oAuthStorage } from './oauth-storage'; |
|||
import { AbpLocalStorageService, APP_STARTED_WITH_SSR } from '@abp/ng.core'; |
|||
|
|||
export function storageFactory(): OAuthStorage { |
|||
return oAuthStorage; |
|||
export class MockStorage implements Storage { |
|||
private data = new Map<string, string>(); |
|||
get length() { |
|||
return this.data.size; |
|||
} |
|||
clear() { |
|||
this.data.clear(); |
|||
} |
|||
getItem(key: string) { |
|||
return this.data.get(key) || null; |
|||
} |
|||
key(index: number) { |
|||
return Array.from(this.data.keys())[index] || null; |
|||
} |
|||
removeItem(key: string) { |
|||
this.data.delete(key); |
|||
} |
|||
setItem(key: string, value: string) { |
|||
this.data.set(key, value); |
|||
} |
|||
} |
|||
|
|||
export function oAuthStorageFactory(): OAuthStorage { |
|||
const platformId = inject(PLATFORM_ID); |
|||
const appStartedWithSSR = inject(APP_STARTED_WITH_SSR); |
|||
if (appStartedWithSSR) { |
|||
return isPlatformBrowser(platformId) |
|||
? inject(BrowserTokenStorageService) |
|||
: inject(ServerTokenStorageService); |
|||
} |
|||
return inject(AbpLocalStorageService); |
|||
} |
|||
|
|||
@ -1,16 +1,18 @@ |
|||
<ul class="navbar-nav"> |
|||
@for (item of navItems.items$ | async; track $index) { |
|||
<ng-container *abpVisible="!item.visible || item.visible(item)"> |
|||
<li class="nav-item d-flex align-items-center" *abpPermission="item.requiredPolicy"> |
|||
@if (item.component) { |
|||
<ng-container |
|||
[ngComponentOutlet]="item.component" |
|||
[ngComponentOutletInjector]="item | toInjector" |
|||
></ng-container> |
|||
} @else { |
|||
<div [innerHTML]="item.html" (click)="item.action ? item.action() : null"></div> |
|||
} |
|||
</li> |
|||
</ng-container> |
|||
@if (isBrowser) { |
|||
@for (item of navItems.items$ | async; track $index) { |
|||
<ng-container *abpVisible="!item.visible || item.visible(item)"> |
|||
<li class="nav-item d-flex align-items-center" *abpPermission="item.requiredPolicy"> |
|||
@if (item.component) { |
|||
<ng-container |
|||
[ngComponentOutlet]="item.component" |
|||
[ngComponentOutletInjector]="item | toInjector" |
|||
></ng-container> |
|||
} @else { |
|||
<div [innerHTML]="item.html" (click)="item.action ? item.action() : null"></div> |
|||
} |
|||
</li> |
|||
</ng-container> |
|||
} |
|||
} |
|||
</ul> |
|||
|
|||
Loading…
Reference in new issue