create-stackr 0.2.0 → 0.3.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/README.md +10 -0
- package/dist/prompts/features.d.ts +1 -1
- package/dist/prompts/features.d.ts.map +1 -1
- package/dist/prompts/features.js +34 -25
- package/dist/prompts/features.js.map +1 -1
- package/dist/prompts/index.js +33 -6
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/preset.d.ts.map +1 -1
- package/dist/prompts/preset.js +69 -34
- package/dist/prompts/preset.js.map +1 -1
- package/dist/utils/template.js +1 -1
- package/dist/utils/template.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +43 -1
- package/dist/utils/validation.js.map +1 -1
- package/package.json +1 -1
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +33 -1
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +6 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +28 -1
- package/templates/base/backend/domain/user/repository.prisma.ts +25 -0
- package/templates/base/backend/drizzle/{schema.drizzle.ts → schema.drizzle.ts.ejs} +25 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +27 -18
- package/templates/base/backend/lib/auth.prisma.ts.ejs +24 -18
- package/templates/base/backend/package.json.ejs +29 -23
- package/templates/base/backend/prisma/schema.prisma.ejs +20 -0
- package/templates/base/mobile/app/+not-found.tsx +1 -1
- package/templates/base/mobile/app/_layout.tsx.ejs +95 -10
- package/templates/base/mobile/package.json.ejs +21 -13
- package/templates/base/mobile/src/components/ui/Button.tsx +5 -3
- package/templates/base/mobile/src/components/ui/Card.tsx +1 -1
- package/templates/base/mobile/src/components/ui/Input.tsx +1 -1
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +1 -1
- package/templates/base/mobile/src/components/ui/Toast.tsx +106 -0
- package/templates/base/mobile/src/components/ui/{IconSymbol.tsx → icon-symbol.tsx} +7 -0
- package/templates/base/mobile/src/components/ui/{LoadingSpinner.tsx → loading-spinner.tsx} +1 -1
- package/templates/base/mobile/src/components/ui/{OnboardingLayout.tsx → onboarding-layout.tsx} +2 -2
- package/templates/base/mobile/src/components/ui/{PaywallLayout.tsx → paywall-layout.tsx} +3 -3
- package/templates/base/mobile/src/constants/Theme.ts +3 -3
- package/templates/base/mobile/src/context/{ThemeContext.tsx → theme-context.tsx} +2 -2
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +18 -0
- package/templates/base/mobile/src/services/{sdkInitializer.ts.ejs → sdk-initializer.ts.ejs} +4 -4
- package/templates/base/web/.prettierignore +6 -0
- package/templates/base/web/.prettierrc +8 -0
- package/templates/base/web/eslint.config.mjs +31 -7
- package/templates/base/web/next.config.ts +50 -1
- package/templates/base/web/package.json.ejs +14 -2
- package/templates/base/web/src/app/globals.css +1 -1
- package/templates/base/web/src/app/layout.tsx.ejs +2 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +32 -5
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +1 -1
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +2 -2
- package/templates/base/web/src/lib/auth/actions.ts.ejs +438 -15
- package/templates/base/web/src/lib/auth/config.ts.ejs +13 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +61 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +2 -10
- package/templates/base/web/src/lib/device/types.ts +37 -0
- package/templates/base/web/src/proxy.ts.ejs +12 -2
- package/templates/base/web/src/store/{deviceSession.store.ts.ejs → device-session-store.ts.ejs} +1 -1
- package/templates/features/mobile/auth/app/(auth)/{_layout.tsx → _layout.tsx.ejs} +7 -0
- package/templates/features/mobile/auth/app/(auth)/{login.tsx → login.tsx.ejs} +9 -8
- package/templates/features/mobile/auth/app/(auth)/{register.tsx → register.tsx.ejs} +9 -8
- package/templates/features/mobile/auth/app/(auth)/two-factor-expired.tsx.ejs +88 -0
- package/templates/features/mobile/auth/app/(auth)/two-factor.tsx.ejs +259 -0
- package/templates/features/mobile/auth/app/(public)/_layout.tsx +9 -0
- package/templates/features/mobile/{tabs/app/(tabs)/index.tsx → auth/app/(public)/index.tsx.ejs} +76 -257
- package/templates/features/mobile/auth/app/(tabs)/settings/_layout.tsx.ejs +20 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/index.tsx.ejs +137 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/_layout.tsx.ejs +35 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/index.tsx.ejs +147 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/set-password.tsx.ejs +140 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/two-factor-manage.tsx.ejs +366 -0
- package/templates/features/mobile/auth/app/(tabs)/settings/security/two-factor-setup.tsx.ejs +317 -0
- package/templates/features/mobile/auth/app/account.tsx.ejs +331 -0
- package/templates/features/mobile/auth/app/verify-email.tsx.ejs +315 -0
- package/templates/features/mobile/auth/components/auth/{LoginForm.tsx.ejs → login-form.tsx.ejs} +15 -3
- package/templates/features/mobile/auth/components/auth/protected-screen.tsx.ejs +56 -0
- package/templates/features/mobile/auth/components/auth/{RegisterForm.tsx.ejs → register-form.tsx.ejs} +5 -3
- package/templates/features/mobile/auth/hooks/{useAuth.ts.ejs → auth.ts.ejs} +255 -1
- package/templates/features/mobile/auth/hooks/two-factor-timeout.ts.ejs +56 -0
- package/templates/features/mobile/auth/services/{deviceSession.ts → device-session.ts} +2 -10
- package/templates/features/mobile/auth/store/{deviceSession.store.ts → device-session-store.ts} +14 -5
- package/templates/features/mobile/auth/types/device-session.ts +37 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +3 -1
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +3 -1
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +6 -1
- package/templates/features/mobile/paywall/app/paywall.tsx +4 -4
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +0 -6
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx.ejs +60 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx.ejs +264 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +8 -22
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +19 -3
- package/templates/features/web/auth/app/(auth)/login/two-factor/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +117 -152
- package/templates/features/web/auth/app/{(app) → (protected)}/dashboard/dashboard-client.tsx.ejs +41 -1
- package/templates/features/web/auth/app/{(app) → (protected)}/dashboard/page.tsx.ejs +3 -1
- package/templates/features/web/auth/app/(protected)/layout.tsx.ejs +67 -0
- package/templates/features/web/auth/app/(protected)/settings/security/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(protected)/settings/security/security-settings.tsx.ejs +73 -0
- package/templates/features/web/auth/app/{(app) → (protected)}/settings/sessions/page.tsx.ejs +2 -6
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +5 -1
- package/templates/features/web/auth/app/auth/session-expired/route.ts.ejs +39 -0
- package/templates/features/web/auth/app/auth/two-factor-expired/route.ts.ejs +22 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +18 -0
- package/templates/features/web/auth/components/auth/two-factor-verify.tsx.ejs +173 -0
- package/templates/features/web/auth/components/settings/set-password-form.tsx.ejs +123 -0
- package/templates/features/web/auth/components/settings/two-factor-manage.tsx.ejs +253 -0
- package/templates/features/web/auth/components/settings/two-factor-setup.tsx.ejs +249 -0
- package/templates/features/web/auth/components/ui/checkbox.tsx +32 -0
- package/templates/features/web/auth/components/ui/dialog.tsx +143 -0
- package/templates/features/web/auth/components/ui/input-otp.tsx +71 -0
- package/templates/integrations/mobile/adjust/store/{adjust.store.ts → adjust-store.ts} +3 -3
- package/templates/integrations/mobile/att/store/{att.store.ts → att-store.ts} +2 -2
- package/templates/integrations/mobile/revenuecat/store/{revenuecat.store.ts → revenuecat-store.ts} +1 -1
- package/templates/integrations/mobile/scate/store/{scate.store.ts → scate-store.ts} +1 -1
- package/templates/base/mobile/src/components/ui/index.ts +0 -6
- package/templates/base/mobile/src/store/index.ts.ejs +0 -18
- package/templates/base/web/src/lib/auth/index.ts.ejs +0 -40
- package/templates/features/mobile/auth/components/auth/index.ts +0 -2
- package/templates/features/mobile/auth/hooks/index.ts.ejs +0 -1
- /package/templates/base/mobile/src/services/{errorService.ts → error-service.ts} +0 -0
- /package/templates/base/mobile/src/store/{ui.store.ts → ui-store.ts} +0 -0
- /package/templates/features/web/auth/app/{(app) → (protected)}/settings/sessions/sessions-client.tsx.ejs +0 -0
- /package/templates/integrations/mobile/adjust/services/{adjustService.ts.ejs → adjust-service.ts.ejs} +0 -0
- /package/templates/integrations/mobile/att/services/{attService.ts → att-service.ts} +0 -0
- /package/templates/integrations/mobile/att/services/{trackingPermissions.ts → tracking-permissions.ts} +0 -0
- /package/templates/integrations/mobile/revenuecat/services/{revenuecatService.ts.ejs → revenuecat-service.ts.ejs} +0 -0
- /package/templates/integrations/mobile/scate/services/{scateService.ts.ejs → scate-service.ts.ejs} +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { Animated, StyleSheet, Text, Pressable, Platform } from "react-native";
|
|
3
|
+
import { useUI, useUIActions } from "@/store/ui-store";
|
|
4
|
+
|
|
5
|
+
const COLORS = {
|
|
6
|
+
success: { bg: "#dcfce7", border: "#16a34a", text: "#166534" },
|
|
7
|
+
error: { bg: "#fee2e2", border: "#dc2626", text: "#991b1b" },
|
|
8
|
+
warning: { bg: "#fef3c7", border: "#d97706", text: "#92400e" },
|
|
9
|
+
info: { bg: "#dbeafe", border: "#2563eb", text: "#1e40af" },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function Toast() {
|
|
13
|
+
const { notification } = useUI();
|
|
14
|
+
const { hideNotification } = useUIActions();
|
|
15
|
+
const opacity = useRef(new Animated.Value(0)).current;
|
|
16
|
+
const translateY = useRef(new Animated.Value(-100)).current;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (notification) {
|
|
20
|
+
// Animate in
|
|
21
|
+
Animated.parallel([
|
|
22
|
+
Animated.timing(opacity, {
|
|
23
|
+
toValue: 1,
|
|
24
|
+
duration: 200,
|
|
25
|
+
useNativeDriver: true,
|
|
26
|
+
}),
|
|
27
|
+
Animated.timing(translateY, {
|
|
28
|
+
toValue: 0,
|
|
29
|
+
duration: 200,
|
|
30
|
+
useNativeDriver: true,
|
|
31
|
+
}),
|
|
32
|
+
]).start();
|
|
33
|
+
} else {
|
|
34
|
+
// Animate out (store handles auto-hide timing)
|
|
35
|
+
Animated.parallel([
|
|
36
|
+
Animated.timing(opacity, {
|
|
37
|
+
toValue: 0,
|
|
38
|
+
duration: 200,
|
|
39
|
+
useNativeDriver: true,
|
|
40
|
+
}),
|
|
41
|
+
Animated.timing(translateY, {
|
|
42
|
+
toValue: -100,
|
|
43
|
+
duration: 200,
|
|
44
|
+
useNativeDriver: true,
|
|
45
|
+
}),
|
|
46
|
+
]).start();
|
|
47
|
+
}
|
|
48
|
+
}, [notification, opacity, translateY]);
|
|
49
|
+
|
|
50
|
+
if (!notification) return null;
|
|
51
|
+
|
|
52
|
+
const colors = COLORS[notification.type];
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Animated.View
|
|
56
|
+
style={[
|
|
57
|
+
styles.container,
|
|
58
|
+
{
|
|
59
|
+
opacity,
|
|
60
|
+
transform: [{ translateY }],
|
|
61
|
+
backgroundColor: colors.bg,
|
|
62
|
+
borderColor: colors.border,
|
|
63
|
+
},
|
|
64
|
+
]}
|
|
65
|
+
>
|
|
66
|
+
<Pressable onPress={hideNotification} style={styles.content}>
|
|
67
|
+
<Text style={[styles.title, { color: colors.text }]}>
|
|
68
|
+
{notification.title}
|
|
69
|
+
</Text>
|
|
70
|
+
{notification.message && (
|
|
71
|
+
<Text style={[styles.message, { color: colors.text }]}>
|
|
72
|
+
{notification.message}
|
|
73
|
+
</Text>
|
|
74
|
+
)}
|
|
75
|
+
</Pressable>
|
|
76
|
+
</Animated.View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const styles = StyleSheet.create({
|
|
81
|
+
container: {
|
|
82
|
+
position: "absolute",
|
|
83
|
+
top: Platform.OS === "ios" ? 60 : 40,
|
|
84
|
+
left: 16,
|
|
85
|
+
right: 16,
|
|
86
|
+
borderRadius: 8,
|
|
87
|
+
borderWidth: 1,
|
|
88
|
+
zIndex: 9999,
|
|
89
|
+
elevation: 10, // Android shadow
|
|
90
|
+
shadowColor: "#000", // iOS shadow
|
|
91
|
+
shadowOffset: { width: 0, height: 2 },
|
|
92
|
+
shadowOpacity: 0.25,
|
|
93
|
+
shadowRadius: 4,
|
|
94
|
+
},
|
|
95
|
+
content: {
|
|
96
|
+
padding: 16,
|
|
97
|
+
},
|
|
98
|
+
title: {
|
|
99
|
+
fontSize: 16,
|
|
100
|
+
fontWeight: "600",
|
|
101
|
+
},
|
|
102
|
+
message: {
|
|
103
|
+
fontSize: 14,
|
|
104
|
+
marginTop: 4,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
@@ -22,6 +22,8 @@ const MAPPING = {
|
|
|
22
22
|
"arrow.left": "arrow-back",
|
|
23
23
|
magnifyingglass: "search",
|
|
24
24
|
"person.fill": "person",
|
|
25
|
+
"person.circle.fill": "account-circle",
|
|
26
|
+
"rectangle.portrait.and.arrow.right": "logout",
|
|
25
27
|
"doc.text": "description",
|
|
26
28
|
calendar: "event",
|
|
27
29
|
checkmark: "check",
|
|
@@ -58,6 +60,11 @@ const MAPPING = {
|
|
|
58
60
|
"videocam.fill": "videocam",
|
|
59
61
|
sparkles: "auto-awesome",
|
|
60
62
|
infinity: "all-inclusive",
|
|
63
|
+
// 2FA-specific icons
|
|
64
|
+
"key.fill": "vpn-key",
|
|
65
|
+
"checkmark.shield.fill": "verified-user",
|
|
66
|
+
"shield.slash.fill": "remove-moderator",
|
|
67
|
+
"gearshape.fill": "settings",
|
|
61
68
|
} as const;
|
|
62
69
|
|
|
63
70
|
export type IconSymbolName = keyof typeof MAPPING;
|
package/templates/base/mobile/src/components/ui/{OnboardingLayout.tsx → onboarding-layout.tsx}
RENAMED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
KeyboardAvoidingView,
|
|
10
10
|
Platform,
|
|
11
11
|
} from 'react-native';
|
|
12
|
-
import { IconSymbol } from './
|
|
13
|
-
import { useAppTheme, AppTheme } from '@/context/
|
|
12
|
+
import { IconSymbol } from './icon-symbol';
|
|
13
|
+
import { useAppTheme, AppTheme } from '@/context/theme-context';
|
|
14
14
|
import { responsive, fontSize, getSpacing } from '@/utils/responsive';
|
|
15
15
|
|
|
16
16
|
interface OnboardingLayoutProps {
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
KeyboardAvoidingView,
|
|
10
10
|
Platform,
|
|
11
11
|
} from 'react-native';
|
|
12
|
-
import { IconSymbol } from './
|
|
13
|
-
import { useAppTheme, AppTheme } from '@/context/
|
|
14
|
-
import { createTheme } from '@/constants/
|
|
12
|
+
import { IconSymbol } from './icon-symbol';
|
|
13
|
+
import { useAppTheme, AppTheme } from '@/context/theme-context';
|
|
14
|
+
import { createTheme } from '@/constants/theme';
|
|
15
15
|
import { responsive, fontSize, getSpacing } from '@/utils/responsive';
|
|
16
16
|
|
|
17
17
|
type ThemeType = 'light' | 'dark';
|
|
@@ -51,8 +51,8 @@ const fontFamily = Platform.OS === 'ios'
|
|
|
51
51
|
// Light theme colors (monochrome primary matching web)
|
|
52
52
|
const lightColors = {
|
|
53
53
|
background: palette.neutral[0],
|
|
54
|
-
backgroundSecondary: palette.neutral[
|
|
55
|
-
backgroundTertiary: palette.neutral[
|
|
54
|
+
backgroundSecondary: palette.neutral[100], // More distinct from white background
|
|
55
|
+
backgroundTertiary: palette.neutral[200],
|
|
56
56
|
card: palette.neutral[0],
|
|
57
57
|
|
|
58
58
|
text: palette.neutral[950],
|
|
@@ -82,7 +82,7 @@ const lightColors = {
|
|
|
82
82
|
// Dark theme colors (monochrome primary matching web)
|
|
83
83
|
const darkColors = {
|
|
84
84
|
background: palette.neutral[950],
|
|
85
|
-
backgroundSecondary:
|
|
85
|
+
backgroundSecondary: 'rgba(255, 255, 255, 0.04)', // Subtle overlay, less prominent
|
|
86
86
|
backgroundTertiary: palette.neutral[800],
|
|
87
87
|
card: palette.neutral[900],
|
|
88
88
|
|
|
@@ -2,7 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCall
|
|
|
2
2
|
import { useColorScheme, Appearance } from 'react-native';
|
|
3
3
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
4
4
|
import { StatusBar } from 'expo-status-bar';
|
|
5
|
-
import { createTheme, ThemeMode, AppTheme } from '@/constants/
|
|
5
|
+
import { createTheme, ThemeMode, AppTheme } from '@/constants/theme';
|
|
6
6
|
|
|
7
7
|
const THEME_STORAGE_KEY = '@app_theme_mode';
|
|
8
8
|
|
|
@@ -154,4 +154,4 @@ export function useAppTheme(): AppTheme {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// Re-export AppTheme for convenience (so components can import from one place)
|
|
157
|
-
export type { AppTheme } from '@/constants/
|
|
157
|
+
export type { AppTheme } from '@/constants/theme';
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { createAuthClient } from "better-auth/react";
|
|
2
2
|
import { expoClient } from "@better-auth/expo/client";
|
|
3
|
+
<% if (features.authentication.emailVerification) { %>
|
|
4
|
+
import { emailOTPClient } from "better-auth/client/plugins";
|
|
5
|
+
<% } %>
|
|
6
|
+
<% if (features.authentication.twoFactor) { %>
|
|
7
|
+
import { twoFactorClient } from "better-auth/client/plugins";
|
|
8
|
+
<% } %>
|
|
3
9
|
import * as SecureStore from "expo-secure-store";
|
|
4
10
|
import Constants from "expo-constants";
|
|
5
11
|
|
|
@@ -25,6 +31,12 @@ export const authClient = createAuthClient({
|
|
|
25
31
|
storagePrefix: APP_SCHEME, // Prefix for secure storage keys
|
|
26
32
|
storage: SecureStore, // Use secure storage (not AsyncStorage!)
|
|
27
33
|
}),
|
|
34
|
+
<% if (features.authentication.emailVerification) { %>
|
|
35
|
+
emailOTPClient(),
|
|
36
|
+
<% } %>
|
|
37
|
+
<% if (features.authentication.twoFactor) { %>
|
|
38
|
+
twoFactorClient(),
|
|
39
|
+
<% } %>
|
|
28
40
|
],
|
|
29
41
|
});
|
|
30
42
|
|
|
@@ -35,6 +47,12 @@ export const {
|
|
|
35
47
|
signOut,
|
|
36
48
|
useSession,
|
|
37
49
|
getSession,
|
|
50
|
+
<% if (features.authentication.emailVerification) { %>
|
|
51
|
+
emailOtp,
|
|
52
|
+
<% } %>
|
|
53
|
+
<% if (features.authentication.twoFactor) { %>
|
|
54
|
+
twoFactor,
|
|
55
|
+
<% } %>
|
|
38
56
|
} = authClient;
|
|
39
57
|
|
|
40
58
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<% if (integrations.att.enabled) { %>import { Platform } from 'react-native';
|
|
2
|
-
import { useATTStore } from '@/store/att
|
|
3
|
-
<% } %><% if (integrations.revenueCat.enabled) { %>import { revenueCatService } from './
|
|
4
|
-
<% } %><% if (integrations.adjust.enabled) { %>import { adjustService } from './
|
|
5
|
-
<% } %><% if (integrations.scate.enabled) { %>import { scateService } from './
|
|
2
|
+
import { useATTStore } from '@/store/att-store';
|
|
3
|
+
<% } %><% if (integrations.revenueCat.enabled) { %>import { revenueCatService } from './revenuecat-service';
|
|
4
|
+
<% } %><% if (integrations.adjust.enabled) { %>import { adjustService } from './adjust-service';
|
|
5
|
+
<% } %><% if (integrations.scate.enabled) { %>import { scateService } from './scate-service';
|
|
6
6
|
<% } %>
|
|
7
7
|
export const initializeSDKs = async () => {
|
|
8
8
|
try {
|
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
2
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
3
|
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
|
4
5
|
|
|
5
6
|
const eslintConfig = defineConfig([
|
|
6
7
|
...nextVitals,
|
|
7
8
|
...nextTs,
|
|
9
|
+
{
|
|
10
|
+
plugins: {
|
|
11
|
+
"simple-import-sort": simpleImportSort,
|
|
12
|
+
},
|
|
13
|
+
rules: {
|
|
14
|
+
"simple-import-sort/imports": [
|
|
15
|
+
"error",
|
|
16
|
+
{
|
|
17
|
+
groups: [
|
|
18
|
+
// Node.js builtins
|
|
19
|
+
["^node:"],
|
|
20
|
+
// React and Next.js
|
|
21
|
+
["^react", "^next"],
|
|
22
|
+
// External packages
|
|
23
|
+
["^@?\\w"],
|
|
24
|
+
// Internal paths (aliases)
|
|
25
|
+
["^@/"],
|
|
26
|
+
// Parent imports
|
|
27
|
+
["^\\.\\."],
|
|
28
|
+
// Sibling imports
|
|
29
|
+
["^\\."],
|
|
30
|
+
// Style imports
|
|
31
|
+
["^.+\\.css$"],
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
"simple-import-sort/exports": "error",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
8
38
|
// Override default ignores of eslint-config-next.
|
|
9
|
-
globalIgnores([
|
|
10
|
-
// Default ignores of eslint-config-next:
|
|
11
|
-
".next/**",
|
|
12
|
-
"out/**",
|
|
13
|
-
"build/**",
|
|
14
|
-
"next-env.d.ts",
|
|
15
|
-
]),
|
|
39
|
+
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
|
|
16
40
|
]);
|
|
17
41
|
|
|
18
42
|
export default eslintConfig;
|
|
@@ -1,7 +1,56 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
2
|
|
|
3
3
|
const nextConfig: NextConfig = {
|
|
4
|
-
|
|
4
|
+
// Hide X-Powered-By header for security
|
|
5
|
+
poweredByHeader: false,
|
|
6
|
+
|
|
7
|
+
// Enable React strict mode for development
|
|
8
|
+
reactStrictMode: true,
|
|
9
|
+
|
|
10
|
+
// Configure allowed image sources
|
|
11
|
+
// NOTE: hostname "**" allows all domains - intentionally permissive for scaffolding.
|
|
12
|
+
// Users should restrict this in production to their specific CDN/image domains.
|
|
13
|
+
images: {
|
|
14
|
+
remotePatterns: [
|
|
15
|
+
{
|
|
16
|
+
protocol: "https",
|
|
17
|
+
hostname: "**",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// Security headers
|
|
23
|
+
async headers() {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
// Apply to all routes (/:path* handles i18n correctly unlike /(.*))
|
|
27
|
+
source: "/:path*",
|
|
28
|
+
headers: [
|
|
29
|
+
{
|
|
30
|
+
key: "X-Content-Type-Options",
|
|
31
|
+
value: "nosniff",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "X-Frame-Options",
|
|
35
|
+
value: "DENY",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "Referrer-Policy",
|
|
39
|
+
value: "strict-origin-when-cross-origin",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "Permissions-Policy",
|
|
43
|
+
value: "camera=(), microphone=(), geolocation=()",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
// HSTS - enforce HTTPS connections
|
|
47
|
+
key: "Strict-Transport-Security",
|
|
48
|
+
value: "max-age=31536000; includeSubDomains",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
},
|
|
5
54
|
};
|
|
6
55
|
|
|
7
56
|
export default nextConfig;
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
"dev": "next dev",
|
|
7
7
|
"build": "next build",
|
|
8
8
|
"start": "next start",
|
|
9
|
-
"lint": "eslint"
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"lint:fix": "eslint . --fix",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"format": "prettier --write .",
|
|
13
|
+
"format:check": "prettier --check ."
|
|
10
14
|
},
|
|
11
15
|
"dependencies": {
|
|
12
16
|
"@radix-ui/react-slot": "^1.2.4",
|
|
@@ -18,7 +22,12 @@
|
|
|
18
22
|
"@radix-ui/react-label": "^2.1.8",
|
|
19
23
|
"react": "19.2.3",
|
|
20
24
|
"react-dom": "19.2.3",
|
|
21
|
-
"tailwind-merge": "^3.4.0"<% if (features.authentication.enabled
|
|
25
|
+
"tailwind-merge": "^3.4.0"<% if (features.authentication.enabled) { %>,
|
|
26
|
+
"sonner": "^1.7.1"<% } %><% if (features.authentication.emailVerification) { %>,
|
|
27
|
+
"input-otp": "^1.4.2"<% } %><% if (features.authentication.twoFactor) { %>,
|
|
28
|
+
"react-qr-code": "^2.0.15",
|
|
29
|
+
"@radix-ui/react-checkbox": "^1.3.3",
|
|
30
|
+
"@radix-ui/react-dialog": "^1.1.15"<% } %><% if (features.authentication.enabled || features.sessionManagement) { %>,
|
|
22
31
|
"zustand": "^5.0.2"<% } %>
|
|
23
32
|
},
|
|
24
33
|
"devDependencies": {
|
|
@@ -28,6 +37,9 @@
|
|
|
28
37
|
"@types/react-dom": "^19",
|
|
29
38
|
"eslint": "^9",
|
|
30
39
|
"eslint-config-next": "16.1.0",
|
|
40
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
41
|
+
"prettier": "^3.4.2",
|
|
42
|
+
"prettier-plugin-tailwindcss": "^0.6.9",
|
|
31
43
|
"tailwindcss": "^4",
|
|
32
44
|
"tw-animate-css": "^1.4.0",
|
|
33
45
|
"typescript": "^5"
|
|
@@ -5,6 +5,7 @@ import { ThemeProvider } from "@/components/providers/theme-provider";
|
|
|
5
5
|
<% if (features.authentication.enabled) { %>
|
|
6
6
|
import { AuthHydrator } from "@/components/auth/auth-hydrator";
|
|
7
7
|
import { getSession } from "@/lib/auth/actions";
|
|
8
|
+
import { Toaster } from "sonner";
|
|
8
9
|
<% } %>
|
|
9
10
|
<% if (features.sessionManagement) { %>
|
|
10
11
|
import { DeviceSessionSetup } from "@/components/providers/device-session-setup";
|
|
@@ -42,6 +43,7 @@ export default async function RootLayout({
|
|
|
42
43
|
<ThemeProvider>
|
|
43
44
|
<% if (features.authentication.enabled) { %>
|
|
44
45
|
<AuthHydrator session={session} />
|
|
46
|
+
<Toaster position="top-center" richColors />
|
|
45
47
|
<% } %>
|
|
46
48
|
<% if (features.sessionManagement) { %>
|
|
47
49
|
<DeviceSessionSetup />
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { useEffect, useCallback } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
5
|
import { useSession } from "@/hooks/use-session";
|
|
6
|
-
import { getSession } from "@/lib/auth/actions";
|
|
7
|
-
import {
|
|
6
|
+
import { getSession, signOut } from "@/lib/auth/actions";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
import { useAuthActions, useAuthStore } from "@/store/auth.store";
|
|
8
9
|
|
|
9
10
|
interface ProtectedRouteProps {
|
|
10
11
|
children: React.ReactNode;
|
|
@@ -41,8 +42,9 @@ export function ProtectedRoute({
|
|
|
41
42
|
redirectTo = "/login",
|
|
42
43
|
loadingComponent,
|
|
43
44
|
}: ProtectedRouteProps) {
|
|
44
|
-
const { isAuthenticated, isLoading } = useSession();
|
|
45
|
+
const { isAuthenticated, isLoading, user } = useSession();
|
|
45
46
|
const { handleAuthError } = useAuthActions();
|
|
47
|
+
const authError = useAuthStore((state) => state.error);
|
|
46
48
|
const router = useRouter();
|
|
47
49
|
|
|
48
50
|
// Revalidate session when window regains focus
|
|
@@ -52,7 +54,9 @@ export function ProtectedRoute({
|
|
|
52
54
|
|
|
53
55
|
const session = await getSession();
|
|
54
56
|
if (!session) {
|
|
55
|
-
// Session expired on backend -
|
|
57
|
+
// Session expired on backend - call signOut to properly clear cookie via Server Action
|
|
58
|
+
// This ensures the browser gets the Set-Cookie header to clear the cookie
|
|
59
|
+
await signOut();
|
|
56
60
|
handleAuthError();
|
|
57
61
|
}
|
|
58
62
|
}, [isAuthenticated, handleAuthError]);
|
|
@@ -66,12 +70,28 @@ export function ProtectedRoute({
|
|
|
66
70
|
// Redirect when not authenticated
|
|
67
71
|
useEffect(() => {
|
|
68
72
|
if (!isLoading && !isAuthenticated) {
|
|
73
|
+
// Show toast if this was a session expiration
|
|
74
|
+
if (authError === "Session expired") {
|
|
75
|
+
toast.error("Session Expired", {
|
|
76
|
+
description: "Your session has expired. Please sign in again.",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
// Include current path as redirect parameter
|
|
70
81
|
const currentPath = window.location.pathname;
|
|
71
82
|
const loginUrl = `${redirectTo}?redirect=${encodeURIComponent(currentPath)}`;
|
|
72
83
|
router.push(loginUrl);
|
|
73
84
|
}
|
|
74
|
-
}, [isAuthenticated, isLoading, redirectTo, router]);
|
|
85
|
+
}, [isAuthenticated, isLoading, redirectTo, router, authError]);
|
|
86
|
+
|
|
87
|
+
<% if (features.authentication.emailVerification) { %>
|
|
88
|
+
// Redirect when email is not verified
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!isLoading && isAuthenticated && user && user.emailVerified === false) {
|
|
91
|
+
router.push(`/verify-email?email=${encodeURIComponent(user.email)}`);
|
|
92
|
+
}
|
|
93
|
+
}, [isAuthenticated, isLoading, user, router]);
|
|
94
|
+
<% } %>
|
|
75
95
|
|
|
76
96
|
// Show loading state
|
|
77
97
|
if (isLoading) {
|
|
@@ -89,6 +109,13 @@ export function ProtectedRoute({
|
|
|
89
109
|
return null;
|
|
90
110
|
}
|
|
91
111
|
|
|
112
|
+
<% if (features.authentication.emailVerification) { %>
|
|
113
|
+
// Don't render content if email is not verified
|
|
114
|
+
if (user && user.emailVerified === false) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
<% } %>
|
|
118
|
+
|
|
92
119
|
return <>{children}</>;
|
|
93
120
|
}
|
|
94
121
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
useDeviceSession as useDeviceSessionStore,
|
|
5
5
|
useDeviceSessionActions,
|
|
6
|
-
} from "@/store/
|
|
6
|
+
} from "@/store/device-session-store";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Hook to access the current device session
|
|
@@ -37,4 +37,4 @@ export function useDeviceSession() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Re-export store hooks for direct access when needed
|
|
40
|
-
export { useDeviceSessionStore, useDeviceSessionActions } from "@/store/
|
|
40
|
+
export { useDeviceSessionStore, useDeviceSessionActions } from "@/store/device-session-store";
|