die tenant admin oauth authentifizierung wurde implementiert und funktioniert jetzt. Zudem wurde das marketing frontend dashboard implementiert.
This commit is contained in:
172
resources/js/admin/lib/returnTo.ts
Normal file
172
resources/js/admin/lib/returnTo.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { ADMIN_LOGIN_PATH, ADMIN_LOGIN_START_PATH } from '../constants';
|
||||
|
||||
const LAST_DESTINATION_KEY = 'tenant.oauth.lastDestination';
|
||||
|
||||
function ensureLeadingSlash(target: string): string {
|
||||
if (!target) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(target)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
return target.startsWith('/') ? target : `/${target}`;
|
||||
}
|
||||
|
||||
function base64UrlEncode(value: string): string {
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(value);
|
||||
|
||||
let binary = '';
|
||||
bytes.forEach((byte) => {
|
||||
binary += String.fromCharCode(byte);
|
||||
});
|
||||
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/u, '');
|
||||
}
|
||||
|
||||
function base64UrlDecode(value: string): string | null {
|
||||
try {
|
||||
const padded = value.padEnd(value.length + ((4 - (value.length % 4)) % 4), '=');
|
||||
const normalized = padded.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const binary = atob(normalized);
|
||||
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
return decoder.decode(bytes);
|
||||
} catch (error) {
|
||||
console.warn('[Auth] Failed to decode return_to parameter', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeReturnTo(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return base64UrlEncode(trimmed);
|
||||
}
|
||||
|
||||
export function decodeReturnTo(value: string | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return base64UrlDecode(value);
|
||||
}
|
||||
|
||||
export interface ReturnTargetResolution {
|
||||
finalTarget: string;
|
||||
encodedFinal: string;
|
||||
}
|
||||
|
||||
export function resolveReturnTarget(raw: string | null, fallback: string): ReturnTargetResolution {
|
||||
const normalizedFallback = ensureLeadingSlash(fallback);
|
||||
|
||||
if (!raw) {
|
||||
const encoded = encodeReturnTo(normalizedFallback);
|
||||
return { finalTarget: normalizedFallback, encodedFinal: encoded };
|
||||
}
|
||||
|
||||
const decodedPrimary = decodeReturnTo(raw);
|
||||
if (!decodedPrimary) {
|
||||
const encoded = encodeReturnTo(normalizedFallback);
|
||||
return { finalTarget: normalizedFallback, encodedFinal: encoded };
|
||||
}
|
||||
|
||||
const normalizedPrimary = decodedPrimary.trim();
|
||||
|
||||
const wrapperPaths = [ADMIN_LOGIN_START_PATH, ADMIN_LOGIN_PATH];
|
||||
for (const wrapper of wrapperPaths) {
|
||||
if (normalizedPrimary.startsWith(wrapper)) {
|
||||
try {
|
||||
const url = new URL(normalizedPrimary, window.location.origin);
|
||||
const innerRaw = url.searchParams.get('return_to');
|
||||
if (!innerRaw) {
|
||||
const encoded = encodeReturnTo(normalizedFallback);
|
||||
return { finalTarget: normalizedFallback, encodedFinal: encoded };
|
||||
}
|
||||
|
||||
return resolveReturnTarget(innerRaw, normalizedFallback);
|
||||
} catch (error) {
|
||||
console.warn('[Auth] Failed to parse return_to chain', error);
|
||||
const encoded = encodeReturnTo(normalizedFallback);
|
||||
return { finalTarget: normalizedFallback, encodedFinal: encoded };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalTarget = ensureLeadingSlash(normalizedPrimary);
|
||||
const encodedFinal = encodeReturnTo(finalTarget);
|
||||
|
||||
return { finalTarget, encodedFinal };
|
||||
}
|
||||
|
||||
export function buildAdminOAuthStartPath(targetPath: string, encodedTarget?: string): string {
|
||||
const sanitizedTarget = ensureLeadingSlash(targetPath);
|
||||
const encoded = encodedTarget ?? encodeReturnTo(sanitizedTarget);
|
||||
const url = new URL(ADMIN_LOGIN_START_PATH, window.location.origin);
|
||||
url.searchParams.set('return_to', encoded);
|
||||
|
||||
return `${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
function resolveLocale(): string {
|
||||
const raw = document.documentElement.lang || 'de';
|
||||
const normalized = raw.toLowerCase();
|
||||
|
||||
if (normalized.includes('-')) {
|
||||
return normalized.split('-')[0] || 'de';
|
||||
}
|
||||
|
||||
return normalized || 'de';
|
||||
}
|
||||
|
||||
export function buildMarketingLoginUrl(returnPath: string): string {
|
||||
const sanitizedPath = ensureLeadingSlash(returnPath);
|
||||
const encoded = encodeReturnTo(sanitizedPath);
|
||||
const locale = resolveLocale();
|
||||
const loginPath = `/${locale}/login`;
|
||||
const url = new URL(loginPath, window.location.origin);
|
||||
url.searchParams.set('return_to', encoded);
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export function storeLastDestination(path: string): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitized = ensureLeadingSlash(path.trim());
|
||||
try {
|
||||
window.sessionStorage.setItem(LAST_DESTINATION_KEY, sanitized);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('[Auth] Failed to store last destination', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function consumeLastDestination(): string | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const value = window.sessionStorage.getItem(LAST_DESTINATION_KEY);
|
||||
if (value) {
|
||||
window.sessionStorage.removeItem(LAST_DESTINATION_KEY);
|
||||
return ensureLeadingSlash(value);
|
||||
}
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('[Auth] Failed to read last destination', error);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user