Initial Commit 🚀 🚀

This commit is contained in:
Abhishek Kumar 2025-09-09 14:37:32 +05:30
commit 4f2a629340
444 changed files with 76863 additions and 0 deletions

View 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
View 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';

View 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;
}

View 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
View 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;
}

View 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';

View 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;
}

View 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';
}
}

View 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
View 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;
}