mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-07 11:02:12 +02:00
Initial Commit 🚀 🚀
This commit is contained in:
commit
4f2a629340
444 changed files with 76863 additions and 0 deletions
69
ui/src/lib/auth/hooks/useAuth.ts
Normal file
69
ui/src/lib/auth/hooks/useAuth.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useAuthContext } from '../providers/AuthProvider';
|
||||
|
||||
export function useAuth() {
|
||||
const context = useAuthContext();
|
||||
|
||||
// Memoize functions that are recreated on every render
|
||||
const logout = React.useCallback(() => context.service.logout(), [context.service]);
|
||||
const redirectToLogin = React.useCallback(() => context.service.redirectToLogin(), [context.service]);
|
||||
const getSelectedTeam = React.useCallback(() => context.service.getSelectedTeam?.(), [context.service]);
|
||||
const listPermissions = React.useCallback(
|
||||
(team?: unknown) => context.service.listPermissions?.(team) || Promise.resolve([]),
|
||||
[context.service]
|
||||
);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
// Core functionality
|
||||
getAccessToken: context.getAccessToken,
|
||||
user: context.user, // This is now AuthUser (CurrentUser | LocalUser)
|
||||
isAuthenticated: context.isAuthenticated,
|
||||
loading: context.loading,
|
||||
|
||||
// Service methods
|
||||
logout,
|
||||
redirectToLogin,
|
||||
|
||||
// Provider info
|
||||
provider: context.provider,
|
||||
|
||||
// Stack-specific methods (optional)
|
||||
getSelectedTeam,
|
||||
listPermissions,
|
||||
}), [
|
||||
context.getAccessToken,
|
||||
context.user,
|
||||
context.isAuthenticated,
|
||||
context.loading,
|
||||
context.provider,
|
||||
logout,
|
||||
redirectToLogin,
|
||||
getSelectedTeam,
|
||||
listPermissions,
|
||||
]);
|
||||
}
|
||||
|
||||
// Compatibility wrapper for gradual migration from useUser
|
||||
export function useUser(options?: { or?: 'redirect' }) {
|
||||
const auth = useAuth();
|
||||
|
||||
// Handle redirect option
|
||||
if (options?.or === 'redirect' && !auth.isAuthenticated && !auth.loading) {
|
||||
auth.redirectToLogin();
|
||||
}
|
||||
|
||||
// Return Stack-compatible interface
|
||||
return {
|
||||
...auth.user,
|
||||
getAuthJson: async () => ({
|
||||
accessToken: await auth.getAccessToken(),
|
||||
}),
|
||||
selectedTeam: auth.getSelectedTeam(),
|
||||
listPermissions: auth.listPermissions,
|
||||
signOut: auth.logout,
|
||||
};
|
||||
}
|
||||
|
||||
11
ui/src/lib/auth/index.ts
Normal file
11
ui/src/lib/auth/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export { useAuth, useUser } from './hooks/useAuth';
|
||||
export { AuthProvider } from './providers/AuthProvider';
|
||||
export type {
|
||||
AuthProvider as AuthProviderType,
|
||||
AuthToken,
|
||||
AuthUser,
|
||||
BaseUser,
|
||||
LocalUser,
|
||||
TeamPermission
|
||||
} from './types';
|
||||
|
||||
140
ui/src/lib/auth/providers/AuthProvider.tsx
Normal file
140
ui/src/lib/auth/providers/AuthProvider.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
'use client';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import React, { createContext, lazy, Suspense, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { createAuthService, IAuthService, StackAuthService } from '../services';
|
||||
import type { AuthUser } from '../types';
|
||||
|
||||
interface AuthContextType {
|
||||
service: IAuthService;
|
||||
user: AuthUser | null; // Union type: CurrentUser | LocalUser
|
||||
isAuthenticated: boolean;
|
||||
loading: boolean;
|
||||
getAccessToken: () => Promise<string>;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
interface AuthContextProviderProps {
|
||||
service: IAuthService;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// Lazy load Stack components only when needed
|
||||
const StackProviderWrapper = lazy(() =>
|
||||
import('./StackProviderWrapper').then(module => ({
|
||||
default: module.StackProviderWrapper
|
||||
}))
|
||||
);
|
||||
|
||||
// Generic context provider for non-Stack providers
|
||||
function GenericAuthContextProvider({ service, children }: AuthContextProviderProps) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch current user
|
||||
const fetchUser = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const currentUser = await service.getCurrentUser();
|
||||
setUser(currentUser);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user:', error);
|
||||
setUser(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, [service]);
|
||||
|
||||
const getAccessToken = React.useCallback(() => service.getAccessToken(), [service]);
|
||||
|
||||
const contextValue: AuthContextType = React.useMemo(() => ({
|
||||
service,
|
||||
user,
|
||||
isAuthenticated: service.isAuthenticated(),
|
||||
loading,
|
||||
getAccessToken,
|
||||
provider: service.getProviderName(),
|
||||
}), [service, user, loading, getAccessToken]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
|
||||
const authService = useMemo(() => createAuthService(authProvider), [authProvider]);
|
||||
|
||||
// For Stack provider, wrap with StackProvider and use Stack-specific context
|
||||
if (authProvider === 'stack' && authService instanceof StackAuthService) {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-gray-600" />
|
||||
</div>
|
||||
}>
|
||||
<StackProviderWrapper service={authService}>
|
||||
{children}
|
||||
</StackProviderWrapper>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// For other providers, use generic context provider
|
||||
return (
|
||||
<GenericAuthContextProvider service={authService}>
|
||||
{children}
|
||||
</GenericAuthContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Export the context for Stack-specific provider
|
||||
export { AuthContext };
|
||||
|
||||
// Stack-specific context provider that uses the useUser hook
|
||||
export function StackAuthContextProvider({ service, children }: AuthContextProviderProps) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const stackUser: AuthUser | null = null;
|
||||
|
||||
useEffect(() => {
|
||||
// For Stack provider, we'll get the user from the StackProviderWrapper
|
||||
// This is a placeholder that will be overridden by the actual implementation
|
||||
if (service instanceof StackAuthService) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [service]);
|
||||
|
||||
const getAccessToken = React.useCallback(() => service.getAccessToken(), [service]);
|
||||
|
||||
const contextValue: AuthContextType = React.useMemo(() => ({
|
||||
service,
|
||||
user: stackUser,
|
||||
isAuthenticated: service.isAuthenticated(),
|
||||
loading,
|
||||
getAccessToken,
|
||||
provider: service.getProviderName(),
|
||||
}), [service, loading, getAccessToken]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuthContext() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuthContext must be used within AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
66
ui/src/lib/auth/providers/StackProviderWrapper.tsx
Normal file
66
ui/src/lib/auth/providers/StackProviderWrapper.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'use client';
|
||||
|
||||
import { StackClientApp,StackProvider, StackTheme, useUser as useStackUser } from '@stackframe/stack';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { StackAuthService } from '../services';
|
||||
import type { AuthUser } from '../types';
|
||||
import { AuthContext } from './AuthProvider';
|
||||
|
||||
interface StackProviderWrapperProps {
|
||||
service: StackAuthService;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// Stack-specific context provider that uses the useUser hook
|
||||
function StackAuthContextProvider({ service, children }: { service: StackAuthService; children: React.ReactNode }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const stackUser = useStackUser(); // Always call the hook
|
||||
|
||||
useEffect(() => {
|
||||
// Set the user instance in the service
|
||||
if (service instanceof StackAuthService && stackUser) {
|
||||
service.setUserInstance(stackUser);
|
||||
setLoading(false);
|
||||
} else if (!stackUser) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [service, stackUser]);
|
||||
|
||||
const getAccessToken = React.useCallback(() => service.getAccessToken(), [service]);
|
||||
|
||||
const contextValue = React.useMemo(() => ({
|
||||
service,
|
||||
user: stackUser as AuthUser, // Pass the actual Stack CurrentUser
|
||||
isAuthenticated: service.isAuthenticated(),
|
||||
loading,
|
||||
getAccessToken,
|
||||
provider: service.getProviderName(),
|
||||
}), [service, stackUser, loading, getAccessToken]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function StackProviderWrapper({ service, children }: StackProviderWrapperProps) {
|
||||
// Create the Stack client app here, only when actually needed
|
||||
const stackClientApp = new StackClientApp({
|
||||
tokenStore: "nextjs-cookie",
|
||||
urls: {
|
||||
afterSignIn: "/after-sign-in"
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<StackProvider app={stackClientApp}>
|
||||
<StackTheme>
|
||||
<StackAuthContextProvider service={service}>
|
||||
{children}
|
||||
</StackAuthContextProvider>
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
);
|
||||
}
|
||||
150
ui/src/lib/auth/server.ts
Normal file
150
ui/src/lib/auth/server.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import "server-only";
|
||||
|
||||
import type { CurrentUser, StackServerApp } from '@stackframe/stack';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import logger from '@/lib/logger';
|
||||
|
||||
import type { LocalUser } from './types';
|
||||
|
||||
// Server-side auth utilities for SSR pages
|
||||
// This file should only be imported in server components
|
||||
|
||||
let stackServerApp: StackServerApp<boolean, string> | null = null;
|
||||
const OSS_TOKEN_COOKIE = 'dograh_oss_token';
|
||||
const OSS_USER_COOKIE = 'dograh_oss_user';
|
||||
|
||||
// Lazy load and cache the stack server app
|
||||
async function getStackServerApp(): Promise<StackServerApp<boolean, string> | null> {
|
||||
if (!stackServerApp) {
|
||||
// Only import if using Stack provider
|
||||
const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
|
||||
if (authProvider === 'stack') {
|
||||
const stackModule = await import('@stackframe/stack');
|
||||
const { StackServerApp } = stackModule;
|
||||
stackServerApp = new StackServerApp({
|
||||
tokenStore: "nextjs-cookie",
|
||||
urls: {
|
||||
afterSignIn: "/after-sign-in"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return stackServerApp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user on the server side (for SSR)
|
||||
* Returns CurrentUser for stack, LocalUser for OSS, or null if not authenticated
|
||||
*/
|
||||
export async function getServerUser(): Promise<CurrentUser | LocalUser | null> {
|
||||
const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
|
||||
|
||||
if (authProvider === 'stack') {
|
||||
const app = await getStackServerApp();
|
||||
if (app) {
|
||||
try {
|
||||
const user = await app.getUser();
|
||||
return user;
|
||||
} catch (error) {
|
||||
logger.error('Error getting user from Stack:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if (authProvider === 'local') {
|
||||
// For OSS mode, get user from cookies (created by middleware)
|
||||
const user = await getOSSUser();
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated on the server side
|
||||
* For local provider, always returns true in development
|
||||
*/
|
||||
export async function isServerAuthenticated(): Promise<boolean> {
|
||||
const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
|
||||
|
||||
if (authProvider === 'stack') {
|
||||
const user = await getServerUser();
|
||||
return !!user;
|
||||
}
|
||||
|
||||
// For local provider, consider authenticated in development
|
||||
if (authProvider === 'local') {
|
||||
return process.env.NODE_ENV === 'development';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider name for server-side rendering
|
||||
*/
|
||||
export function getServerAuthProvider(): string {
|
||||
return process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OSS token from cookies (read-only)
|
||||
* Token creation happens in middleware
|
||||
*/
|
||||
export async function getOSSToken(): Promise<string | null> {
|
||||
const cookieStore = await cookies();
|
||||
return cookieStore.get(OSS_TOKEN_COOKIE)?.value || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OSS user from cookies
|
||||
*/
|
||||
export async function getOSSUser(): Promise<LocalUser | null> {
|
||||
const cookieStore = await cookies();
|
||||
const userCookie = cookieStore.get(OSS_USER_COOKIE)?.value;
|
||||
|
||||
if (userCookie) {
|
||||
try {
|
||||
return JSON.parse(userCookie);
|
||||
} catch (error) {
|
||||
logger.error('Error listing permissions:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// If no user cookie, but we have a token, create user
|
||||
const token = cookieStore.get(OSS_TOKEN_COOKIE)?.value;
|
||||
if (token) {
|
||||
const user: LocalUser = {
|
||||
id: token,
|
||||
name: 'Local User',
|
||||
provider: 'local',
|
||||
organizationId: `org_${token}`,
|
||||
};
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token for API calls
|
||||
*/
|
||||
export async function getServerAccessToken(): Promise<string | null> {
|
||||
const authProvider = getServerAuthProvider();
|
||||
|
||||
if (authProvider === 'stack') {
|
||||
const user = await getServerUser();
|
||||
if (user && 'getAuthJson' in user) {
|
||||
const auth = await user.getAuthJson();
|
||||
return auth?.accessToken ?? null;
|
||||
}
|
||||
} else if (authProvider === 'local') {
|
||||
// Get token from cookies (created by middleware)
|
||||
const oss_token = await getOSSToken();
|
||||
logger.debug(`oss_token: ${oss_token}`);
|
||||
return oss_token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
28
ui/src/lib/auth/services/index.ts
Normal file
28
ui/src/lib/auth/services/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import type { AuthProvider } from '../types';
|
||||
import type { IAuthService } from './interface';
|
||||
import { LocalAuthService } from './localAuthService';
|
||||
import { StackAuthService } from './stackAuthService';
|
||||
|
||||
export function createAuthService(provider?: AuthProvider | string): IAuthService {
|
||||
const authProvider = provider || process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
|
||||
|
||||
switch (authProvider) {
|
||||
case 'stack':
|
||||
return new StackAuthService();
|
||||
case 'local':
|
||||
return new LocalAuthService();
|
||||
// Future providers can be added here
|
||||
// case 'auth0':
|
||||
// return new Auth0Service();
|
||||
// case 'supabase':
|
||||
// return new SupabaseService();
|
||||
default:
|
||||
console.warn(`Unknown auth provider: ${authProvider}, falling back to local`);
|
||||
return new LocalAuthService();
|
||||
}
|
||||
}
|
||||
|
||||
export type { IAuthService } from './interface';
|
||||
export { LocalAuthService } from './localAuthService';
|
||||
export { StackAuthService } from './stackAuthService';
|
||||
|
||||
23
ui/src/lib/auth/services/interface.ts
Normal file
23
ui/src/lib/auth/services/interface.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { AuthUser } from '../types';
|
||||
|
||||
export interface IAuthService {
|
||||
// Token management
|
||||
getAccessToken(): Promise<string>;
|
||||
refreshToken(): Promise<string>;
|
||||
|
||||
// User management
|
||||
getCurrentUser(): Promise<AuthUser | null>;
|
||||
isAuthenticated(): boolean;
|
||||
|
||||
// Navigation
|
||||
redirectToLogin(): void;
|
||||
logout(): Promise<void>;
|
||||
|
||||
// Team/Organization management (optional for some providers)
|
||||
getSelectedTeam?(): unknown;
|
||||
listPermissions?(team?: unknown): Promise<Array<{ id: string }>>;
|
||||
|
||||
// Provider-specific
|
||||
getProviderName(): string;
|
||||
}
|
||||
|
||||
109
ui/src/lib/auth/services/localAuthService.ts
Normal file
109
ui/src/lib/auth/services/localAuthService.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
'use client';
|
||||
|
||||
import logger from '@/lib/logger';
|
||||
|
||||
import type { LocalUser } from '../types';
|
||||
import type { IAuthService } from './interface';
|
||||
|
||||
export class LocalAuthService implements IAuthService {
|
||||
private currentUser: LocalUser | null = null;
|
||||
private currentToken: string | null = null;
|
||||
private authPromise: Promise<void> | null = null;
|
||||
private static instance: LocalAuthService | null = null;
|
||||
|
||||
constructor() {
|
||||
// Singleton pattern to ensure single initialization
|
||||
if (LocalAuthService.instance) {
|
||||
return LocalAuthService.instance;
|
||||
}
|
||||
LocalAuthService.instance = this;
|
||||
|
||||
// Initialize auth on creation
|
||||
if (typeof window !== 'undefined') {
|
||||
this.authPromise = this.initializeAuth();
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeAuth(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/api/auth/oss');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.currentToken = data.token;
|
||||
this.currentUser = data.user;
|
||||
logger.info('OSS auth initialized', { user: data.user });
|
||||
} else {
|
||||
logger.error('Failed to initialize OSS auth');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error initializing OSS auth', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureAuth(): Promise<void> {
|
||||
if (this.authPromise) {
|
||||
await this.authPromise;
|
||||
} else if (!this.currentToken && typeof window !== 'undefined') {
|
||||
this.authPromise = this.initializeAuth();
|
||||
await this.authPromise;
|
||||
}
|
||||
}
|
||||
|
||||
async getAccessToken(): Promise<string> {
|
||||
if (typeof window === 'undefined') {
|
||||
// SSR: Server will handle this
|
||||
return 'ssr-placeholder-token';
|
||||
}
|
||||
|
||||
await this.ensureAuth();
|
||||
|
||||
if (!this.currentToken) {
|
||||
logger.warn('No OSS token available after initialization');
|
||||
return '';
|
||||
}
|
||||
return this.currentToken;
|
||||
}
|
||||
|
||||
async refreshToken(): Promise<string> {
|
||||
// For local mode, just return the same token
|
||||
return this.getAccessToken();
|
||||
}
|
||||
|
||||
async getCurrentUser(): Promise<LocalUser | null> {
|
||||
if (typeof window === 'undefined') {
|
||||
// SSR: Server will handle this
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.ensureAuth();
|
||||
|
||||
if (!this.currentUser) {
|
||||
logger.warn('No OSS user available after initialization');
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.currentUser;
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
// In local mode, always authenticated
|
||||
return true;
|
||||
}
|
||||
|
||||
redirectToLogin(): void {
|
||||
// No-op for local mode
|
||||
logger.info('Login redirect not needed in local mode');
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
// In OSS mode, logout would require server-side cookie clearing
|
||||
// For now, just clear the cached user
|
||||
this.currentUser = null;
|
||||
logger.info('Logout requested in OSS mode - server cookies need to be cleared');
|
||||
}
|
||||
|
||||
getProviderName(): string {
|
||||
return 'local';
|
||||
}
|
||||
}
|
||||
|
||||
86
ui/src/lib/auth/services/stackAuthService.ts
Normal file
86
ui/src/lib/auth/services/stackAuthService.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
'use client';
|
||||
|
||||
import type { CurrentUser } from '@stackframe/stack';
|
||||
|
||||
import logger from '@/lib/logger';
|
||||
|
||||
import type { IAuthService } from './interface';
|
||||
|
||||
export class StackAuthService implements IAuthService {
|
||||
private userInstance: CurrentUser | null = null;
|
||||
|
||||
// Set the user instance from the Stack useUser hook
|
||||
setUserInstance(user: CurrentUser) {
|
||||
this.userInstance = user;
|
||||
}
|
||||
|
||||
async getAccessToken(): Promise<string> {
|
||||
if (!this.userInstance) {
|
||||
throw new Error('User not initialized');
|
||||
}
|
||||
const authJson = await this.userInstance.getAuthJson();
|
||||
if (!authJson.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
return authJson.accessToken;
|
||||
}
|
||||
|
||||
async refreshToken(): Promise<string> {
|
||||
if (!this.userInstance) {
|
||||
throw new Error('User not initialized');
|
||||
}
|
||||
// Stack handles token refresh internally
|
||||
const authJson = await this.userInstance.getAuthJson();
|
||||
if (!authJson.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
return authJson.accessToken;
|
||||
}
|
||||
|
||||
async getCurrentUser(): Promise<CurrentUser | null> {
|
||||
// Return the actual Stack user instance
|
||||
return this.userInstance;
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return !!this.userInstance;
|
||||
}
|
||||
|
||||
redirectToLogin(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/handler/sign-in';
|
||||
}
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
if (this.userInstance && this.userInstance.signOut) {
|
||||
await this.userInstance.signOut();
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedTeam(): unknown {
|
||||
return this.userInstance?.selectedTeam;
|
||||
}
|
||||
|
||||
async listPermissions(team?: unknown): Promise<Array<{ id: string }>> {
|
||||
if (!this.userInstance || !this.userInstance.listPermissions) {
|
||||
return [];
|
||||
}
|
||||
const targetTeam = team || this.userInstance.selectedTeam;
|
||||
if (!targetTeam) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const perms = await this.userInstance.listPermissions(targetTeam);
|
||||
return Array.isArray(perms) ? perms : [];
|
||||
} catch (error) {
|
||||
logger.error('Error listing permissions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getProviderName(): string {
|
||||
return 'stack';
|
||||
}
|
||||
}
|
||||
|
||||
38
ui/src/lib/auth/types.ts
Normal file
38
ui/src/lib/auth/types.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { CurrentUser } from '@stackframe/stack';
|
||||
|
||||
// Base user interface that all providers must support
|
||||
export interface BaseUser {
|
||||
id: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
// Local/OSS user type
|
||||
export interface LocalUser extends BaseUser {
|
||||
provider: 'local';
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
// Union type for all user types
|
||||
export type AuthUser = CurrentUser | LocalUser;
|
||||
|
||||
|
||||
export interface AuthToken {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: number;
|
||||
}
|
||||
|
||||
export interface TeamPermission {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type AuthProvider = 'stack' | 'local';
|
||||
|
||||
export interface AuthConfig {
|
||||
provider: AuthProvider;
|
||||
// Provider-specific configuration
|
||||
[key: string]: string | number | boolean;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue