feat: simplify testing structure into api and e2e categories
This commit is contained in:
@@ -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
67
src/api/profile.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -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
146
src/routes/profile/edit.tsx
Normal 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";
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user