create-stackr 0.2.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/LICENSE +21 -0
- package/README.md +642 -0
- package/bin/cli.js +12 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/dependencies.d.ts +82 -0
- package/dist/config/dependencies.d.ts.map +1 -0
- package/dist/config/dependencies.js +82 -0
- package/dist/config/dependencies.js.map +1 -0
- package/dist/config/presets.d.ts +3 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/presets.js +174 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/generators/index.d.ts +40 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +130 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/onboarding.d.ts +8 -0
- package/dist/generators/onboarding.d.ts.map +1 -0
- package/dist/generators/onboarding.js +141 -0
- package/dist/generators/onboarding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/features.d.ts +14 -0
- package/dist/prompts/features.d.ts.map +1 -0
- package/dist/prompts/features.js +96 -0
- package/dist/prompts/features.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +93 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +6 -0
- package/dist/prompts/onboarding.d.ts.map +1 -0
- package/dist/prompts/onboarding.js +37 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/orm.d.ts +3 -0
- package/dist/prompts/orm.d.ts.map +1 -0
- package/dist/prompts/orm.js +23 -0
- package/dist/prompts/orm.js.map +1 -0
- package/dist/prompts/packageManager.d.ts +2 -0
- package/dist/prompts/packageManager.d.ts.map +1 -0
- package/dist/prompts/packageManager.js +18 -0
- package/dist/prompts/packageManager.js.map +1 -0
- package/dist/prompts/platform.d.ts +3 -0
- package/dist/prompts/platform.d.ts.map +1 -0
- package/dist/prompts/platform.js +21 -0
- package/dist/prompts/platform.js.map +1 -0
- package/dist/prompts/preset.d.ts +4 -0
- package/dist/prompts/preset.d.ts.map +1 -0
- package/dist/prompts/preset.js +165 -0
- package/dist/prompts/preset.js.map +1 -0
- package/dist/prompts/project.d.ts +2 -0
- package/dist/prompts/project.d.ts.map +1 -0
- package/dist/prompts/project.js +27 -0
- package/dist/prompts/project.js.map +1 -0
- package/dist/prompts/sdks.d.ts +2 -0
- package/dist/prompts/sdks.d.ts.map +1 -0
- package/dist/prompts/sdks.js +46 -0
- package/dist/prompts/sdks.js.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cleanup.d.ts +5 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +38 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/copy.d.ts +10 -0
- package/dist/utils/copy.d.ts.map +1 -0
- package/dist/utils/copy.js +53 -0
- package/dist/utils/copy.js.map +1 -0
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +136 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +33 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package.d.ts +16 -0
- package/dist/utils/package.d.ts.map +1 -0
- package/dist/utils/package.js +86 -0
- package/dist/utils/package.js.map +1 -0
- package/dist/utils/system-validation.d.ts +9 -0
- package/dist/utils/system-validation.d.ts.map +1 -0
- package/dist/utils/system-validation.js +31 -0
- package/dist/utils/system-validation.js.map +1 -0
- package/dist/utils/template.d.ts +20 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +234 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +94 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +96 -0
- package/templates/base/backend/.dockerignore.ejs +62 -0
- package/templates/base/backend/.env.example.ejs +116 -0
- package/templates/base/backend/Dockerfile.ejs +142 -0
- package/templates/base/backend/controllers/event-queue/index.ts +20 -0
- package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
- package/templates/base/backend/controllers/rest-api/index.ts +48 -0
- package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
- package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
- package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
- package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
- package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
- package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
- package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
- package/templates/base/backend/domain/device-session/schema.ts +72 -0
- package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
- package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
- package/templates/base/backend/domain/session/schema.ts +29 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
- package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
- package/templates/base/backend/domain/user/schema.ts +14 -0
- package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
- package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
- package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
- package/templates/base/backend/lib/constants.ts.ejs +29 -0
- package/templates/base/backend/package.json.ejs +50 -0
- package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
- package/templates/base/backend/prisma.config.prisma.ts +12 -0
- package/templates/base/backend/tsconfig.json +39 -0
- package/templates/base/backend/utils/db.drizzle.ts +41 -0
- package/templates/base/backend/utils/db.prisma.ts +51 -0
- package/templates/base/backend/utils/email.ts.ejs +35 -0
- package/templates/base/backend/utils/errors.ts +348 -0
- package/templates/base/backend/utils/redis.ts.ejs +279 -0
- package/templates/base/mobile/.env.example.ejs +35 -0
- package/templates/base/mobile/.gitignore.ejs +167 -0
- package/templates/base/mobile/app/+not-found.tsx +85 -0
- package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
- package/templates/base/mobile/app.json.ejs +88 -0
- package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
- package/templates/base/mobile/assets/images/favicon.png +0 -0
- package/templates/base/mobile/assets/images/icon.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
- package/templates/base/mobile/assets/images/paywall_image.png +0 -0
- package/templates/base/mobile/assets/images/splash.png +0 -0
- package/templates/base/mobile/eas.json.ejs +49 -0
- package/templates/base/mobile/metro.config.js +9 -0
- package/templates/base/mobile/package.json.ejs +53 -0
- package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
- package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
- package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
- package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
- package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
- package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
- package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
- package/templates/base/mobile/src/components/ui/index.ts +6 -0
- package/templates/base/mobile/src/constants/Theme.ts +163 -0
- package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
- package/templates/base/mobile/src/services/api.ts.ejs +71 -0
- package/templates/base/mobile/src/services/errorService.ts +179 -0
- package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
- package/templates/base/mobile/src/store/index.ts.ejs +18 -0
- package/templates/base/mobile/src/store/ui.store.ts +100 -0
- package/templates/base/mobile/src/utils/formatters.ts +105 -0
- package/templates/base/mobile/src/utils/logger.ts +73 -0
- package/templates/base/mobile/src/utils/responsive.ts +234 -0
- package/templates/base/mobile/tsconfig.json +32 -0
- package/templates/base/web/.env.example.ejs +26 -0
- package/templates/base/web/components.json +22 -0
- package/templates/base/web/eslint.config.mjs +18 -0
- package/templates/base/web/next.config.ts +7 -0
- package/templates/base/web/package.json.ejs +35 -0
- package/templates/base/web/postcss.config.mjs +7 -0
- package/templates/base/web/public/.gitkeep +0 -0
- package/templates/base/web/public/file.svg +1 -0
- package/templates/base/web/public/globe.svg +1 -0
- package/templates/base/web/public/next.svg +1 -0
- package/templates/base/web/public/vercel.svg +1 -0
- package/templates/base/web/public/window.svg +1 -0
- package/templates/base/web/src/app/favicon.ico +0 -0
- package/templates/base/web/src/app/globals.css +152 -0
- package/templates/base/web/src/app/layout.tsx.ejs +54 -0
- package/templates/base/web/src/app/page.tsx.ejs +92 -0
- package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
- package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
- package/templates/base/web/src/components/theme-toggle.tsx +34 -0
- package/templates/base/web/src/components/ui/button.tsx +62 -0
- package/templates/base/web/src/components/ui/card.tsx +92 -0
- package/templates/base/web/src/components/ui/input.tsx +21 -0
- package/templates/base/web/src/components/ui/label.tsx +24 -0
- package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/web/src/components/ui/spinner.tsx +20 -0
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
- package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
- package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
- package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
- package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
- package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
- package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
- package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
- package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
- package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
- package/templates/base/web/src/lib/utils.ts +6 -0
- package/templates/base/web/src/proxy.ts.ejs +66 -0
- package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
- package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
- package/templates/base/web/tsconfig.json +34 -0
- package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
- package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
- package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
- package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
- package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
- package/templates/features/mobile/auth/components/auth/index.ts +2 -0
- package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
- package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
- package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
- package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
- package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
- package/templates/features/web/.gitkeep +0 -0
- package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
- package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
- package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
- package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
- package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
- package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
- package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
- package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
- package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
- package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
- package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
- package/templates/integrations/mobile/att/services/attService.ts +84 -0
- package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
- package/templates/integrations/mobile/att/store/att.store.ts +162 -0
- package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
- package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
- package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
- package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
- package/templates/integrations/web/.gitkeep +0 -0
- package/templates/shared/.env.example.ejs +21 -0
- package/templates/shared/.gitignore.ejs +145 -0
- package/templates/shared/README.md.ejs +134 -0
- package/templates/shared/docker-compose.prod.yml.ejs +120 -0
- package/templates/shared/docker-compose.yml.ejs +129 -0
- package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
- package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
- package/templates/shared/scripts/setup.sh.ejs +979 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { db } from "../../utils/db";
|
|
2
|
+
import { ErrorFactory } from "../../utils/errors";
|
|
3
|
+
import {
|
|
4
|
+
DeviceSession,
|
|
5
|
+
CreateDeviceSessionBody,
|
|
6
|
+
CreateDeviceSessionResponse,
|
|
7
|
+
DeviceSessionMigrationEligibilityResponse,
|
|
8
|
+
} from "./schema";
|
|
9
|
+
|
|
10
|
+
// Generate a UUID v4 for session tokens
|
|
11
|
+
const generateSessionToken = (): string => {
|
|
12
|
+
return crypto.randomUUID();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const createDeviceSession = async (data: CreateDeviceSessionBody): Promise<CreateDeviceSessionResponse> => {
|
|
16
|
+
const { deviceId } = data;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const sessionToken = generateSessionToken();
|
|
20
|
+
|
|
21
|
+
const session = await db.deviceSession.create({
|
|
22
|
+
data: {
|
|
23
|
+
deviceId,
|
|
24
|
+
sessionToken,
|
|
25
|
+
preferredCurrency: 'USD',
|
|
26
|
+
migrated: false,
|
|
27
|
+
lastActiveAt: new Date(),
|
|
28
|
+
},
|
|
29
|
+
select: {
|
|
30
|
+
id: true,
|
|
31
|
+
deviceId: true,
|
|
32
|
+
sessionToken: true,
|
|
33
|
+
createdAt: true,
|
|
34
|
+
lastActiveAt: true,
|
|
35
|
+
migrated: true,
|
|
36
|
+
migratedToUserId: true,
|
|
37
|
+
preferredCurrency: true,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
session: {
|
|
43
|
+
...session,
|
|
44
|
+
createdAt: session.createdAt.toISOString(),
|
|
45
|
+
lastActiveAt: session.lastActiveAt.toISOString(),
|
|
46
|
+
},
|
|
47
|
+
sessionToken,
|
|
48
|
+
};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw ErrorFactory.databaseError({
|
|
51
|
+
operation: 'createDeviceSession',
|
|
52
|
+
deviceId,
|
|
53
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const validateDeviceSession = async (sessionToken: string): Promise<DeviceSession | null> => {
|
|
59
|
+
try {
|
|
60
|
+
const session = await db.deviceSession.findUnique({
|
|
61
|
+
where: { sessionToken },
|
|
62
|
+
select: {
|
|
63
|
+
id: true,
|
|
64
|
+
deviceId: true,
|
|
65
|
+
sessionToken: true,
|
|
66
|
+
createdAt: true,
|
|
67
|
+
lastActiveAt: true,
|
|
68
|
+
migrated: true,
|
|
69
|
+
migratedToUserId: true,
|
|
70
|
+
preferredCurrency: true,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!session) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if session is expired (30 days of inactivity)
|
|
79
|
+
const thirtyDaysAgo = new Date();
|
|
80
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
81
|
+
|
|
82
|
+
if (session.lastActiveAt < thirtyDaysAgo) {
|
|
83
|
+
// Session expired, delete it
|
|
84
|
+
await deleteDeviceSession(sessionToken);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
...session,
|
|
90
|
+
createdAt: session.createdAt.toISOString(),
|
|
91
|
+
lastActiveAt: session.lastActiveAt.toISOString(),
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw ErrorFactory.databaseError({
|
|
95
|
+
operation: 'validateDeviceSession',
|
|
96
|
+
sessionToken,
|
|
97
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const updateDeviceSessionActivity = async (sessionToken: string): Promise<void> => {
|
|
103
|
+
try {
|
|
104
|
+
await db.deviceSession.update({
|
|
105
|
+
where: { sessionToken },
|
|
106
|
+
data: { lastActiveAt: new Date() },
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw ErrorFactory.databaseError({
|
|
110
|
+
operation: 'updateDeviceSessionActivity',
|
|
111
|
+
sessionToken,
|
|
112
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const deleteDeviceSession = async (sessionToken: string): Promise<void> => {
|
|
118
|
+
try {
|
|
119
|
+
await db.deviceSession.delete({
|
|
120
|
+
where: { sessionToken },
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// Don't throw error if session doesn't exist
|
|
124
|
+
if (error instanceof Error && error.message.includes('Record to delete does not exist')) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw ErrorFactory.databaseError({
|
|
129
|
+
operation: 'deleteDeviceSession',
|
|
130
|
+
sessionToken,
|
|
131
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const validateDeviceSessionMigrationEligibility = async (sessionToken: string): Promise<DeviceSessionMigrationEligibilityResponse> => {
|
|
137
|
+
try {
|
|
138
|
+
const session = await validateDeviceSession(sessionToken);
|
|
139
|
+
|
|
140
|
+
if (!session) {
|
|
141
|
+
return {
|
|
142
|
+
canMigrate: false,
|
|
143
|
+
reason: 'Device session not found or expired',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (session.migrated) {
|
|
148
|
+
return {
|
|
149
|
+
canMigrate: false,
|
|
150
|
+
reason: 'Device session already migrated to user account',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
canMigrate: true,
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw ErrorFactory.databaseError({
|
|
159
|
+
operation: 'validateDeviceSessionMigrationEligibility',
|
|
160
|
+
sessionToken,
|
|
161
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Migrate a device session to a user account
|
|
168
|
+
* This is called after a user signs up through BetterAuth to link their device session
|
|
169
|
+
*/
|
|
170
|
+
export const migrateDeviceSessionToUser = async (sessionToken: string, userId: string): Promise<void> => {
|
|
171
|
+
try {
|
|
172
|
+
await db.deviceSession.update({
|
|
173
|
+
where: { sessionToken },
|
|
174
|
+
data: {
|
|
175
|
+
migrated: true,
|
|
176
|
+
migratedToUserId: userId,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw ErrorFactory.databaseError({
|
|
181
|
+
operation: 'migrateDeviceSessionToUser',
|
|
182
|
+
sessionToken,
|
|
183
|
+
userId,
|
|
184
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const cleanupExpiredDeviceSessions = async (): Promise<number> => {
|
|
190
|
+
try {
|
|
191
|
+
// Delete sessions older than 30 days of inactivity
|
|
192
|
+
const thirtyDaysAgo = new Date();
|
|
193
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
194
|
+
|
|
195
|
+
const result = await db.deviceSession.deleteMany({
|
|
196
|
+
where: {
|
|
197
|
+
lastActiveAt: {
|
|
198
|
+
lt: thirtyDaysAgo,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return result.count;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw ErrorFactory.databaseError({
|
|
206
|
+
operation: 'cleanupExpiredDeviceSessions',
|
|
207
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const getDeviceSessionById = async (sessionId: string): Promise<DeviceSession | null> => {
|
|
213
|
+
try {
|
|
214
|
+
const session = await db.deviceSession.findUnique({
|
|
215
|
+
where: { id: sessionId },
|
|
216
|
+
select: {
|
|
217
|
+
id: true,
|
|
218
|
+
deviceId: true,
|
|
219
|
+
sessionToken: true,
|
|
220
|
+
createdAt: true,
|
|
221
|
+
lastActiveAt: true,
|
|
222
|
+
migrated: true,
|
|
223
|
+
migratedToUserId: true,
|
|
224
|
+
preferredCurrency: true,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!session) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
...session,
|
|
234
|
+
createdAt: session.createdAt.toISOString(),
|
|
235
|
+
lastActiveAt: session.lastActiveAt.toISOString(),
|
|
236
|
+
};
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw ErrorFactory.databaseError({
|
|
239
|
+
operation: 'getDeviceSessionById',
|
|
240
|
+
sessionId,
|
|
241
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const getDeviceSessionByToken = async (sessionToken: string): Promise<DeviceSession | null> => {
|
|
247
|
+
return validateDeviceSession(sessionToken);
|
|
248
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Static, Type } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
// Core DeviceSession entity schema (anonymous device sessions before authentication)
|
|
4
|
+
export const DeviceSessionSchema = Type.Object({
|
|
5
|
+
id: Type.String(),
|
|
6
|
+
deviceId: Type.String(),
|
|
7
|
+
sessionToken: Type.String(),
|
|
8
|
+
createdAt: Type.String({ format: 'date-time' }),
|
|
9
|
+
lastActiveAt: Type.String({ format: 'date-time' }),
|
|
10
|
+
migrated: Type.Boolean(),
|
|
11
|
+
migratedToUserId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
12
|
+
preferredCurrency: Type.String({ default: 'USD' }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type DeviceSession = Static<typeof DeviceSessionSchema>;
|
|
16
|
+
|
|
17
|
+
// Create device session request schema
|
|
18
|
+
export const CreateDeviceSessionBodySchema = Type.Object({
|
|
19
|
+
deviceId: Type.String({
|
|
20
|
+
minLength: 1,
|
|
21
|
+
maxLength: 128,
|
|
22
|
+
pattern: "^[a-zA-Z0-9_-]+$" // Allow alphanumeric, underscore, and hyphen
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type CreateDeviceSessionBody = Static<typeof CreateDeviceSessionBodySchema>;
|
|
27
|
+
|
|
28
|
+
// Create device session response schema
|
|
29
|
+
export const CreateDeviceSessionResponseSchema = Type.Object({
|
|
30
|
+
session: DeviceSessionSchema,
|
|
31
|
+
sessionToken: Type.String(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type CreateDeviceSessionResponse = Static<typeof CreateDeviceSessionResponseSchema>;
|
|
35
|
+
|
|
36
|
+
// Validate device session request schema (for session token validation)
|
|
37
|
+
export const ValidateDeviceSessionBodySchema = Type.Object({
|
|
38
|
+
sessionToken: Type.String({
|
|
39
|
+
minLength: 36,
|
|
40
|
+
maxLength: 36,
|
|
41
|
+
pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" // UUID format
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type ValidateDeviceSessionBody = Static<typeof ValidateDeviceSessionBodySchema>;
|
|
46
|
+
|
|
47
|
+
// Device session validation response schema
|
|
48
|
+
export const DeviceSessionValidationResponseSchema = Type.Object({
|
|
49
|
+
valid: Type.Boolean(),
|
|
50
|
+
session: Type.Optional(DeviceSessionSchema),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type DeviceSessionValidationResponse = Static<typeof DeviceSessionValidationResponseSchema>;
|
|
54
|
+
|
|
55
|
+
// Update device session activity request schema
|
|
56
|
+
export const UpdateDeviceSessionActivityBodySchema = Type.Object({
|
|
57
|
+
sessionToken: Type.String({
|
|
58
|
+
minLength: 36,
|
|
59
|
+
maxLength: 36,
|
|
60
|
+
pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type UpdateDeviceSessionActivityBody = Static<typeof UpdateDeviceSessionActivityBodySchema>;
|
|
65
|
+
|
|
66
|
+
// Device session migration eligibility response schema
|
|
67
|
+
export const DeviceSessionMigrationEligibilityResponseSchema = Type.Object({
|
|
68
|
+
canMigrate: Type.Boolean(),
|
|
69
|
+
reason: Type.Optional(Type.String()),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export type DeviceSessionMigrationEligibilityResponse = Static<typeof DeviceSessionMigrationEligibilityResponseSchema>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { eq, and } from 'drizzle-orm';
|
|
2
|
+
import { db, schema } from "../../utils/db";
|
|
3
|
+
import { auth } from "../../lib/auth";
|
|
4
|
+
import { ErrorFactory } from "../../utils/errors";
|
|
5
|
+
import type { SessionWithToken, RevokeSessionResponse } from "./schema";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Session Repository (Drizzle)
|
|
9
|
+
*
|
|
10
|
+
* Provides session management operations for the BFF pattern.
|
|
11
|
+
* Note: Session creation/authentication is handled by BetterAuth.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find a session by ID that belongs to the specified user
|
|
16
|
+
*/
|
|
17
|
+
export const findUserSessionById = async (
|
|
18
|
+
sessionId: string,
|
|
19
|
+
userId: string
|
|
20
|
+
): Promise<SessionWithToken | null> => {
|
|
21
|
+
try {
|
|
22
|
+
const [session] = await db
|
|
23
|
+
.select()
|
|
24
|
+
.from(schema.session)
|
|
25
|
+
.where(and(eq(schema.session.id, sessionId), eq(schema.session.userId, userId)))
|
|
26
|
+
.limit(1);
|
|
27
|
+
|
|
28
|
+
if (!session) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id: session.id,
|
|
34
|
+
userId: session.userId,
|
|
35
|
+
token: session.token,
|
|
36
|
+
expiresAt: session.expiresAt.toISOString(),
|
|
37
|
+
createdAt: session.createdAt.toISOString(),
|
|
38
|
+
updatedAt: session.updatedAt.toISOString(),
|
|
39
|
+
ipAddress: session.ipAddress,
|
|
40
|
+
userAgent: session.userAgent,
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw ErrorFactory.databaseError({
|
|
44
|
+
operation: 'findUserSessionById',
|
|
45
|
+
sessionId,
|
|
46
|
+
userId,
|
|
47
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Revoke a session using Better Auth's native revocation
|
|
54
|
+
*/
|
|
55
|
+
export const revokeSessionByToken = async (
|
|
56
|
+
token: string,
|
|
57
|
+
headers: Headers
|
|
58
|
+
): Promise<RevokeSessionResponse> => {
|
|
59
|
+
try {
|
|
60
|
+
const response = await auth.api.revokeSession({
|
|
61
|
+
headers,
|
|
62
|
+
body: { token },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return response as RevokeSessionResponse;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw ErrorFactory.databaseError({
|
|
68
|
+
operation: 'revokeSessionByToken',
|
|
69
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { db } from "../../utils/db";
|
|
2
|
+
import { auth } from "../../lib/auth";
|
|
3
|
+
import { ErrorFactory } from "../../utils/errors";
|
|
4
|
+
import type { SessionWithToken, RevokeSessionResponse } from "./schema";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Session Repository (Prisma)
|
|
8
|
+
*
|
|
9
|
+
* Provides session management operations for the BFF pattern.
|
|
10
|
+
* Note: Session creation/authentication is handled by BetterAuth.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find a session by ID that belongs to the specified user
|
|
15
|
+
*/
|
|
16
|
+
export const findUserSessionById = async (
|
|
17
|
+
sessionId: string,
|
|
18
|
+
userId: string
|
|
19
|
+
): Promise<SessionWithToken | null> => {
|
|
20
|
+
try {
|
|
21
|
+
const session = await db.session.findFirst({
|
|
22
|
+
where: {
|
|
23
|
+
id: sessionId,
|
|
24
|
+
userId: userId,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!session) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id: session.id,
|
|
34
|
+
userId: session.userId,
|
|
35
|
+
token: session.token,
|
|
36
|
+
expiresAt: session.expiresAt.toISOString(),
|
|
37
|
+
createdAt: session.createdAt.toISOString(),
|
|
38
|
+
updatedAt: session.updatedAt.toISOString(),
|
|
39
|
+
ipAddress: session.ipAddress,
|
|
40
|
+
userAgent: session.userAgent,
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw ErrorFactory.databaseError({
|
|
44
|
+
operation: 'findUserSessionById',
|
|
45
|
+
sessionId,
|
|
46
|
+
userId,
|
|
47
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Revoke a session using Better Auth's native revocation
|
|
54
|
+
*/
|
|
55
|
+
export const revokeSessionByToken = async (
|
|
56
|
+
token: string,
|
|
57
|
+
headers: Headers
|
|
58
|
+
): Promise<RevokeSessionResponse> => {
|
|
59
|
+
try {
|
|
60
|
+
const response = await auth.api.revokeSession({
|
|
61
|
+
headers,
|
|
62
|
+
body: { token },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return response as RevokeSessionResponse;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw ErrorFactory.databaseError({
|
|
68
|
+
operation: 'revokeSessionByToken',
|
|
69
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Type, Static } from '@sinclair/typebox';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session Domain Schema
|
|
5
|
+
*
|
|
6
|
+
* Schemas for BFF session operations (session management from web frontend).
|
|
7
|
+
* Note: Session creation/authentication is handled by BetterAuth.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Session with token (used internally for revocation)
|
|
11
|
+
export const SessionWithTokenSchema = Type.Object({
|
|
12
|
+
id: Type.String(),
|
|
13
|
+
userId: Type.String(),
|
|
14
|
+
token: Type.String(),
|
|
15
|
+
expiresAt: Type.String({ format: 'date-time' }),
|
|
16
|
+
createdAt: Type.String({ format: 'date-time' }),
|
|
17
|
+
updatedAt: Type.String({ format: 'date-time' }),
|
|
18
|
+
ipAddress: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
19
|
+
userAgent: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type SessionWithToken = Static<typeof SessionWithTokenSchema>;
|
|
23
|
+
|
|
24
|
+
// Revoke session response
|
|
25
|
+
export const RevokeSessionResponseSchema = Type.Object({
|
|
26
|
+
status: Type.Boolean(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type RevokeSessionResponse = Static<typeof RevokeSessionResponseSchema>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { db, schema } from "../../utils/db";
|
|
3
|
+
import { ErrorFactory } from "../../utils/errors";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* User Repository (Drizzle)
|
|
7
|
+
*
|
|
8
|
+
* Note: User creation and authentication is handled by BetterAuth.
|
|
9
|
+
* This repository provides helper functions for user lookups and profile updates.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const getUser = async (id: string) => {
|
|
13
|
+
try {
|
|
14
|
+
const [user] = await db
|
|
15
|
+
.select({
|
|
16
|
+
id: schema.user.id,
|
|
17
|
+
email: schema.user.email,
|
|
18
|
+
emailVerified: schema.user.emailVerified,
|
|
19
|
+
name: schema.user.name,
|
|
20
|
+
image: schema.user.image,
|
|
21
|
+
createdAt: schema.user.createdAt,
|
|
22
|
+
updatedAt: schema.user.updatedAt,
|
|
23
|
+
})
|
|
24
|
+
.from(schema.user)
|
|
25
|
+
.where(eq(schema.user.id, id))
|
|
26
|
+
.limit(1);
|
|
27
|
+
|
|
28
|
+
return user || null;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw ErrorFactory.databaseError({
|
|
31
|
+
operation: 'getUser',
|
|
32
|
+
userId: id,
|
|
33
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const getUserByEmail = async (email: string) => {
|
|
39
|
+
try {
|
|
40
|
+
const [user] = await db
|
|
41
|
+
.select({
|
|
42
|
+
id: schema.user.id,
|
|
43
|
+
email: schema.user.email,
|
|
44
|
+
emailVerified: schema.user.emailVerified,
|
|
45
|
+
name: schema.user.name,
|
|
46
|
+
image: schema.user.image,
|
|
47
|
+
createdAt: schema.user.createdAt,
|
|
48
|
+
updatedAt: schema.user.updatedAt,
|
|
49
|
+
})
|
|
50
|
+
.from(schema.user)
|
|
51
|
+
.where(eq(schema.user.email, email))
|
|
52
|
+
.limit(1);
|
|
53
|
+
|
|
54
|
+
return user || null;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw ErrorFactory.databaseError({
|
|
57
|
+
operation: 'getUserByEmail',
|
|
58
|
+
email,
|
|
59
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const isUserExistByEmail = async (email: string): Promise<boolean> => {
|
|
65
|
+
try {
|
|
66
|
+
const [result] = await db
|
|
67
|
+
.select({ id: schema.user.id })
|
|
68
|
+
.from(schema.user)
|
|
69
|
+
.where(eq(schema.user.email, email))
|
|
70
|
+
.limit(1);
|
|
71
|
+
|
|
72
|
+
return !!result;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw ErrorFactory.databaseError({
|
|
75
|
+
operation: 'isUserExistByEmail',
|
|
76
|
+
email,
|
|
77
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const updateUserProfile = async (
|
|
83
|
+
userId: string,
|
|
84
|
+
data: { name?: string }
|
|
85
|
+
) => {
|
|
86
|
+
try {
|
|
87
|
+
const [user] = await db
|
|
88
|
+
.update(schema.user)
|
|
89
|
+
.set({
|
|
90
|
+
...data,
|
|
91
|
+
updatedAt: new Date(),
|
|
92
|
+
})
|
|
93
|
+
.where(eq(schema.user.id, userId))
|
|
94
|
+
.returning({
|
|
95
|
+
id: schema.user.id,
|
|
96
|
+
email: schema.user.email,
|
|
97
|
+
emailVerified: schema.user.emailVerified,
|
|
98
|
+
name: schema.user.name,
|
|
99
|
+
image: schema.user.image,
|
|
100
|
+
createdAt: schema.user.createdAt,
|
|
101
|
+
updatedAt: schema.user.updatedAt,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return user;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw ErrorFactory.databaseError({
|
|
107
|
+
operation: 'updateUserProfile',
|
|
108
|
+
userId,
|
|
109
|
+
updateData: data,
|
|
110
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const deleteUser = async (userId: string): Promise<void> => {
|
|
116
|
+
try {
|
|
117
|
+
await db
|
|
118
|
+
.delete(schema.user)
|
|
119
|
+
.where(eq(schema.user.id, userId));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw ErrorFactory.databaseError({
|
|
122
|
+
operation: 'deleteUser',
|
|
123
|
+
userId,
|
|
124
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|