@startsimpli/auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @startsimpli/auth - Shared authentication package
3
+ *
4
+ * Provides JWT-based authentication for Next.js apps with Django backend
5
+ *
6
+ * NOTE: Server-only utilities (using next/headers) are available via '@startsimpli/auth/server'
7
+ * NOTE: Functional auth API (signInWithCredentials, authFetch, etc.) is available via '@startsimpli/auth/client'
8
+ */
9
+
10
+ // Re-export client-safe code only
11
+ export * from './types';
12
+ export * from './utils';
13
+ // Export client without AuthUser to avoid duplicate with types/AuthUser.
14
+ // Consumers needing the functional AuthUser should import from '@startsimpli/auth/client'.
15
+ export {
16
+ AuthClient,
17
+ AuthProvider,
18
+ useAuthContext,
19
+ useAuth,
20
+ usePermissions,
21
+ resolveAuthUrl,
22
+ getAccessToken,
23
+ setAccessToken,
24
+ signInWithCredentials,
25
+ registerAccount,
26
+ requestPasswordReset,
27
+ resetPassword,
28
+ verifyEmail,
29
+ resendVerification,
30
+ initiateGoogleOAuth,
31
+ completeGoogleOAuth,
32
+ refreshAccessToken,
33
+ getMe,
34
+ signOut,
35
+ authFetch,
36
+ hasPermission,
37
+ hasGroup,
38
+ } from './client';
39
+ export type { UseAuthReturn, UsePermissionsReturn } from './client';
40
+
41
+ // DO NOT export './server' here - it uses next/headers and must be imported explicitly via '@startsimpli/auth/server'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Auth guards for API routes
3
+ */
4
+
5
+ import { NextRequest, NextResponse } from 'next/server';
6
+ import type { CompanyRole } from '../types';
7
+ import { hasRolePermission } from '../types';
8
+ import { getRequestToken } from './middleware';
9
+
10
+ /**
11
+ * Auth guard result
12
+ */
13
+ export interface GuardResult {
14
+ authorized: boolean;
15
+ response?: NextResponse;
16
+ token?: string;
17
+ }
18
+
19
+ /**
20
+ * Require authentication for API route
21
+ */
22
+ export async function requireAuthGuard(
23
+ request: NextRequest
24
+ ): Promise<GuardResult> {
25
+ const token = getRequestToken(request);
26
+
27
+ if (!token) {
28
+ return {
29
+ authorized: false,
30
+ response: NextResponse.json(
31
+ { error: 'Unauthorized' },
32
+ { status: 401 }
33
+ ),
34
+ };
35
+ }
36
+
37
+ return {
38
+ authorized: true,
39
+ token,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Require specific role for API route
45
+ */
46
+ export async function requireRoleGuard(
47
+ request: NextRequest,
48
+ requiredRole: CompanyRole,
49
+ getUserRole: () => Promise<CompanyRole | null>
50
+ ): Promise<GuardResult> {
51
+ const authResult = await requireAuthGuard(request);
52
+
53
+ if (!authResult.authorized) {
54
+ return authResult;
55
+ }
56
+
57
+ const userRole = await getUserRole();
58
+
59
+ if (!userRole || !hasRolePermission(userRole, requiredRole)) {
60
+ return {
61
+ authorized: false,
62
+ response: NextResponse.json(
63
+ { error: 'Forbidden' },
64
+ { status: 403 }
65
+ ),
66
+ };
67
+ }
68
+
69
+ return {
70
+ authorized: true,
71
+ token: authResult.token,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Higher-order function to wrap API route with auth guard
77
+ */
78
+ export function withAuth<T = any>(
79
+ handler: (request: NextRequest, token: string) => Promise<NextResponse<T>>
80
+ ) {
81
+ return async (request: NextRequest): Promise<NextResponse> => {
82
+ const guardResult = await requireAuthGuard(request);
83
+
84
+ if (!guardResult.authorized) {
85
+ return guardResult.response!;
86
+ }
87
+
88
+ return handler(request, guardResult.token!);
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Higher-order function to wrap API route with role guard
94
+ */
95
+ export function withRole<T = any>(
96
+ requiredRole: CompanyRole,
97
+ getUserRole: () => Promise<CompanyRole | null>,
98
+ handler: (request: NextRequest, token: string) => Promise<NextResponse<T>>
99
+ ) {
100
+ return async (request: NextRequest): Promise<NextResponse> => {
101
+ const guardResult = await requireRoleGuard(
102
+ request,
103
+ requiredRole,
104
+ getUserRole
105
+ );
106
+
107
+ if (!guardResult.authorized) {
108
+ return guardResult.response!;
109
+ }
110
+
111
+ return handler(request, guardResult.token!);
112
+ };
113
+ }
@@ -0,0 +1,20 @@
1
+ export {
2
+ getServerSession,
3
+ validateSession,
4
+ requireAuth,
5
+ refreshServerToken,
6
+ } from './session';
7
+ export {
8
+ createAuthMiddleware,
9
+ hasValidToken,
10
+ getRequestToken,
11
+ getTokenFromRequest,
12
+ type AuthMiddlewareConfig,
13
+ } from './middleware';
14
+ export {
15
+ requireAuthGuard,
16
+ requireRoleGuard,
17
+ withAuth,
18
+ withRole,
19
+ type GuardResult,
20
+ } from './guards';
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Next.js middleware helpers for authentication
3
+ */
4
+
5
+ import { NextRequest, NextResponse } from 'next/server';
6
+ import { isTokenExpired } from '../utils';
7
+
8
+ export interface AuthMiddlewareConfig {
9
+ apiBaseUrl: string;
10
+ publicPaths?: string[];
11
+ loginPath?: string;
12
+ }
13
+
14
+ /**
15
+ * Create auth middleware for Next.js
16
+ */
17
+ export function createAuthMiddleware(config: AuthMiddlewareConfig) {
18
+ const {
19
+ apiBaseUrl,
20
+ publicPaths = ['/login', '/register', '/forgot-password'],
21
+ loginPath = '/login',
22
+ } = config;
23
+
24
+ return async function authMiddleware(request: NextRequest) {
25
+ const { pathname } = request.nextUrl;
26
+
27
+ const isPublicPath = publicPaths.some((path) =>
28
+ pathname.startsWith(path)
29
+ );
30
+
31
+ if (isPublicPath) {
32
+ return NextResponse.next();
33
+ }
34
+
35
+ const accessToken = request.cookies.get('access_token')?.value;
36
+
37
+ if (!accessToken || isTokenExpired(accessToken)) {
38
+ const url = request.nextUrl.clone();
39
+ url.pathname = loginPath;
40
+ url.searchParams.set('from', pathname);
41
+ return NextResponse.redirect(url);
42
+ }
43
+
44
+ return NextResponse.next();
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Check if request has valid auth token
50
+ */
51
+ export function hasValidToken(request: NextRequest): boolean {
52
+ const accessToken = request.cookies.get('access_token')?.value;
53
+
54
+ if (!accessToken) {
55
+ return false;
56
+ }
57
+
58
+ return !isTokenExpired(accessToken);
59
+ }
60
+
61
+ /**
62
+ * Get access token from request
63
+ */
64
+ export function getRequestToken(request: NextRequest): string | null {
65
+ const accessToken = request.cookies.get('access_token')?.value;
66
+
67
+ if (!accessToken) {
68
+ return null;
69
+ }
70
+
71
+ if (isTokenExpired(accessToken)) {
72
+ return null;
73
+ }
74
+
75
+ return accessToken;
76
+ }
77
+
78
+ /**
79
+ * Extract bearer token from a standard Request (Authorization header or access_token cookie).
80
+ * Framework-agnostic — works with any Request-compatible object (Next.js, Node, edge runtimes).
81
+ * Returns the raw token string without expiry validation.
82
+ */
83
+ export function getTokenFromRequest(req: Request): string | undefined {
84
+ // Authorization: Bearer <token>
85
+ const authHeader = req.headers.get('authorization');
86
+ if (authHeader?.startsWith('Bearer ')) {
87
+ const token = authHeader.slice(7).trim();
88
+ if (token) return token;
89
+ }
90
+
91
+ // Fallback: access_token cookie (parsed from Cookie header)
92
+ const cookieHeader = req.headers.get('cookie');
93
+ if (cookieHeader) {
94
+ const match = cookieHeader
95
+ .split(';')
96
+ .map((part) => part.trim())
97
+ .find((part) => part.startsWith('access_token='));
98
+
99
+ if (match) {
100
+ const token = match.slice('access_token='.length).trim();
101
+ if (token) return token;
102
+ }
103
+ }
104
+
105
+ return undefined;
106
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Server-side session validation
3
+ * For Next.js API routes and server components
4
+ */
5
+
6
+ import { cookies } from 'next/headers';
7
+ import type { Session, AuthUser, TokenPayload } from '../types';
8
+ import { decodeToken, isTokenExpired } from '../utils';
9
+
10
+ /**
11
+ * Get session from server-side cookies and headers
12
+ */
13
+ export async function getServerSession(
14
+ apiBaseUrl: string
15
+ ): Promise<Session | null> {
16
+ const cookieStore = await cookies();
17
+ const accessToken = cookieStore.get('access_token')?.value;
18
+
19
+ if (!accessToken) {
20
+ return null;
21
+ }
22
+
23
+ if (isTokenExpired(accessToken)) {
24
+ return null;
25
+ }
26
+
27
+ try {
28
+ const user = await fetchUser(apiBaseUrl, accessToken);
29
+ const payload = decodeToken(accessToken);
30
+
31
+ if (!payload) {
32
+ return null;
33
+ }
34
+
35
+ return {
36
+ user,
37
+ accessToken,
38
+ expiresAt: payload.exp * 1000,
39
+ };
40
+ } catch (error) {
41
+ console.error('Failed to get server session:', error);
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Fetch user data from Django backend
48
+ */
49
+ async function fetchUser(
50
+ apiBaseUrl: string,
51
+ accessToken: string
52
+ ): Promise<AuthUser> {
53
+ const response = await fetch(`${apiBaseUrl}/api/v1/auth/me/`, {
54
+ headers: {
55
+ Authorization: `Bearer ${accessToken}`,
56
+ },
57
+ cache: 'no-store',
58
+ });
59
+
60
+ if (!response.ok) {
61
+ throw new Error('Failed to fetch user');
62
+ }
63
+
64
+ return response.json();
65
+ }
66
+
67
+ /**
68
+ * Validate session and return user or null
69
+ */
70
+ export async function validateSession(
71
+ apiBaseUrl: string
72
+ ): Promise<AuthUser | null> {
73
+ const session = await getServerSession(apiBaseUrl);
74
+ return session?.user || null;
75
+ }
76
+
77
+ /**
78
+ * Require authenticated session (throws if not authenticated)
79
+ */
80
+ export async function requireAuth(apiBaseUrl: string): Promise<Session> {
81
+ const session = await getServerSession(apiBaseUrl);
82
+
83
+ if (!session) {
84
+ throw new Error('Unauthorized');
85
+ }
86
+
87
+ return session;
88
+ }
89
+
90
+ /**
91
+ * Refresh access token using refresh token cookie
92
+ */
93
+ export async function refreshServerToken(
94
+ apiBaseUrl: string
95
+ ): Promise<string | null> {
96
+ try {
97
+ const response = await fetch(`${apiBaseUrl}/api/v1/auth/token/refresh/`, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ },
102
+ credentials: 'include',
103
+ });
104
+
105
+ if (!response.ok) {
106
+ return null;
107
+ }
108
+
109
+ const data = await response.json();
110
+ return data.access;
111
+ } catch (error) {
112
+ console.error('Token refresh failed:', error);
113
+ return null;
114
+ }
115
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Authentication types for StartSimpli apps
3
+ */
4
+
5
+ /**
6
+ * Generic token pair (access + optional refresh)
7
+ */
8
+ export interface TokenPair {
9
+ access: string;
10
+ refresh?: string;
11
+ }
12
+
13
+ /**
14
+ * Generic decoded JWT payload — framework and backend agnostic
15
+ */
16
+ export interface DecodedToken {
17
+ sub?: string;
18
+ email?: string;
19
+ exp?: number;
20
+ iat?: number;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ /**
25
+ * Generic auth session — framework agnostic
26
+ */
27
+ export interface AuthSession {
28
+ user: {
29
+ id: string;
30
+ email: string;
31
+ [key: string]: unknown;
32
+ };
33
+ accessToken: string;
34
+ refreshToken?: string;
35
+ }
36
+
37
+ /**
38
+ * User profile from Django backend
39
+ */
40
+ export interface AuthUser {
41
+ id: string;
42
+ email: string;
43
+ firstName: string;
44
+ lastName: string;
45
+ isEmailVerified: boolean;
46
+ createdAt: string;
47
+ updatedAt: string;
48
+ // Company/team context (if applicable)
49
+ companies?: Array<{
50
+ id: string;
51
+ name: string;
52
+ role: 'owner' | 'admin' | 'member' | 'viewer';
53
+ }>;
54
+ currentCompanyId?: string;
55
+ }
56
+
57
+ /**
58
+ * JWT token payload structure
59
+ */
60
+ export interface TokenPayload {
61
+ token_type: 'access';
62
+ exp: number;
63
+ iat: number;
64
+ jti: string;
65
+ user_id: string;
66
+ }
67
+
68
+ /**
69
+ * Session data stored in client
70
+ */
71
+ export interface Session {
72
+ user: AuthUser;
73
+ accessToken: string;
74
+ expiresAt: number;
75
+ }
76
+
77
+ /**
78
+ * Login response from Django backend
79
+ */
80
+ export interface LoginResponse {
81
+ access: string;
82
+ user: AuthUser;
83
+ }
84
+
85
+ /**
86
+ * Token refresh response
87
+ */
88
+ export interface RefreshResponse {
89
+ access: string;
90
+ }
91
+
92
+ /**
93
+ * Permission check result
94
+ */
95
+ export interface PermissionCheck {
96
+ hasPermission: boolean;
97
+ reason?: string;
98
+ }
99
+
100
+ /**
101
+ * Auth configuration options
102
+ */
103
+ export interface AuthConfig {
104
+ apiBaseUrl: string;
105
+ tokenRefreshInterval?: number; // milliseconds, default 4 minutes
106
+ onSessionExpired?: () => void;
107
+ onUnauthorized?: () => void;
108
+ }
109
+
110
+ /**
111
+ * Auth state for React context
112
+ */
113
+ export interface AuthState {
114
+ session: Session | null;
115
+ isLoading: boolean;
116
+ isAuthenticated: boolean;
117
+ }
118
+
119
+ /**
120
+ * Company role hierarchy
121
+ */
122
+ export type CompanyRole = 'owner' | 'admin' | 'member' | 'viewer';
123
+
124
+ /**
125
+ * Role hierarchy map (higher number = more permissions)
126
+ */
127
+ export const ROLE_HIERARCHY: Record<CompanyRole, number> = {
128
+ owner: 4,
129
+ admin: 3,
130
+ member: 2,
131
+ viewer: 1,
132
+ };
133
+
134
+ /**
135
+ * Check if role has sufficient permissions
136
+ */
137
+ export function hasRolePermission(
138
+ userRole: CompanyRole,
139
+ requiredRole: CompanyRole
140
+ ): boolean {
141
+ return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole];
142
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Cookie utilities for client-side access
3
+ */
4
+
5
+ /**
6
+ * Get cookie value by name
7
+ */
8
+ export function getCookie(name: string): string | null {
9
+ if (typeof document === 'undefined') {
10
+ return null;
11
+ }
12
+
13
+ const value = `; ${document.cookie}`;
14
+ const parts = value.split(`; ${name}=`);
15
+
16
+ if (parts.length === 2) {
17
+ return parts.pop()?.split(';').shift() || null;
18
+ }
19
+
20
+ return null;
21
+ }
22
+
23
+ /**
24
+ * Set cookie with options
25
+ */
26
+ export function setCookie(
27
+ name: string,
28
+ value: string,
29
+ options: {
30
+ maxAge?: number;
31
+ path?: string;
32
+ domain?: string;
33
+ secure?: boolean;
34
+ sameSite?: 'strict' | 'lax' | 'none';
35
+ } = {}
36
+ ): void {
37
+ if (typeof document === 'undefined') {
38
+ return;
39
+ }
40
+
41
+ const {
42
+ maxAge,
43
+ path = '/',
44
+ domain,
45
+ secure = true,
46
+ sameSite = 'lax',
47
+ } = options;
48
+
49
+ let cookie = `${name}=${value}`;
50
+
51
+ if (maxAge) {
52
+ cookie += `; Max-Age=${maxAge}`;
53
+ }
54
+
55
+ cookie += `; Path=${path}`;
56
+
57
+ if (domain) {
58
+ cookie += `; Domain=${domain}`;
59
+ }
60
+
61
+ if (secure) {
62
+ cookie += '; Secure';
63
+ }
64
+
65
+ cookie += `; SameSite=${sameSite}`;
66
+
67
+ document.cookie = cookie;
68
+ }
69
+
70
+ /**
71
+ * Get the Django CSRF token from cookies
72
+ */
73
+ export function getCsrfToken(): string | null {
74
+ return getCookie('csrftoken');
75
+ }
76
+
77
+ /**
78
+ * Delete cookie by name
79
+ */
80
+ export function deleteCookie(name: string, path: string = '/'): void {
81
+ if (typeof document === 'undefined') {
82
+ return;
83
+ }
84
+
85
+ document.cookie = `${name}=; Path=${path}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
86
+ }
@@ -0,0 +1,3 @@
1
+ export * from './token';
2
+ export * from './cookies';
3
+ export * from '../validation';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * JWT token utilities
3
+ */
4
+
5
+ import type { TokenPayload, DecodedToken } from '../types';
6
+
7
+ /**
8
+ * Decode JWT token payload (does NOT verify signature)
9
+ */
10
+ export function decodeToken(token: string): TokenPayload | null {
11
+ try {
12
+ const parts = token.split('.');
13
+ if (parts.length !== 3) {
14
+ return null;
15
+ }
16
+
17
+ const payload = parts[1];
18
+ const decoded = JSON.parse(atob(payload));
19
+ return decoded as TokenPayload;
20
+ } catch (error) {
21
+ console.error('Failed to decode token:', error);
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Check if token is expired
28
+ */
29
+ export function isTokenExpired(token: string): boolean {
30
+ const payload = decodeToken(token);
31
+ if (!payload) {
32
+ return true;
33
+ }
34
+
35
+ const now = Math.floor(Date.now() / 1000);
36
+ return payload.exp <= now;
37
+ }
38
+
39
+ /**
40
+ * Get token expiration time in milliseconds
41
+ */
42
+ export function getTokenExpiresAt(token: string): number | null {
43
+ const payload = decodeToken(token);
44
+ if (!payload) {
45
+ return null;
46
+ }
47
+
48
+ return payload.exp * 1000;
49
+ }
50
+
51
+ /**
52
+ * Get raw JWT payload as a generic record — framework and backend agnostic.
53
+ * Does NOT verify the signature; use only for reading claims client-side.
54
+ */
55
+ export function getTokenPayload(token: string): DecodedToken | null {
56
+ try {
57
+ const parts = token.split('.');
58
+ if (parts.length !== 3) {
59
+ return null;
60
+ }
61
+
62
+ const payload = parts[1];
63
+ const decoded = JSON.parse(atob(payload));
64
+
65
+ if (typeof decoded !== 'object' || decoded === null) {
66
+ return null;
67
+ }
68
+
69
+ return decoded as DecodedToken;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Check if token needs refresh (expires in less than 5 minutes)
77
+ */
78
+ export function shouldRefreshToken(token: string): boolean {
79
+ const expiresAt = getTokenExpiresAt(token);
80
+ if (!expiresAt) {
81
+ return true;
82
+ }
83
+
84
+ const now = Date.now();
85
+ const timeUntilExpiry = expiresAt - now;
86
+ const FIVE_MINUTES = 5 * 60 * 1000;
87
+
88
+ return timeUntilExpiry < FIVE_MINUTES;
89
+ }