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.
Files changed (127) hide show
  1. package/README.md +10 -0
  2. package/dist/prompts/features.d.ts +1 -1
  3. package/dist/prompts/features.d.ts.map +1 -1
  4. package/dist/prompts/features.js +34 -25
  5. package/dist/prompts/features.js.map +1 -1
  6. package/dist/prompts/index.js +33 -6
  7. package/dist/prompts/index.js.map +1 -1
  8. package/dist/prompts/preset.d.ts.map +1 -1
  9. package/dist/prompts/preset.js +69 -34
  10. package/dist/prompts/preset.js.map +1 -1
  11. package/dist/utils/template.js +1 -1
  12. package/dist/utils/template.js.map +1 -1
  13. package/dist/utils/validation.d.ts.map +1 -1
  14. package/dist/utils/validation.js +43 -1
  15. package/dist/utils/validation.js.map +1 -1
  16. package/package.json +1 -1
  17. package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +33 -1
  18. package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +6 -0
  19. package/templates/base/backend/domain/user/repository.drizzle.ts +28 -1
  20. package/templates/base/backend/domain/user/repository.prisma.ts +25 -0
  21. package/templates/base/backend/drizzle/{schema.drizzle.ts → schema.drizzle.ts.ejs} +25 -0
  22. package/templates/base/backend/lib/auth.drizzle.ts.ejs +27 -18
  23. package/templates/base/backend/lib/auth.prisma.ts.ejs +24 -18
  24. package/templates/base/backend/package.json.ejs +29 -23
  25. package/templates/base/backend/prisma/schema.prisma.ejs +20 -0
  26. package/templates/base/mobile/app/+not-found.tsx +1 -1
  27. package/templates/base/mobile/app/_layout.tsx.ejs +95 -10
  28. package/templates/base/mobile/package.json.ejs +21 -13
  29. package/templates/base/mobile/src/components/ui/Button.tsx +5 -3
  30. package/templates/base/mobile/src/components/ui/Card.tsx +1 -1
  31. package/templates/base/mobile/src/components/ui/Input.tsx +1 -1
  32. package/templates/base/mobile/src/components/ui/Skeleton.tsx +1 -1
  33. package/templates/base/mobile/src/components/ui/Toast.tsx +106 -0
  34. package/templates/base/mobile/src/components/ui/{IconSymbol.tsx → icon-symbol.tsx} +7 -0
  35. package/templates/base/mobile/src/components/ui/{LoadingSpinner.tsx → loading-spinner.tsx} +1 -1
  36. package/templates/base/mobile/src/components/ui/{OnboardingLayout.tsx → onboarding-layout.tsx} +2 -2
  37. package/templates/base/mobile/src/components/ui/{PaywallLayout.tsx → paywall-layout.tsx} +3 -3
  38. package/templates/base/mobile/src/constants/Theme.ts +3 -3
  39. package/templates/base/mobile/src/context/{ThemeContext.tsx → theme-context.tsx} +2 -2
  40. package/templates/base/mobile/src/lib/auth-client.ts.ejs +18 -0
  41. package/templates/base/mobile/src/services/{sdkInitializer.ts.ejs → sdk-initializer.ts.ejs} +4 -4
  42. package/templates/base/web/.prettierignore +6 -0
  43. package/templates/base/web/.prettierrc +8 -0
  44. package/templates/base/web/eslint.config.mjs +31 -7
  45. package/templates/base/web/next.config.ts +50 -1
  46. package/templates/base/web/package.json.ejs +14 -2
  47. package/templates/base/web/src/app/globals.css +1 -1
  48. package/templates/base/web/src/app/layout.tsx.ejs +2 -0
  49. package/templates/base/web/src/components/auth/protected-route.tsx.ejs +32 -5
  50. package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +1 -1
  51. package/templates/base/web/src/hooks/use-device-session.ts.ejs +2 -2
  52. package/templates/base/web/src/lib/auth/actions.ts.ejs +438 -15
  53. package/templates/base/web/src/lib/auth/config.ts.ejs +13 -0
  54. package/templates/base/web/src/lib/auth/cookies.ts.ejs +61 -0
  55. package/templates/base/web/src/lib/device/actions.ts.ejs +2 -10
  56. package/templates/base/web/src/lib/device/types.ts +37 -0
  57. package/templates/base/web/src/proxy.ts.ejs +12 -2
  58. package/templates/base/web/src/store/{deviceSession.store.ts.ejs → device-session-store.ts.ejs} +1 -1
  59. package/templates/features/mobile/auth/app/(auth)/{_layout.tsx → _layout.tsx.ejs} +7 -0
  60. package/templates/features/mobile/auth/app/(auth)/{login.tsx → login.tsx.ejs} +9 -8
  61. package/templates/features/mobile/auth/app/(auth)/{register.tsx → register.tsx.ejs} +9 -8
  62. package/templates/features/mobile/auth/app/(auth)/two-factor-expired.tsx.ejs +88 -0
  63. package/templates/features/mobile/auth/app/(auth)/two-factor.tsx.ejs +259 -0
  64. package/templates/features/mobile/auth/app/(public)/_layout.tsx +9 -0
  65. package/templates/features/mobile/{tabs/app/(tabs)/index.tsx → auth/app/(public)/index.tsx.ejs} +76 -257
  66. package/templates/features/mobile/auth/app/(tabs)/settings/_layout.tsx.ejs +20 -0
  67. package/templates/features/mobile/auth/app/(tabs)/settings/index.tsx.ejs +137 -0
  68. package/templates/features/mobile/auth/app/(tabs)/settings/security/_layout.tsx.ejs +35 -0
  69. package/templates/features/mobile/auth/app/(tabs)/settings/security/index.tsx.ejs +147 -0
  70. package/templates/features/mobile/auth/app/(tabs)/settings/security/set-password.tsx.ejs +140 -0
  71. package/templates/features/mobile/auth/app/(tabs)/settings/security/two-factor-manage.tsx.ejs +366 -0
  72. package/templates/features/mobile/auth/app/(tabs)/settings/security/two-factor-setup.tsx.ejs +317 -0
  73. package/templates/features/mobile/auth/app/account.tsx.ejs +331 -0
  74. package/templates/features/mobile/auth/app/verify-email.tsx.ejs +315 -0
  75. package/templates/features/mobile/auth/components/auth/{LoginForm.tsx.ejs → login-form.tsx.ejs} +15 -3
  76. package/templates/features/mobile/auth/components/auth/protected-screen.tsx.ejs +56 -0
  77. package/templates/features/mobile/auth/components/auth/{RegisterForm.tsx.ejs → register-form.tsx.ejs} +5 -3
  78. package/templates/features/mobile/auth/hooks/{useAuth.ts.ejs → auth.ts.ejs} +255 -1
  79. package/templates/features/mobile/auth/hooks/two-factor-timeout.ts.ejs +56 -0
  80. package/templates/features/mobile/auth/services/{deviceSession.ts → device-session.ts} +2 -10
  81. package/templates/features/mobile/auth/store/{deviceSession.store.ts → device-session-store.ts} +14 -5
  82. package/templates/features/mobile/auth/types/device-session.ts +37 -0
  83. package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +3 -1
  84. package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +3 -1
  85. package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +6 -1
  86. package/templates/features/mobile/paywall/app/paywall.tsx +4 -4
  87. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +0 -6
  88. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx.ejs +60 -0
  89. package/templates/features/mobile/tabs/app/(tabs)/index.tsx.ejs +264 -0
  90. package/templates/features/web/auth/app/(app)/layout.tsx.ejs +8 -22
  91. package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +19 -3
  92. package/templates/features/web/auth/app/(auth)/login/two-factor/page.tsx.ejs +24 -0
  93. package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +117 -152
  94. package/templates/features/web/auth/app/{(app) → (protected)}/dashboard/dashboard-client.tsx.ejs +41 -1
  95. package/templates/features/web/auth/app/{(app) → (protected)}/dashboard/page.tsx.ejs +3 -1
  96. package/templates/features/web/auth/app/(protected)/layout.tsx.ejs +67 -0
  97. package/templates/features/web/auth/app/(protected)/settings/security/page.tsx.ejs +19 -0
  98. package/templates/features/web/auth/app/(protected)/settings/security/security-settings.tsx.ejs +73 -0
  99. package/templates/features/web/auth/app/{(app) → (protected)}/settings/sessions/page.tsx.ejs +2 -6
  100. package/templates/features/web/auth/app/auth/callback/route.ts.ejs +5 -1
  101. package/templates/features/web/auth/app/auth/session-expired/route.ts.ejs +39 -0
  102. package/templates/features/web/auth/app/auth/two-factor-expired/route.ts.ejs +22 -0
  103. package/templates/features/web/auth/components/auth/login-form.tsx.ejs +18 -0
  104. package/templates/features/web/auth/components/auth/two-factor-verify.tsx.ejs +173 -0
  105. package/templates/features/web/auth/components/settings/set-password-form.tsx.ejs +123 -0
  106. package/templates/features/web/auth/components/settings/two-factor-manage.tsx.ejs +253 -0
  107. package/templates/features/web/auth/components/settings/two-factor-setup.tsx.ejs +249 -0
  108. package/templates/features/web/auth/components/ui/checkbox.tsx +32 -0
  109. package/templates/features/web/auth/components/ui/dialog.tsx +143 -0
  110. package/templates/features/web/auth/components/ui/input-otp.tsx +71 -0
  111. package/templates/integrations/mobile/adjust/store/{adjust.store.ts → adjust-store.ts} +3 -3
  112. package/templates/integrations/mobile/att/store/{att.store.ts → att-store.ts} +2 -2
  113. package/templates/integrations/mobile/revenuecat/store/{revenuecat.store.ts → revenuecat-store.ts} +1 -1
  114. package/templates/integrations/mobile/scate/store/{scate.store.ts → scate-store.ts} +1 -1
  115. package/templates/base/mobile/src/components/ui/index.ts +0 -6
  116. package/templates/base/mobile/src/store/index.ts.ejs +0 -18
  117. package/templates/base/web/src/lib/auth/index.ts.ejs +0 -40
  118. package/templates/features/mobile/auth/components/auth/index.ts +0 -2
  119. package/templates/features/mobile/auth/hooks/index.ts.ejs +0 -1
  120. /package/templates/base/mobile/src/services/{errorService.ts → error-service.ts} +0 -0
  121. /package/templates/base/mobile/src/store/{ui.store.ts → ui-store.ts} +0 -0
  122. /package/templates/features/web/auth/app/{(app) → (protected)}/settings/sessions/sessions-client.tsx.ejs +0 -0
  123. /package/templates/integrations/mobile/adjust/services/{adjustService.ts.ejs → adjust-service.ts.ejs} +0 -0
  124. /package/templates/integrations/mobile/att/services/{attService.ts → att-service.ts} +0 -0
  125. /package/templates/integrations/mobile/att/services/{trackingPermissions.ts → tracking-permissions.ts} +0 -0
  126. /package/templates/integrations/mobile/revenuecat/services/{revenuecatService.ts.ejs → revenuecat-service.ts.ejs} +0 -0
  127. /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;
@@ -7,7 +7,7 @@ import {
7
7
  ViewStyle,
8
8
  TextStyle,
9
9
  } from 'react-native';
10
- import { useAppTheme } from '@/context/ThemeContext';
10
+ import { useAppTheme } from '@/context/theme-context';
11
11
 
12
12
  interface LoadingSpinnerProps {
13
13
  size?: 'small' | 'large';
@@ -9,8 +9,8 @@ import {
9
9
  KeyboardAvoidingView,
10
10
  Platform,
11
11
  } from 'react-native';
12
- import { IconSymbol } from './IconSymbol';
13
- import { useAppTheme, AppTheme } from '@/context/ThemeContext';
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 './IconSymbol';
13
- import { useAppTheme, AppTheme } from '@/context/ThemeContext';
14
- import { createTheme } from '@/constants/Theme';
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[50],
55
- backgroundTertiary: palette.neutral[100],
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: palette.neutral[900],
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/Theme';
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/Theme';
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.store';
3
- <% } %><% if (integrations.revenueCat.enabled) { %>import { revenueCatService } from './revenuecatService';
4
- <% } %><% if (integrations.adjust.enabled) { %>import { adjustService } from './adjustService';
5
- <% } %><% if (integrations.scate.enabled) { %>import { scateService } from './scateService';
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 {
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ .next
3
+ out
4
+ dist
5
+ coverage
6
+ *.min.js
@@ -0,0 +1,8 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "trailingComma": "es5",
6
+ "printWidth": 100,
7
+ "plugins": ["prettier-plugin-tailwindcss"]
8
+ }
@@ -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
- /* config options here */
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 || features.sessionManagement) { %>,
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"
@@ -116,7 +116,7 @@
116
116
  --accent: #27272a;
117
117
  --accent-foreground: #fafafa;
118
118
 
119
- --destructive: #7f1d1d;
119
+ --destructive: #dc2626;
120
120
  --destructive-foreground: #fafafa;
121
121
 
122
122
  --border: #27272a;
@@ -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 { useAuthActions } from "@/store/auth.store";
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 - update UI state
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
 
@@ -4,7 +4,7 @@ import { useEffect } from "react";
4
4
  import {
5
5
  useDeviceSessionStore,
6
6
  useDeviceSessionActions,
7
- } from "@/store/deviceSession.store";
7
+ } from "@/store/device-session-store";
8
8
  import { AUTH_CONFIG } from "@/lib/auth/config";
9
9
 
10
10
  /**
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  useDeviceSession as useDeviceSessionStore,
5
5
  useDeviceSessionActions,
6
- } from "@/store/deviceSession.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/deviceSession.store";
40
+ export { useDeviceSessionStore, useDeviceSessionActions } from "@/store/device-session-store";