Test AUTH - 30 Jul

This commit is contained in:
2025-07-30 12:11:17 +08:00
parent c11cc421a4
commit 4e61695649
14 changed files with 619 additions and 13 deletions

1
auth Submodule

Submodule auth added at 51d749567a

BIN
bun.lockb

Binary file not shown.

View File

@@ -14,8 +14,10 @@
},
"dependencies": {
"@cubejs-client/core": "^0.31.0",
"@elysiajs/cookie": "^0.8.0",
"@elysiajs/cors": "^1.2.0",
"@elysiajs/eden": "^1.3.2",
"@elysiajs/jwt": "^1.3.2",
"@elysiajs/static": "^1.3.0",
"@elysiajs/stream": "^1.1.0",
"@elysiajs/swagger": "^1.2.0",
@@ -44,6 +46,7 @@
"@types/lodash": "^4.17.16",
"add": "^2.0.6",
"animate.css": "^4.1.1",
"bcryptjs": "^3.0.2",
"bun": "^1.2.2",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
@@ -54,6 +57,7 @@
"framer-motion": "^12.23.5",
"get-port": "^7.1.0",
"jotai": "^2.12.3",
"jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4",
"list": "^2.0.19",
"lodash": "^4.17.21",
@@ -78,15 +82,16 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"parcel": "^2.6.2",
"postcss": "^8.5.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5",
"parcel": "^2.6.2"
"typescript": "^5"
}
}

View File

@@ -1987,27 +1987,49 @@ model JenisProgramYangDiselenggarakan {
// ========================================= PERPUSTAKAAN ========================================= //
model DataPerpustakaan {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
kategori KategoriBuku @relation(fields: [kategoriId], references: [id])
id String @id @default(cuid())
judul String
deskripsi String @db.Text
kategori KategoriBuku @relation(fields: [kategoriId], references: [id])
kategoriId String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model KategoriBuku {
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataPerpustakaan DataPerpustakaan[]
}
model User {
id String @id @default(cuid())
nama String
email String @unique
password String
role Role @relation(fields: [roleId], references: [id])
roleId String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Role {
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataPerpustakaan DataPerpustakaan[]
User User[]
}
// ========================================= DATA PENDIDIKAN ========================================= //

View File

@@ -0,0 +1,220 @@
import { proxy } from "valtio";
import { toast } from "react-toastify";
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { z } from "zod";
const userSchema = z.object({
nama: z.string().min(1, "Nama harus diisi"),
email: z.string().email("Email tidak valid"),
password: z.string().min(6, "Password minimal 6 karakter"),
roleId: z.string().optional(),
});
const defaultForm = {
nama: "",
email: "",
password: "",
roleId: "",
};
const userState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create(isAdmin: boolean = false) {
const valid = userSchema.safeParse(userState.create.form);
if (!valid.success) {
const err = valid.error.issues.map((i) => i.message).join(", ");
return toast.error(err);
}
try {
userState.create.loading = true;
const res = await ApiFetch.api.user[
isAdmin ? "create" : "register"
].post(userState.create.form);
if (res.status === 200) {
toast.success("User berhasil dibuat");
userState.findMany.load();
} else {
toast.error(res.data?.message || "Gagal membuat user");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat membuat user");
} finally {
userState.create.loading = false;
}
},
},
login: {
form: { email: "", password: "" },
loading: false,
async submit() {
try {
userState.login.loading = true;
const res = await ApiFetch.api.user.login.post(userState.login.form);
if (res.status === 200) {
toast.success("Login berhasil");
const token = res.data?.data?.token;
if (typeof token === "string") {
localStorage.setItem("token", token);
}
} else {
toast.error(res.data?.message || "Login gagal");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat login");
} finally {
userState.login.loading = false;
}
},
},
register: {
form: { ...defaultForm },
loading: false,
async submit() {
const valid = userSchema.safeParse(userState.register.form);
if (!valid.success) {
const err = valid.error.issues.map(i => i.message).join(", ");
return toast.error(err);
}
try {
userState.register.loading = true;
const res = await ApiFetch.api.user.register.post(userState.register.form);
if (res.status === 200) {
toast.success("Registrasi berhasil, silakan login");
userState.register.form = { ...defaultForm }; // Reset form
} else {
toast.error(res.data?.message || "Gagal registrasi");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat registrasi");
} finally {
userState.register.loading = false;
}
},
},
findMany: {
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
loading: false,
async load() {
userState.findMany.loading = true;
const res = await ApiFetch.api.user.findMany.get();
if (res.status === 200) {
userState.findMany.data = res.data?.data ?? [];
}
userState.findMany.loading = false;
},
},
findUnique: {
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
loading: false,
async load(id: string) {
try {
userState.findUnique.loading = true;
const res = await fetch(`/api/user/findUnique/${id}`);
const data = await res.json();
if (res.status === 200) {
userState.findUnique.data = data.data ?? null;
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat mengambil data user");
} finally {
userState.findUnique.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
try {
userState.update.loading = true;
const res = await fetch(`/api/user/findUnique/${id}`);
const data = await res.json();
if (res.status === 200) {
const user = data.data;
userState.update.id = user.id;
userState.update.form = {
nama: user.nama,
email: user.email,
password: "",
roleId: user.roleId,
};
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat mengambil data user");
} finally {
userState.update.loading = false;
}
},
async submit() {
try {
userState.update.loading = true;
const res = await fetch(`/api/user/update/${userState.update.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userState.update.form),
});
const data = await res.json();
if (res.status === 200) {
toast.success("Berhasil update user");
userState.findMany.load();
} else {
toast.error(data?.message || "Gagal update user");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat update user");
} finally {
userState.update.loading = false;
}
},
},
delete: {
loading: false,
async submit(id: string) {
try {
userState.delete.loading = true;
const res = await fetch(`/api/user/del/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
const data = await res.json();
if (res.status === 200) {
toast.success("User berhasil dihapus");
userState.findMany.load();
} else {
toast.error(data?.message || "Gagal hapus user");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat hapus user");
} finally {
userState.delete.loading = false;
}
},
},
});
export default userState;

View File

@@ -0,0 +1,51 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
import bcrypt from "bcryptjs";
type FormCreateUser = {
nama: string;
email: string;
password: string;
roleId: string;
isActive?: boolean;
};
export default async function userCreate(context: Context) {
const body = (await context.body) as FormCreateUser;
if (!body.nama || !body.email || !body.password || !body.roleId) {
throw new Error("Semua field wajib diisi");
}
try {
// Cek apakah email sudah terdaftar
const existing = await prisma.user.findUnique({
where: { email: body.email },
});
if (existing) {
throw new Error("Email sudah terdaftar");
}
// Hash password sebelum simpan
const hashedPassword = await bcrypt.hash(body.password, 10);
const result = await prisma.user.create({
data: {
nama: body.nama,
email: body.email,
password: hashedPassword,
roleId: body.roleId,
isActive: body.isActive ?? true,
},
});
return {
success: true,
message: "User berhasil dibuat",
data: result,
};
} catch (error) {
console.error("Error creating user:", error);
throw new Error("Gagal membuat user: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,28 @@
// /api/user/delete.ts
import prisma from '@/lib/prisma';
import { Context } from 'elysia';
export default async function userDelete(context: Context) {
const { id } = context.params as { id: string };
try {
const deleted = await prisma.user.update({
where: { id },
data: {
isActive: false,
},
});
return {
success: true,
message: 'User berhasil dinonaktifkan',
data: deleted,
};
} catch (error) {
console.error(error);
return {
success: false,
message: 'Gagal menghapus user',
};
}
}

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
export default async function userFindMany() {
try {
const data = await prisma.user.findMany({
include: {
role: true,
},
orderBy: {
createdAt: "desc",
},
});
return {
success: true,
message: "Success get all user",
data,
};
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message:
"Gagal mengambil data: " +
(error instanceof Error ? error.message : "Unknown error"),
};
}
}

View File

@@ -0,0 +1,31 @@
import prisma from '@/lib/prisma';
import { Context } from 'elysia';
export default async function userFindUnique(context: Context) {
const { id } = context.params as { id: string };
try {
const user = await prisma.user.findUnique({
where: { id },
include: {
role: true,
},
});
if (!user) {
return { success: false, message: 'User tidak ditemukan' };
}
return {
success: true,
message: 'Berhasil mendapatkan user',
data: user,
};
} catch (error) {
console.error(error);
return {
success: false,
message: 'Gagal mengambil data user',
};
}
}

View File

@@ -0,0 +1,57 @@
import { Elysia, t } from "elysia";
// Import semua handler
import userCreate from "./create";
import userFindMany from "./findMany";
import userFindUnique from "./findUnique";
import userUpdate from "./updt";
import userDelete from "./del"; // `delete` nggak boleh jadi nama file JS langsung, jadi biasanya `del.ts`
import userLogin from "./login";
import userRegister from "./register";
const User = new Elysia({ prefix: "/api/user" })
.post("/register", userRegister, {
body: t.Object({
nama: t.String(),
email: t.String(),
password: t.String(),
}),
})
.post("/login", userLogin, {
body: t.Object({
email: t.String(),
password: t.String(),
}),
})
.post("/create", userCreate, {
body: t.Object({
nama: t.String(),
email: t.String(),
password: t.String(),
roleId: t.String(),
}),
})
.get("/findMany", userFindMany)
.get("/findUnique/:id", userFindUnique)
.put(
"/update/:id",
async (context) => {
const response = await userUpdate(context);
return response;
},
{
body: t.Object({
nama: t.String(),
email: t.String(),
password: t.String(),
roleId: t.String(),
}),
}
)
.put("/del/:id", userDelete, {
params: t.Object({
id: t.String(),
}),
}); // pakai PUT untuk soft delete
export default User;

View File

@@ -0,0 +1,81 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
// ENV atau secret key untuk token
const JWT_SECRET = process.env.JWT_SECRET || "super-secret-key"; // ganti di env production
type LoginForm = {
email: string;
password: string;
};
export default async function userLogin(context: Context) {
const body = (await context.body) as LoginForm;
try {
// 1. Cari user berdasarkan email
const user = await prisma.user.findUnique({
where: { email: body.email },
include: { role: true }, // include role untuk otorisasi
});
// 2. Jika tidak ada user
if (!user) {
return {
success: false,
message: "Email tidak ditemukan",
};
}
// 3. Cek apakah user aktif
if (!user.isActive) {
return {
success: false,
message: "Akun tidak aktif",
};
}
// 4. Verifikasi password
const isMatch = await bcrypt.compare(body.password, user.password);
if (!isMatch) {
return {
success: false,
message: "Password salah",
};
}
// 5. Buat JWT token
const token = jwt.sign(
{
id: user.id,
email: user.email,
role: user.role.name,
},
JWT_SECRET,
{ expiresIn: "7d" } // expire 7 hari
);
// 6. Kirim response
return {
success: true,
message: "Login berhasil",
data: {
user: {
id: user.id,
nama: user.nama,
email: user.email,
role: user.role.name,
},
token,
},
};
} catch (error) {
console.error("Login error:", error);
return {
success: false,
message: "Terjadi kesalahan saat login",
};
}
}

View File

@@ -0,0 +1,43 @@
import prisma from "@/lib/prisma";
import bcrypt from "bcryptjs";
import { Context } from "elysia";
export default async function userRegister(context: Context) {
const body = (await context.body) as {
nama: string;
email: string;
password: string;
};
const existingUser = await prisma.user.findUnique({
where: { email: body.email },
});
if (existingUser) {
return {
success: false,
message: "Email sudah terdaftar",
};
}
const role = await prisma.role.findFirst({ where: { name: "warga" } });
if (!role) throw new Error("Role warga tidak ditemukan");
const hashedPassword = await bcrypt.hash(body.password, 10);
const user = await prisma.user.create({
data: {
nama: body.nama,
email: body.email,
password: hashedPassword,
roleId: role.id,
},
});
return {
success: true,
message: "Berhasil daftar sebagai warga",
data: user,
};
}

View File

@@ -0,0 +1,35 @@
// /api/user/update.ts
import prisma from '@/lib/prisma';
import { Context } from 'elysia';
export default async function userUpdate(context: Context) {
const { id } = context.params as { id: string };
const body = await context.body as {
nama?: string;
email?: string;
password?: string;
roleId?: string;
isActive?: boolean;
};
try {
const updated = await prisma.user.update({
where: { id },
data: {
...body,
},
});
return {
success: true,
message: 'User berhasil diupdate',
data: updated,
};
} catch (error) {
console.error(error);
return {
success: false,
message: 'Gagal mengupdate user',
};
}
}

View File

@@ -23,6 +23,8 @@ import Inovasi from "./_lib/inovasi";
import Lingkungan from "./_lib/lingkungan";
import LandingPage from "./_lib/landing_page";
import Pendidikan from "./_lib/pendidikan";
import User from "./_lib/user";
const ROOT = process.cwd();
@@ -89,6 +91,8 @@ const ApiServer = new Elysia()
.use(Inovasi)
.use(Lingkungan)
.use(Pendidikan)
.use(User)
.onError(({ code }) => {
if (code === "NOT_FOUND") {
return {