Skip to content

Commit d38c538

Browse files
HyteqHyteq
authored andcommitted
step 2 to migration
1 parent c755ec5 commit d38c538

9 files changed

Lines changed: 448 additions & 1 deletion

File tree

apps/api2/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"module": "index.ts",
44
"type": "module",
55
"private": true,
6+
"scripts": {
7+
"dev": "bun --hot src/index.ts"
8+
},
69
"devDependencies": {
710
"@types/bun": "latest"
811
},

apps/api2/src/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { Elysia } from "elysia";
2+
import domainsRouter from "./routes/v1/domains";
23

34
const app = new Elysia();
45

56
app.get('/', () => {
67
return 'Hello World';
8+
})
9+
.use(domainsRouter)
10+
11+
app.listen(3500)
12+
.onStart(() => {
13+
console.log('Server is running on port 3500');
714
});
815

9-
app.listen(3000);
16+
app.onError(({ error, code }) => {
17+
console.error(error);
18+
return {
19+
success: false,
20+
code: code
21+
};
22+
});

apps/api2/src/lib/auth.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { betterAuth } from "better-auth";
2+
import { db, eq, user } from "@databuddy/db";
3+
import { getRedisCache } from "@databuddy/redis";
4+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
5+
import { customSession } from "better-auth/plugins";
6+
7+
function isProduction() {
8+
return process.env.NODE_ENV === 'production';
9+
}
10+
11+
const redisCache = getRedisCache();
12+
13+
export const auth = betterAuth({
14+
database: drizzleAdapter(db, {
15+
provider: "pg",
16+
}),
17+
security: {
18+
ipAddress: {
19+
ipAddressHeaders: ["cf-connecting-ip", "x-forwarded-for"],
20+
}
21+
},
22+
appName: "databuddy.cc",
23+
advanced: {
24+
crossSubDomainCookies: {
25+
enabled: isProduction(),
26+
domain: ".databuddy.cc"
27+
},
28+
cookiePrefix: "databuddy",
29+
useSecureCookies: isProduction()
30+
},
31+
trustedOrigins: [
32+
'https://databuddy.cc',
33+
'https://app.databuddy.cc',
34+
'https://api.databuddy.cc'
35+
],
36+
session: {
37+
expiresIn: 60 * 60 * 24 * 30, // 30 days
38+
updateAge: 60 * 60 * 24, // 1 day
39+
cookieCache: {
40+
enabled: true,
41+
maxAge: 5 * 60 // 5 minutes
42+
}
43+
},
44+
secondaryStorage: {
45+
get: async (key) => {
46+
const value = await redisCache.get(key);
47+
return value ? value : null;
48+
},
49+
set: async (key, value, ttl) => {
50+
if (ttl) await redisCache.setex(key, ttl, value);
51+
else await redisCache.set(key, value);
52+
},
53+
delete: async (key) => {
54+
await redisCache.del(key);
55+
},
56+
},
57+
plugins: [
58+
customSession(async ({ user: sessionUser, session }) => {
59+
const [dbUser] = await db.query.user.findMany({
60+
where: eq(user.id, session.userId),
61+
columns: {
62+
role: true,
63+
}
64+
});
65+
return {
66+
role: dbUser?.role,
67+
user: {
68+
...sessionUser,
69+
role: dbUser?.role,
70+
},
71+
session
72+
};
73+
}),
74+
]
75+
})
76+
77+
export type User = (typeof auth)["$Infer"]["Session"]["user"];
78+
export type Session = (typeof auth)["$Infer"]["Session"];

apps/api2/src/lib/timezone.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/**
2+
* Timezone utilities for handling user timezone detection and date conversions
3+
*/
4+
5+
export interface TimezoneInfo {
6+
timezone: string;
7+
detected: boolean;
8+
source: 'explicit' | 'header' | 'cloudflare' | 'accept-language' | 'default';
9+
}
10+
11+
/**
12+
* Detect user timezone from various sources
13+
*/
14+
export function detectUserTimezone(headers: Headers, explicitTimezone?: string): TimezoneInfo {
15+
// Use explicit timezone if provided and valid
16+
if (explicitTimezone) {
17+
try {
18+
Intl.DateTimeFormat(undefined, { timeZone: explicitTimezone });
19+
return {
20+
timezone: explicitTimezone,
21+
detected: true,
22+
source: 'explicit'
23+
};
24+
} catch {
25+
// Invalid timezone, fall through to header detection
26+
}
27+
}
28+
29+
// Try to get timezone from various headers
30+
const timezoneHeader = headers.get('x-timezone') ||
31+
headers.get('timezone') ||
32+
headers.get('x-user-timezone');
33+
34+
if (timezoneHeader) {
35+
try {
36+
// Validate timezone
37+
Intl.DateTimeFormat(undefined, { timeZone: timezoneHeader });
38+
return {
39+
timezone: timezoneHeader,
40+
detected: true,
41+
source: 'header'
42+
};
43+
} catch {
44+
// Invalid timezone, fall through to other methods
45+
}
46+
}
47+
48+
// Try to infer from CloudFlare headers (if using CloudFlare)
49+
const cfTimezone = headers.get('cf-timezone');
50+
if (cfTimezone) {
51+
try {
52+
Intl.DateTimeFormat(undefined, { timeZone: cfTimezone });
53+
return {
54+
timezone: cfTimezone,
55+
detected: true,
56+
source: 'cloudflare'
57+
};
58+
} catch {
59+
// Invalid timezone
60+
}
61+
}
62+
63+
// Try to extract from Accept-Language header
64+
const acceptLanguage = headers.get('accept-language');
65+
if (acceptLanguage) {
66+
// Look for timezone info in accept-language (some browsers include it)
67+
const timezoneMatch = acceptLanguage.match(/timezone=([^,;]+)/i);
68+
if (timezoneMatch) {
69+
try {
70+
Intl.DateTimeFormat(undefined, { timeZone: timezoneMatch[1] });
71+
return {
72+
timezone: timezoneMatch[1],
73+
detected: true,
74+
source: 'accept-language'
75+
};
76+
} catch {
77+
// Invalid timezone
78+
}
79+
}
80+
}
81+
82+
// Default to UTC if no timezone detected
83+
return {
84+
timezone: 'UTC',
85+
detected: false,
86+
source: 'default'
87+
};
88+
}
89+
90+
/**
91+
* Convert date to user's timezone
92+
*/
93+
export function convertToUserTimezone(date: string, timezone: string): string {
94+
try {
95+
const utcDate = new Date(`${date}T00:00:00Z`);
96+
const userDate = new Date(utcDate.toLocaleString('en-US', { timeZone: timezone }));
97+
return userDate.toISOString().split('T')[0];
98+
} catch {
99+
// If timezone conversion fails, return original date
100+
return date;
101+
}
102+
}
103+
104+
/**
105+
* Adjust date range for user timezone to ensure we capture all relevant data
106+
*/
107+
export function adjustDateRangeForTimezone(
108+
startDate: string,
109+
endDate: string,
110+
timezone: string
111+
): { startDate: string, endDate: string } {
112+
if (timezone === 'UTC') {
113+
return { startDate, endDate };
114+
}
115+
116+
try {
117+
// Convert start date to user timezone (might need to go back a day)
118+
const startUTC = new Date(`${startDate}T00:00:00Z`);
119+
const startInUserTZ = new Date(startUTC.toLocaleString('en-US', { timeZone: timezone }));
120+
121+
// Convert end date to user timezone (might need to go forward a day)
122+
const endUTC = new Date(`${endDate}T23:59:59Z`);
123+
const endInUserTZ = new Date(endUTC.toLocaleString('en-US', { timeZone: timezone }));
124+
125+
// Adjust the range to ensure we capture all data for the user's timezone
126+
const adjustedStart = new Date(startInUserTZ.getTime() - 24 * 60 * 60 * 1000); // Go back 1 day
127+
const adjustedEnd = new Date(endInUserTZ.getTime() + 24 * 60 * 60 * 1000); // Go forward 1 day
128+
129+
return {
130+
startDate: adjustedStart.toISOString().split('T')[0],
131+
endDate: adjustedEnd.toISOString().split('T')[0]
132+
};
133+
} catch {
134+
// If timezone conversion fails, return original dates
135+
return { startDate, endDate };
136+
}
137+
}
138+
139+
/**
140+
* Validate timezone string
141+
*/
142+
export function isValidTimezone(timezone: string): boolean {
143+
try {
144+
Intl.DateTimeFormat(undefined, { timeZone: timezone });
145+
return true;
146+
} catch {
147+
return false;
148+
}
149+
}
150+
151+
/**
152+
* Get current date in user's timezone
153+
*/
154+
export function getCurrentDateInTimezone(timezone: string): string {
155+
try {
156+
const now = new Date();
157+
const userDate = new Date(now.toLocaleString('en-US', { timeZone: timezone }));
158+
return userDate.toISOString().split('T')[0];
159+
} catch {
160+
// If timezone conversion fails, return UTC date
161+
return new Date().toISOString().split('T')[0];
162+
}
163+
}
164+
165+
/**
166+
* Convert timestamp to user's timezone
167+
*/
168+
export function convertTimestampToUserTimezone(timestamp: number, timezone: string): Date {
169+
try {
170+
const utcDate = new Date(timestamp);
171+
return new Date(utcDate.toLocaleString('en-US', { timeZone: timezone }));
172+
} catch {
173+
// If timezone conversion fails, return original date
174+
return new Date(timestamp);
175+
}
176+
}

apps/api2/src/middleware/auth.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Elysia } from "elysia";
2+
import { auth } from "../lib/auth";
3+
4+
type AuthMiddlewareOptions = {
5+
required: boolean;
6+
}
7+
8+
export const authMiddleware = (options: AuthMiddlewareOptions = { required: false }) => {
9+
return new Elysia({ name: 'middleware.auth' })
10+
.derive(async ({ request }) => {
11+
try {
12+
const session = await auth.api.getSession({
13+
headers: request.headers,
14+
});
15+
16+
if (!session) {
17+
return { user: null, session: null };
18+
}
19+
20+
return {
21+
user: session.user,
22+
session,
23+
};
24+
25+
} catch (error) {
26+
console.error("Error getting session:", error);
27+
return { user: null, session: null };
28+
}
29+
})
30+
.onBeforeHandle(({ user, session, set }) => {
31+
if (options.required && (!user || !session)) {
32+
set.status = 401;
33+
return {
34+
success: false,
35+
error: 'Unauthorized',
36+
code: 'AUTH_REQUIRED'
37+
}
38+
}
39+
});
40+
};
41+
42+
export type AuthMiddleware = ReturnType<typeof authMiddleware>;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Elysia } from 'elysia';
2+
import { detectUserTimezone, isValidTimezone } from '../lib/timezone';
3+
4+
export const timezoneMiddleware = new Elysia({ name: 'middleware.timezone' })
5+
.derive(({ request, query }) => {
6+
const explicitTimezone = query.timezone as string | undefined;
7+
8+
if (explicitTimezone && !isValidTimezone(explicitTimezone)) {
9+
throw new Error('Invalid timezone provided');
10+
}
11+
12+
const timezoneInfo = detectUserTimezone(request.headers, explicitTimezone);
13+
14+
return {
15+
timezoneInfo
16+
};
17+
});
18+
19+
export type TimezoneMiddleware = typeof timezoneMiddleware;

0 commit comments

Comments
 (0)