diff --git a/package.json b/package.json index 5f305d8..3639fb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bun-react-template", - "version": "0.1.3", + "version": "0.1.4", "private": true, "type": "module", "scripts": { diff --git a/src/app.ts b/src/app.ts index 91e2f03..4a4d98d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,9 @@ import { getMinioDownloadUrl, uploadBugImage } from './lib/minio' import { addConnection, broadcastToAdmins, getOnlineUserIds, removeConnection } from './lib/presence' import { parseSchema } from './lib/schema-parser' +const isProduction = process.env.NODE_ENV === 'production' +const cookieFlags = isProduction ? '; Secure' : '' + function getPublicOrigin(request: Request): string { if (process.env.BUN_PUBLIC_BASE_URL) return process.env.BUN_PUBLIC_BASE_URL.replace(/\/$/, '') const url = new URL(request.url) @@ -127,7 +130,7 @@ export function createApp() { }) const headers = new Headers() headers.set('Location', `https://accounts.google.com/o/oauth2/v2/auth?${params}`) - headers.set('Set-Cookie', `oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`) + headers.set('Set-Cookie', `oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600${cookieFlags}`) return new Response(null, { status: 302, headers }) }, { detail: { @@ -212,8 +215,8 @@ export function createApp() { const redirectPath = user.role === 'DEVELOPER' ? '/dev' : user.role === 'USER' ? '/profile' : '/dashboard' const headers = new Headers() headers.append('Location', redirectPath) - headers.append('Set-Cookie', `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`) - headers.append('Set-Cookie', 'oauth_state=; Path=/; HttpOnly; Max-Age=0') + headers.append('Set-Cookie', `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${cookieFlags}`) + headers.append('Set-Cookie', `oauth_state=; Path=/; HttpOnly; Max-Age=0${cookieFlags}`) return new Response(null, { status: 302, headers }) }, { detail: { @@ -241,7 +244,7 @@ export function createApp() { const token = crypto.randomUUID() const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours await prisma.session.create({ data: { token, userId: user.id, expiresAt } }) - set.headers['set-cookie'] = `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400` + set.headers['set-cookie'] = `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${cookieFlags}` await createSystemLog(user.id, 'LOGIN', 'Logged in successfully') return { user: { id: user.id, name: user.name, email: user.email, role: user.role, image: user.image } } }, { @@ -266,7 +269,7 @@ export function createApp() { await prisma.session.deleteMany({ where: { token } }) } } - set.headers['set-cookie'] = 'session=; Path=/; HttpOnly; Max-Age=0' + set.headers['set-cookie'] = `session=; Path=/; HttpOnly; Max-Age=0${cookieFlags}` return { ok: true } }, { detail: { diff --git a/src/frontend/components/StatsCard.tsx b/src/frontend/components/StatsCard.tsx index 8525456..09e9ffd 100644 --- a/src/frontend/components/StatsCard.tsx +++ b/src/frontend/components/StatsCard.tsx @@ -14,18 +14,21 @@ interface StatsCardProps { } export function StatsCard({ title, value, description, icon: Icon, color, trend }: StatsCardProps) { + const accentColor = `var(--mantine-color-${color ?? 'brand-blue'}-5)` + return ( ({ + styles={{ root: { backgroundColor: 'var(--mantine-color-body)', borderColor: 'rgba(128,128,128,0.1)', + borderTop: `3px solid ${accentColor}`, }, - })} + }} > - - - - - + + {/* background blobs */} + + + - Bun + Elysia + Vite + React +
+ + logo - - Full-stack starter template with Mantine UI, TanStack Router, and session-based auth. - + + + Monitoring System + + + Pantau semua aplikasi dalam satu tempat, real-time. + + - - - - - - + +
+
) } diff --git a/src/frontend/routes/login.tsx b/src/frontend/routes/login.tsx index ff41ee9..e3d595b 100644 --- a/src/frontend/routes/login.tsx +++ b/src/frontend/routes/login.tsx @@ -1,10 +1,10 @@ import { useLogin } from '@/frontend/hooks/useAuth' import { Alert, + Box, Button, Center, Divider, - Paper, PasswordInput, Stack, Text, @@ -38,6 +38,14 @@ export const Route = createFileRoute('/login')({ component: LoginPage, }) +const OAUTH_ERRORS: Record = { + google_denied: 'Login dengan Google dibatalkan.', + invalid_state: 'Sesi OAuth tidak valid, silakan coba lagi.', + token_failed: 'Gagal menukar token Google, silakan coba lagi.', + userinfo_failed: 'Gagal mengambil info akun Google, silakan coba lagi.', + account_disabled: 'Akun Anda telah dinonaktifkan. Hubungi admin untuk informasi lebih lanjut.', +} + function LoginPage() { const login = useLogin() const { error: searchError } = Route.useSearch() @@ -49,69 +57,117 @@ function LoginPage() { login.mutate({ email, password }) } + const errorMessage = login.isError + ? login.error.message + : searchError + ? (OAUTH_ERRORS[searchError] ?? 'Login dengan Google gagal, silakan coba lagi.') + : null + return ( -
- -
- - - Login - + + {/* background blobs */} + + + - {(login.isError || searchError) && ( - } color="red" variant="light"> - {login.isError ? login.error.message : ( - { - google_denied: 'Login dengan Google dibatalkan.', - invalid_state: 'Sesi OAuth tidak valid, silakan coba lagi.', - token_failed: 'Gagal menukar token Google, silakan coba lagi.', - userinfo_failed: 'Gagal mengambil info akun Google, silakan coba lagi.', - account_disabled: 'Akun Anda telah dinonaktifkan. Hubungi admin untuk informasi lebih lanjut.', - }[searchError ?? ''] ?? 'Login dengan Google gagal, silakan coba lagi.' - )} - - )} +
+ + + + {/* header */} + + logo + + Monitoring System + + + Masuk untuk melanjutkan + + - } - value={email} - onChange={(e) => setEmail(e.currentTarget.value)} - required - /> + {errorMessage && ( + } color="red" variant="light"> + {errorMessage} + + )} - } - value={password} - onChange={(e) => setPassword(e.currentTarget.value)} - required - /> + } + value={email} + onChange={(e) => setEmail(e.currentTarget.value)} + required + /> - + } + value={password} + onChange={(e) => setPassword(e.currentTarget.value)} + required + /> - + - - - - -
+ + + +
+ + +
+
) }