feat: simplify testing structure into api and e2e categories

This commit is contained in:
bipproduction
2026-02-08 11:01:55 +08:00
parent 4640b72ca6
commit 0f71798389
18 changed files with 1006 additions and 62 deletions

View File

@@ -4,6 +4,7 @@ import Elysia from "elysia";
import { apiMiddleware } from "../middleware/apiMiddleware";
import { auth } from "../utils/auth";
import { apikey } from "./apikey";
import { profile } from "./profile";
const isProduction = process.env.NODE_ENV === "production";
@@ -18,7 +19,8 @@ const api = new Elysia({
return { data };
})
.use(apiMiddleware)
.use(apikey);
.use(apikey)
.use(profile);
if (!isProduction) {
api.use(

67
src/api/profile.ts Normal file
View File

@@ -0,0 +1,67 @@
import Elysia, { t } from "elysia";
import { prisma } from "../utils/db";
import logger from "../utils/logger";
export const profile = new Elysia({
prefix: "/profile",
}).post(
"/update",
async (ctx) => {
const { body, set, user } = ctx as any;
try {
if (!user) {
set.status = 401;
return { error: "Unauthorized" };
}
const { name, image } = body;
const updatedUser = await prisma.user.update({
where: { id: user.id },
data: {
name: name || undefined,
image: image || undefined,
},
select: {
id: true,
name: true,
email: true,
image: true,
role: true,
},
});
logger.info({ userId: user.id }, "Profile updated successfully");
return { user: updatedUser };
} catch (error) {
logger.error({ error, userId: user?.id }, "Failed to update profile");
set.status = 500;
return { error: "Failed to update profile" };
}
},
{
body: t.Object({
name: t.Optional(t.String()),
image: t.Optional(t.String()),
}),
response: {
200: t.Object({
user: t.Object({
id: t.String(),
name: t.Any(),
email: t.String(),
image: t.Any(),
role: t.Any(),
}),
}),
401: t.Object({ error: t.String() }),
500: t.Object({ error: t.String() }),
},
detail: {
summary: "Update user profile",
description: "Update the authenticated user's name or profile image",
},
},
);

View File

@@ -11,12 +11,13 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as SignupRouteImport } from './routes/signup'
import { Route as SigninRouteImport } from './routes/signin'
import { Route as ProfileRouteImport } from './routes/profile'
import { Route as DashboardRouteRouteImport } from './routes/dashboard/route'
import { Route as IndexRouteImport } from './routes/index'
import { Route as UsersIndexRouteImport } from './routes/users/index'
import { Route as ProfileIndexRouteImport } from './routes/profile/index'
import { Route as DashboardIndexRouteImport } from './routes/dashboard/index'
import { Route as UsersIdRouteImport } from './routes/users/$id'
import { Route as ProfileEditRouteImport } from './routes/profile/edit'
import { Route as DashboardUsersRouteImport } from './routes/dashboard/users'
import { Route as DashboardSettingsRouteImport } from './routes/dashboard/settings'
import { Route as DashboardApikeyRouteImport } from './routes/dashboard/apikey'
@@ -31,11 +32,6 @@ const SigninRoute = SigninRouteImport.update({
path: '/signin',
getParentRoute: () => rootRouteImport,
} as any)
const ProfileRoute = ProfileRouteImport.update({
id: '/profile',
path: '/profile',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardRouteRoute = DashboardRouteRouteImport.update({
id: '/dashboard',
path: '/dashboard',
@@ -51,6 +47,11 @@ const UsersIndexRoute = UsersIndexRouteImport.update({
path: '/users/',
getParentRoute: () => rootRouteImport,
} as any)
const ProfileIndexRoute = ProfileIndexRouteImport.update({
id: '/profile/',
path: '/profile/',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardIndexRoute = DashboardIndexRouteImport.update({
id: '/',
path: '/',
@@ -61,6 +62,11 @@ const UsersIdRoute = UsersIdRouteImport.update({
path: '/users/$id',
getParentRoute: () => rootRouteImport,
} as any)
const ProfileEditRoute = ProfileEditRouteImport.update({
id: '/profile/edit',
path: '/profile/edit',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardUsersRoute = DashboardUsersRouteImport.update({
id: '/users',
path: '/users',
@@ -80,40 +86,43 @@ const DashboardApikeyRoute = DashboardApikeyRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRouteRouteWithChildren
'/profile': typeof ProfileRoute
'/signin': typeof SigninRoute
'/signup': typeof SignupRoute
'/dashboard/apikey': typeof DashboardApikeyRoute
'/dashboard/settings': typeof DashboardSettingsRoute
'/dashboard/users': typeof DashboardUsersRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
'/dashboard/': typeof DashboardIndexRoute
'/profile/': typeof ProfileIndexRoute
'/users/': typeof UsersIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/profile': typeof ProfileRoute
'/signin': typeof SigninRoute
'/signup': typeof SignupRoute
'/dashboard/apikey': typeof DashboardApikeyRoute
'/dashboard/settings': typeof DashboardSettingsRoute
'/dashboard/users': typeof DashboardUsersRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
'/dashboard': typeof DashboardIndexRoute
'/profile': typeof ProfileIndexRoute
'/users': typeof UsersIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/dashboard': typeof DashboardRouteRouteWithChildren
'/profile': typeof ProfileRoute
'/signin': typeof SigninRoute
'/signup': typeof SignupRoute
'/dashboard/apikey': typeof DashboardApikeyRoute
'/dashboard/settings': typeof DashboardSettingsRoute
'/dashboard/users': typeof DashboardUsersRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
'/dashboard/': typeof DashboardIndexRoute
'/profile/': typeof ProfileIndexRoute
'/users/': typeof UsersIndexRoute
}
export interface FileRouteTypes {
@@ -121,49 +130,53 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/dashboard'
| '/profile'
| '/signin'
| '/signup'
| '/dashboard/apikey'
| '/dashboard/settings'
| '/dashboard/users'
| '/profile/edit'
| '/users/$id'
| '/dashboard/'
| '/profile/'
| '/users/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/profile'
| '/signin'
| '/signup'
| '/dashboard/apikey'
| '/dashboard/settings'
| '/dashboard/users'
| '/profile/edit'
| '/users/$id'
| '/dashboard'
| '/profile'
| '/users'
id:
| '__root__'
| '/'
| '/dashboard'
| '/profile'
| '/signin'
| '/signup'
| '/dashboard/apikey'
| '/dashboard/settings'
| '/dashboard/users'
| '/profile/edit'
| '/users/$id'
| '/dashboard/'
| '/profile/'
| '/users/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DashboardRouteRoute: typeof DashboardRouteRouteWithChildren
ProfileRoute: typeof ProfileRoute
SigninRoute: typeof SigninRoute
SignupRoute: typeof SignupRoute
ProfileEditRoute: typeof ProfileEditRoute
UsersIdRoute: typeof UsersIdRoute
ProfileIndexRoute: typeof ProfileIndexRoute
UsersIndexRoute: typeof UsersIndexRoute
}
@@ -183,13 +196,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SigninRouteImport
parentRoute: typeof rootRouteImport
}
'/profile': {
id: '/profile'
path: '/profile'
fullPath: '/profile'
preLoaderRoute: typeof ProfileRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard': {
id: '/dashboard'
path: '/dashboard'
@@ -211,6 +217,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof UsersIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/profile/': {
id: '/profile/'
path: '/profile'
fullPath: '/profile/'
preLoaderRoute: typeof ProfileIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard/': {
id: '/dashboard/'
path: '/'
@@ -225,6 +238,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof UsersIdRouteImport
parentRoute: typeof rootRouteImport
}
'/profile/edit': {
id: '/profile/edit'
path: '/profile/edit'
fullPath: '/profile/edit'
preLoaderRoute: typeof ProfileEditRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard/users': {
id: '/dashboard/users'
path: '/users'
@@ -270,10 +290,11 @@ const DashboardRouteRouteWithChildren = DashboardRouteRoute._addFileChildren(
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DashboardRouteRoute: DashboardRouteRouteWithChildren,
ProfileRoute: ProfileRoute,
SigninRoute: SigninRoute,
SignupRoute: SignupRoute,
ProfileEditRoute: ProfileEditRoute,
UsersIdRoute: UsersIdRoute,
ProfileIndexRoute: ProfileIndexRoute,
UsersIndexRoute: UsersIndexRoute,
}
export const routeTree = rootRouteImport

146
src/routes/profile/edit.tsx Normal file
View File

@@ -0,0 +1,146 @@
import {
Button,
Card,
Container,
Divider,
Group,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { IconChevronLeft, IconEdit } from "@tabler/icons-react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useSnapshot } from "valtio";
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
import { apiClient } from "@/utils/api-client";
import { authStore } from "../../store/auth";
export const Route = createFileRoute("/profile/edit")({
component: EditProfile,
beforeLoad: protectedRouteMiddleware,
onEnter({ context }) {
authStore.user = context?.user as any;
authStore.session = context?.session as any;
},
});
function EditProfile() {
const snap = useSnapshot(authStore);
const navigate = useNavigate();
const [isUpdating, setIsUpdating] = useState(false);
const form = useForm({
initialValues: {
name: snap.user?.name || "",
image: snap.user?.image || "",
},
validate: {
name: (value) =>
value.length < 2 ? "Name must have at least 2 letters" : null,
},
});
const handleUpdateProfile = async (values: typeof form.values) => {
try {
setIsUpdating(true);
const { data, error } = await apiClient.POST("/api/profile/update", {
body: values,
});
if (data?.user) {
authStore.user = {
...authStore.user,
...data.user,
} as any;
navigate({ to: "/profile" });
} else if (error) {
console.error("Update error:", error);
}
} catch (err) {
console.error("Failed to update profile:", err);
} finally {
setIsUpdating(false);
}
};
return (
<Container size="sm" py={50}>
<Stack gap="xl">
<Group justify="space-between" align="center">
<Box>
<Title order={1} c="#f3d5a3">
Edit Profil
</Title>
<Text c="dimmed" size="sm">
Perbarui informasi profil publik Anda
</Text>
</Box>
<Button
variant="subtle"
color="gray"
leftSection={<IconChevronLeft size={18} />}
onClick={() => navigate({ to: "/profile" })}
>
Kembali
</Button>
</Group>
<Divider color="rgba(251, 240, 223, 0.1)" />
<Card
withBorder
radius="md"
p="xl"
bg="rgba(26, 26, 26, 0.5)"
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
>
<form onSubmit={form.onSubmit(handleUpdateProfile)}>
<Stack gap="md">
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap Anda"
{...form.getInputProps("name")}
styles={{
label: { color: "#fbf0df", marginBottom: 8 },
input: {
backgroundColor: "rgba(0,0,0,0.2)",
color: "#fbf0df",
},
}}
/>
<TextInput
label="URL Foto Profil"
placeholder="https://example.com/photo.jpg"
{...form.getInputProps("image")}
styles={{
label: { color: "#fbf0df", marginBottom: 8 },
input: {
backgroundColor: "rgba(0,0,0,0.2)",
color: "#fbf0df",
},
}}
/>
<Button
type="submit"
fullWidth
mt="lg"
size="md"
color="orange"
loading={isUpdating}
leftSection={<IconEdit size={18} />}
>
Simpan Perubahan
</Button>
</Stack>
</form>
</Card>
</Stack>
</Container>
);
}
// Need Box from @mantine/core
import { Box } from "@mantine/core";

View File

@@ -17,12 +17,14 @@ import {
Title,
Tooltip,
} from "@mantine/core";
import { modals } from "@mantine/modals";
import {
IconAt,
IconCheck,
IconCopy,
IconDashboard,
IconEdit,
IconExternalLink,
IconId,
IconLogout,
@@ -34,9 +36,9 @@ import { useState } from "react";
import { useSnapshot } from "valtio";
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
import { authClient } from "@/utils/auth-client";
import { authStore } from "../store/auth";
import { authStore } from "../../store/auth";
export const Route = createFileRoute("/profile")({
export const Route = createFileRoute("/profile/")({
component: Profile,
beforeLoad: protectedRouteMiddleware,
onEnter({ context }) {
@@ -161,6 +163,14 @@ function Profile() {
Admin Panel
</Button>
)}
<Button
variant="light"
color="blue"
leftSection={<IconEdit size={18} />}
onClick={() => navigate({ to: "/profile/edit" })}
>
Edit Profil
</Button>
<Button
variant="outline"
color="red"