From ba19a4dc5bd8cfb4dfdbbae38bfa4b426d591c9f Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 3 Mar 2025 15:42:12 +0800 Subject: [PATCH 1/6] feat: add staff and user authentication APIs with request handling --- app/apis/admin/get-staff.ts | 21 +++++++++++++++++++ app/apis/admin/login-staff.ts | 20 ++++++++++++++++++ app/apis/news/{login.ts => login-user.ts} | 2 +- .../news/{register.ts => register-user.ts} | 2 +- app/routes/_admin.lg-admin.tsx | 19 +++++++++++++++++ app/routes/actions.login.ts | 4 ++-- app/routes/actions.register.ts | 4 ++-- 7 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 app/apis/admin/get-staff.ts create mode 100644 app/apis/admin/login-staff.ts rename app/apis/news/{login.ts => login-user.ts} (88%) rename app/apis/news/{register.ts => register-user.ts} (91%) diff --git a/app/apis/admin/get-staff.ts b/app/apis/admin/get-staff.ts new file mode 100644 index 0000000..ff34ffd --- /dev/null +++ b/app/apis/admin/get-staff.ts @@ -0,0 +1,21 @@ +import { z } from 'zod' + +import { HttpServer, type THttpServer } from '~/libs/http-server' + +const staffSchema = z.object({ + data: z.object({ + id: z.string(), + email: z.string(), + username: z.string(), + }), +}) + +export const getStaff = async (parameters: THttpServer) => { + try { + const { data } = await HttpServer(parameters).get(`/api/staff/profile`) + return staffSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/apis/admin/login-staff.ts b/app/apis/admin/login-staff.ts new file mode 100644 index 0000000..dbdcaf4 --- /dev/null +++ b/app/apis/admin/login-staff.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' + +import { type TLoginSchema } from '~/layouts/news/form-login' +import { HttpServer } from '~/libs/http-server' + +const loginResponseSchema = z.object({ + data: z.object({ + token: z.string(), + }), +}) + +export const staffLoginRequest = async (payload: TLoginSchema) => { + try { + const { data } = await HttpServer().post('/api/staff/login', payload) + return loginResponseSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/apis/news/login.ts b/app/apis/news/login-user.ts similarity index 88% rename from app/apis/news/login.ts rename to app/apis/news/login-user.ts index 5c74146..a25b0ff 100644 --- a/app/apis/news/login.ts +++ b/app/apis/news/login-user.ts @@ -9,7 +9,7 @@ const loginResponseSchema = z.object({ }), }) -export const newsLoginRequest = async (payload: TLoginSchema) => { +export const userLoginRequest = async (payload: TLoginSchema) => { try { const { data } = await HttpServer().post('/api/user/login', payload) return loginResponseSchema.parse(data) diff --git a/app/apis/news/register.ts b/app/apis/news/register-user.ts similarity index 91% rename from app/apis/news/register.ts rename to app/apis/news/register-user.ts index 66c53bf..7c6ab16 100644 --- a/app/apis/news/register.ts +++ b/app/apis/news/register-user.ts @@ -9,7 +9,7 @@ const loginResponseSchema = z.object({ }), }) -export const newsRegisterRequest = async (payload: TRegisterSchema) => { +export const userRegisterRequest = async (payload: TRegisterSchema) => { try { const { subscribe_plan, ...restPayload } = payload const transformedPayload = { diff --git a/app/routes/_admin.lg-admin.tsx b/app/routes/_admin.lg-admin.tsx index 565ed09..6e75f52 100644 --- a/app/routes/_admin.lg-admin.tsx +++ b/app/routes/_admin.lg-admin.tsx @@ -1,7 +1,26 @@ import { Outlet } from 'react-router' +import { getStaff } from '~/apis/admin/get-staff' import { AdminProvider } from '~/contexts/admin' import { AdminDefaultLayout } from '~/layouts/admin/default' +import { handleCookie } from '~/libs/cookies' + +import type { Route } from './+types/_admin.lg-admin' + +export const loader = async ({ request }: Route.LoaderArgs) => { + const { adminToken } = await handleCookie(request) + let adminData + if (adminToken) { + const { data } = await getStaff({ + accessToken: adminToken, + }) + adminData = data + } + + return { + adminData, + } +} const AdminLayout = () => { return ( diff --git a/app/routes/actions.login.ts b/app/routes/actions.login.ts index 28118c2..6923511 100644 --- a/app/routes/actions.login.ts +++ b/app/routes/actions.login.ts @@ -4,7 +4,7 @@ import { getValidatedFormData } from 'remix-hook-form' import { XiorError } from 'xior' import { getUser } from '~/apis/news/get-user' -import { newsLoginRequest } from '~/apis/news/login' +import { userLoginRequest } from '~/apis/news/login-user' import { loginSchema, type TLoginSchema } from '~/layouts/news/form-login' import { generateTokenCookie } from '~/utils/token' @@ -26,7 +26,7 @@ export const action = async ({ request }: Route.ActionArgs) => { return data({ success: false, errors, defaultValues }, { status: 400 }) } - const { data: loginData } = await newsLoginRequest(payload) + const { data: loginData } = await userLoginRequest(payload) const { token } = loginData const { data: userData } = await getUser({ accessToken: token, diff --git a/app/routes/actions.register.ts b/app/routes/actions.register.ts index ae30723..fac144b 100644 --- a/app/routes/actions.register.ts +++ b/app/routes/actions.register.ts @@ -4,7 +4,7 @@ import { getValidatedFormData } from 'remix-hook-form' import { XiorError } from 'xior' import { getUser } from '~/apis/news/get-user' -import { newsRegisterRequest } from '~/apis/news/register' +import { userRegisterRequest } from '~/apis/news/register-user' import { registerSchema, type TRegisterSchema, @@ -29,7 +29,7 @@ export const action = async ({ request }: Route.ActionArgs) => { return data({ success: false, errors, defaultValues }, { status: 400 }) } - const { data: registerData } = await newsRegisterRequest(payload) + const { data: registerData } = await userRegisterRequest(payload) const { token } = registerData const { data: userData } = await getUser({ accessToken: token, From 4f4e94389eb5739f4cdc12ff3e1fe27ef105b091 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 3 Mar 2025 16:33:30 +0800 Subject: [PATCH 2/6] feat: add admin login page and authentication logic with form handling --- app/configs/pages.ts | 6 + app/pages/admin-login/index.tsx | 107 +++++++++++++++++ app/routes/_admin.lg-admin._auth.login.tsx | 108 +----------------- .../_admin.lg-admin._dashboard._index.tsx | 4 +- .../_admin.lg-admin._dashboard.admins.tsx | 4 +- ...min.lg-admin._dashboard.advertisements.tsx | 4 +- .../_admin.lg-admin._dashboard.contents.tsx | 4 +- .../_admin.lg-admin._dashboard.settings.tsx | 4 +- .../_admin.lg-admin._dashboard.site-data.tsx | 4 +- ...dmin.lg-admin._dashboard.subscriptions.tsx | 4 +- .../_admin.lg-admin._dashboard.users.tsx | 4 +- app/routes/_admin.lg-admin.tsx | 10 +- app/routes/_layout._index.tsx | 4 +- app/routes/_layout.category.$name.tsx | 4 +- app/routes/_layout.detail.$slug.tsx | 4 +- app/routes/actions.admin.login.ts | 72 ++++++++++++ 16 files changed, 207 insertions(+), 140 deletions(-) create mode 100644 app/configs/pages.ts create mode 100644 app/pages/admin-login/index.tsx create mode 100644 app/routes/actions.admin.login.ts diff --git a/app/configs/pages.ts b/app/configs/pages.ts new file mode 100644 index 0000000..42b3ff4 --- /dev/null +++ b/app/configs/pages.ts @@ -0,0 +1,6 @@ +export const AUTH_PAGES = [ + '/lg-admin/login', + '/lg-admin/forgot-password', + '/lg-admin/reset-password', + '/lg-admin/register', +] diff --git a/app/pages/admin-login/index.tsx b/app/pages/admin-login/index.tsx new file mode 100644 index 0000000..2698423 --- /dev/null +++ b/app/pages/admin-login/index.tsx @@ -0,0 +1,107 @@ +import { useState } from 'react' +import { Link } from 'react-router' + +import { EyeIcon } from '~/components/icons/eye' +import { Button } from '~/components/ui/button' +import { APP } from '~/configs/meta' + +export const AdminLoginPage = () => { + const [showPassword, setShowPassword] = useState(false) + + return ( +
+
+
+ + {APP.title} + +
+

+ Selamat Datang, silakan masukkan akun Anda untuk melanjutkan! +

+
+
+ {/* Input Email / No Telepon */} +
+ + +
+ + {/* Input Password */} +
+ + + +
+ + {/* Lupa Kata Sandi */} +
+ Lupa Kata Sandi? + + Reset Kata Sandi + +
+ + {/* Tombol Masuk */} + +
+
+
+ {/* Link Daftar */} +
+ Belum punya akun?{' '} + +
+
+ ) +} diff --git a/app/routes/_admin.lg-admin._auth.login.tsx b/app/routes/_admin.lg-admin._auth.login.tsx index a99db97..0113752 100644 --- a/app/routes/_admin.lg-admin._auth.login.tsx +++ b/app/routes/_admin.lg-admin._auth.login.tsx @@ -1,108 +1,4 @@ -import { useState } from 'react' -import { Link } from 'react-router' +import { AdminLoginPage } from '~/pages/admin-login' -import { EyeIcon } from '~/components/icons/eye' -import { Button } from '~/components/ui/button' -import { APP } from '~/configs/meta' - -const AuthLayout = () => { - const [showPassword, setShowPassword] = useState(false) - - return ( -
-
-
- - {APP.title} - -
-

- Selamat Datang, silakan masukkan akun Anda untuk melanjutkan! -

-
-
- {/* Input Email / No Telepon */} -
- - -
- - {/* Input Password */} -
- - - -
- - {/* Lupa Kata Sandi */} -
- Lupa Kata Sandi? - - Reset Kata Sandi - -
- - {/* Tombol Masuk */} - -
-
-
- {/* Link Daftar */} -
- Belum punya akun?{' '} - -
-
- ) -} +const AuthLayout = () => export default AuthLayout diff --git a/app/routes/_admin.lg-admin._dashboard._index.tsx b/app/routes/_admin.lg-admin._dashboard._index.tsx index 7fc1b2d..23875ef 100644 --- a/app/routes/_admin.lg-admin._dashboard._index.tsx +++ b/app/routes/_admin.lg-admin._dashboard._index.tsx @@ -1,6 +1,4 @@ import { DashboardPage } from '~/pages/dashboard' -const DashboardIndexLayout = () => { - return -} +const DashboardIndexLayout = () => export default DashboardIndexLayout diff --git a/app/routes/_admin.lg-admin._dashboard.admins.tsx b/app/routes/_admin.lg-admin._dashboard.admins.tsx index 9173750..312cc47 100644 --- a/app/routes/_admin.lg-admin._dashboard.admins.tsx +++ b/app/routes/_admin.lg-admin._dashboard.admins.tsx @@ -1,4 +1,2 @@ -const DashboardAdminsLayout = () => { - return
Admins Page
-} +const DashboardAdminsLayout = () =>
Admins Page
export default DashboardAdminsLayout diff --git a/app/routes/_admin.lg-admin._dashboard.advertisements.tsx b/app/routes/_admin.lg-admin._dashboard.advertisements.tsx index d452a18..5bd076d 100644 --- a/app/routes/_admin.lg-admin._dashboard.advertisements.tsx +++ b/app/routes/_admin.lg-admin._dashboard.advertisements.tsx @@ -1,6 +1,4 @@ import { AdvertisementsPage } from '~/pages/dashboard-advertisements' -const DashboardAdvertisementsLayout = () => { - return -} +const DashboardAdvertisementsLayout = () => export default DashboardAdvertisementsLayout diff --git a/app/routes/_admin.lg-admin._dashboard.contents.tsx b/app/routes/_admin.lg-admin._dashboard.contents.tsx index 081af43..f0d4595 100644 --- a/app/routes/_admin.lg-admin._dashboard.contents.tsx +++ b/app/routes/_admin.lg-admin._dashboard.contents.tsx @@ -1,6 +1,4 @@ import { ContentsPage } from '~/pages/dashboard-contents' -const DashboardContentsLayout = () => { - return -} +const DashboardContentsLayout = () => export default DashboardContentsLayout diff --git a/app/routes/_admin.lg-admin._dashboard.settings.tsx b/app/routes/_admin.lg-admin._dashboard.settings.tsx index 52fbb11..4e2632a 100644 --- a/app/routes/_admin.lg-admin._dashboard.settings.tsx +++ b/app/routes/_admin.lg-admin._dashboard.settings.tsx @@ -1,4 +1,2 @@ -const DashboardSettingsLayout = () => { - return
Settings Page
-} +const DashboardSettingsLayout = () =>
Settings Page
export default DashboardSettingsLayout diff --git a/app/routes/_admin.lg-admin._dashboard.site-data.tsx b/app/routes/_admin.lg-admin._dashboard.site-data.tsx index 78a0838..3c0702f 100644 --- a/app/routes/_admin.lg-admin._dashboard.site-data.tsx +++ b/app/routes/_admin.lg-admin._dashboard.site-data.tsx @@ -1,4 +1,2 @@ -const DashboardSiteDataLayout = () => { - return
Site Data Page
-} +const DashboardSiteDataLayout = () =>
Site Data Page
export default DashboardSiteDataLayout diff --git a/app/routes/_admin.lg-admin._dashboard.subscriptions.tsx b/app/routes/_admin.lg-admin._dashboard.subscriptions.tsx index fdff80d..de9e320 100644 --- a/app/routes/_admin.lg-admin._dashboard.subscriptions.tsx +++ b/app/routes/_admin.lg-admin._dashboard.subscriptions.tsx @@ -1,6 +1,4 @@ import { SubscriptionsPage } from '~/pages/dashboard-subscriptions' -const DashboardSubscriptionsLayout = () => { - return -} +const DashboardSubscriptionsLayout = () => export default DashboardSubscriptionsLayout diff --git a/app/routes/_admin.lg-admin._dashboard.users.tsx b/app/routes/_admin.lg-admin._dashboard.users.tsx index e3edc82..8c3838b 100644 --- a/app/routes/_admin.lg-admin._dashboard.users.tsx +++ b/app/routes/_admin.lg-admin._dashboard.users.tsx @@ -1,6 +1,4 @@ import { UsersPage } from '~/pages/dashboard-users' -const DashboardUsersLayout = () => { - return -} +const DashboardUsersLayout = () => export default DashboardUsersLayout diff --git a/app/routes/_admin.lg-admin.tsx b/app/routes/_admin.lg-admin.tsx index 6e75f52..5eb7a6f 100644 --- a/app/routes/_admin.lg-admin.tsx +++ b/app/routes/_admin.lg-admin.tsx @@ -1,6 +1,7 @@ -import { Outlet } from 'react-router' +import { Outlet, redirect } from 'react-router' import { getStaff } from '~/apis/admin/get-staff' +import { AUTH_PAGES } from '~/configs/pages' import { AdminProvider } from '~/contexts/admin' import { AdminDefaultLayout } from '~/layouts/admin/default' import { handleCookie } from '~/libs/cookies' @@ -9,7 +10,14 @@ import type { Route } from './+types/_admin.lg-admin' export const loader = async ({ request }: Route.LoaderArgs) => { const { adminToken } = await handleCookie(request) + const { pathname } = new URL(request.url) + const isAuthPage = AUTH_PAGES.includes(pathname) let adminData + + if (!isAuthPage && !adminToken) { + throw redirect('/lg-admin/login') + } + if (adminToken) { const { data } = await getStaff({ accessToken: adminToken, diff --git a/app/routes/_layout._index.tsx b/app/routes/_layout._index.tsx index 0038b88..ffa7627 100644 --- a/app/routes/_layout._index.tsx +++ b/app/routes/_layout._index.tsx @@ -1,7 +1,5 @@ import { NewsPage } from '~/pages/news' -const NewsIndexLayout = () => { - return -} +const NewsIndexLayout = () => export default NewsIndexLayout diff --git a/app/routes/_layout.category.$name.tsx b/app/routes/_layout.category.$name.tsx index a0fdb3b..ac046f3 100644 --- a/app/routes/_layout.category.$name.tsx +++ b/app/routes/_layout.category.$name.tsx @@ -1,7 +1,5 @@ import { NewsCategoriesPage } from '~/pages/news-categories' -const NewsCategoriesLayout = () => { - return -} +const NewsCategoriesLayout = () => export default NewsCategoriesLayout diff --git a/app/routes/_layout.detail.$slug.tsx b/app/routes/_layout.detail.$slug.tsx index 93d567b..8bbc788 100644 --- a/app/routes/_layout.detail.$slug.tsx +++ b/app/routes/_layout.detail.$slug.tsx @@ -1,7 +1,5 @@ import { NewsDetailPage } from '~/pages/news-detail' -const NewsDetailLayout = () => { - return -} +const NewsDetailLayout = () => export default NewsDetailLayout diff --git a/app/routes/actions.admin.login.ts b/app/routes/actions.admin.login.ts new file mode 100644 index 0000000..6923511 --- /dev/null +++ b/app/routes/actions.admin.login.ts @@ -0,0 +1,72 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { data } from 'react-router' +import { getValidatedFormData } from 'remix-hook-form' +import { XiorError } from 'xior' + +import { getUser } from '~/apis/news/get-user' +import { userLoginRequest } from '~/apis/news/login-user' +import { loginSchema, type TLoginSchema } from '~/layouts/news/form-login' +import { generateTokenCookie } from '~/utils/token' + +import type { Route } from './+types/actions.login' + +export const action = async ({ request }: Route.ActionArgs) => { + try { + const { + errors, + data: payload, + receivedValues: defaultValues, + } = await getValidatedFormData( + request, + zodResolver(loginSchema), + false, + ) + + if (errors) { + return data({ success: false, errors, defaultValues }, { status: 400 }) + } + + const { data: loginData } = await userLoginRequest(payload) + const { token } = loginData + const { data: userData } = await getUser({ + accessToken: token, + }) + const tokenCookie = generateTokenCookie({ + token, + }) + + const headers = new Headers() + headers.append('Set-Cookie', await tokenCookie) + + return data( + { + success: true, + user: userData, + }, + { + headers, + status: 200, + statusText: 'OK', + }, + ) + } catch (error) { + if (error instanceof XiorError) { + return data( + { + success: false, + message: error?.response?.data?.error?.message || error.message, + }, + { + status: error?.response?.status || 500, + }, + ) + } + return data( + { + success: false, + message: 'Internal server error', + }, + { status: 500 }, + ) + } +} From 459e25c010d82a618983a3e4df5354fb19a875ad Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 3 Mar 2025 16:47:59 +0800 Subject: [PATCH 3/6] feat: refactor admin authentication to staff authentication with updated cookie handling --- app/apis/admin/login-staff.ts | 2 +- app/configs/cookies.ts | 4 +- app/libs/cookie.server.ts | 4 +- app/libs/cookies.ts | 6 +- app/libs/logout-header.server.ts | 6 +- app/pages/admin-login/index.tsx | 135 ++++++++++++++++-------------- app/routes/_admin.lg-admin.tsx | 8 +- app/routes/actions.admin.login.ts | 16 ++-- app/routes/actions.login.ts | 4 +- app/routes/actions.register.ts | 4 +- app/utils/token.ts | 21 ++++- 11 files changed, 120 insertions(+), 90 deletions(-) diff --git a/app/apis/admin/login-staff.ts b/app/apis/admin/login-staff.ts index dbdcaf4..64a8381 100644 --- a/app/apis/admin/login-staff.ts +++ b/app/apis/admin/login-staff.ts @@ -1,7 +1,7 @@ import { z } from 'zod' -import { type TLoginSchema } from '~/layouts/news/form-login' import { HttpServer } from '~/libs/http-server' +import type { TLoginSchema } from '~/pages/admin-login' const loginResponseSchema = z.object({ data: z.object({ diff --git a/app/configs/cookies.ts b/app/configs/cookies.ts index 7e832ae..9b78dff 100644 --- a/app/configs/cookies.ts +++ b/app/configs/cookies.ts @@ -2,6 +2,6 @@ export const USER_COOKIES = { token: '__lg-usr-tkn', } -export const ADMIN_COOKIES = { - token: '__lg-adm-tkn', +export const STAFF_COOKIES = { + token: '__lg-stf-tkn', } diff --git a/app/libs/cookie.server.ts b/app/libs/cookie.server.ts index a6c5ce4..557d52e 100644 --- a/app/libs/cookie.server.ts +++ b/app/libs/cookie.server.ts @@ -1,6 +1,6 @@ import { createCookie } from 'react-router' -import { ADMIN_COOKIES, USER_COOKIES } from '~/configs/cookies' +import { STAFF_COOKIES, USER_COOKIES } from '~/configs/cookies' export const userTokenCookieConfig = createCookie(USER_COOKIES.token, { httpOnly: false, @@ -10,7 +10,7 @@ export const userTokenCookieConfig = createCookie(USER_COOKIES.token, { path: '/', }) -export const adminTokenCookieConfig = createCookie(ADMIN_COOKIES.token, { +export const staffTokenCookieConfig = createCookie(STAFF_COOKIES.token, { httpOnly: false, sameSite: 'lax', secure: process.env.NODE_ENV === 'production', diff --git a/app/libs/cookies.ts b/app/libs/cookies.ts index 2a84148..b14ed8e 100644 --- a/app/libs/cookies.ts +++ b/app/libs/cookies.ts @@ -1,16 +1,16 @@ -import { adminTokenCookieConfig, userTokenCookieConfig } from './cookie.server' +import { staffTokenCookieConfig, userTokenCookieConfig } from './cookie.server' export const handleCookie = async (request: Request) => { const headers = request.headers const userToken = (await userTokenCookieConfig.parse( headers.get('Cookie'), )) as string - const adminToken = (await adminTokenCookieConfig.parse( + const staffToken = (await staffTokenCookieConfig.parse( headers.get('Cookie'), )) as string return { userToken, - adminToken, + staffToken, } } diff --git a/app/libs/logout-header.server.ts b/app/libs/logout-header.server.ts index dd8964f..39563de 100644 --- a/app/libs/logout-header.server.ts +++ b/app/libs/logout-header.server.ts @@ -1,4 +1,4 @@ -import { ADMIN_COOKIES, USER_COOKIES } from '~/configs/cookies' +import { STAFF_COOKIES, USER_COOKIES } from '~/configs/cookies' export const setUserLogoutHeaders = () => { const responseHeaders = new Headers() @@ -10,11 +10,11 @@ export const setUserLogoutHeaders = () => { return responseHeaders } -export const setAdminLogoutHeaders = () => { +export const setStaffLogoutHeaders = () => { const responseHeaders = new Headers() responseHeaders.append( 'Set-Cookie', - `${ADMIN_COOKIES.token}=; Path=/lg-admin; HttpOnly; SameSite=Strict; Max-Age=0`, + `${STAFF_COOKIES.token}=; Path=/lg-admin; HttpOnly; SameSite=Strict; Max-Age=0`, ) return responseHeaders diff --git a/app/pages/admin-login/index.tsx b/app/pages/admin-login/index.tsx index 2698423..7389545 100644 --- a/app/pages/admin-login/index.tsx +++ b/app/pages/admin-login/index.tsx @@ -1,12 +1,43 @@ -import { useState } from 'react' -import { Link } from 'react-router' +import { zodResolver } from '@hookform/resolvers/zod' +import { useEffect, useState } from 'react' +import { Link, useFetcher } from 'react-router' +import { RemixFormProvider, useRemixForm } from 'remix-hook-form' +import { z } from 'zod' -import { EyeIcon } from '~/components/icons/eye' import { Button } from '~/components/ui/button' +import { Input } from '~/components/ui/input' import { APP } from '~/configs/meta' +export const loginSchema = z.object({ + email: z.string().email('Email tidak valid'), + password: z.string().min(6, 'Kata sandi minimal 6 karakter'), +}) + +export type TLoginSchema = z.infer + export const AdminLoginPage = () => { - const [showPassword, setShowPassword] = useState(false) + const fetcher = useFetcher() + const formMethods = useRemixForm({ + mode: 'onSubmit', + fetcher, + resolver: zodResolver(loginSchema), + }) + const [error, setError] = useState() + const [disabled, setDisabled] = useState(false) + + const { handleSubmit } = formMethods + + useEffect(() => { + if (!fetcher.data?.success) { + setError(fetcher.data?.message) + setDisabled(false) + return + } + + setDisabled(true) + setError(undefined) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetcher]) return (
@@ -24,70 +55,52 @@ export const AdminLoginPage = () => { Selamat Datang, silakan masukkan akun Anda untuk melanjutkan!

-
- {/* Input Email / No Telepon */} -
- - + + -
- {/* Input Password */} -
- - - -
- {/* Lupa Kata Sandi */} -
- Lupa Kata Sandi? - - Reset Kata Sandi - -
+ {error && ( +
{error}
+ )} - {/* Tombol Masuk */} - -
+ {/* Lupa Kata Sandi */} +
+ Lupa Kata Sandi? + + Reset Kata Sandi + +
+ + + +
{/* Link Daftar */} diff --git a/app/routes/_admin.lg-admin.tsx b/app/routes/_admin.lg-admin.tsx index 5eb7a6f..da8f184 100644 --- a/app/routes/_admin.lg-admin.tsx +++ b/app/routes/_admin.lg-admin.tsx @@ -9,18 +9,18 @@ import { handleCookie } from '~/libs/cookies' import type { Route } from './+types/_admin.lg-admin' export const loader = async ({ request }: Route.LoaderArgs) => { - const { adminToken } = await handleCookie(request) + const { staffToken } = await handleCookie(request) const { pathname } = new URL(request.url) const isAuthPage = AUTH_PAGES.includes(pathname) let adminData - if (!isAuthPage && !adminToken) { + if (!isAuthPage && !staffToken) { throw redirect('/lg-admin/login') } - if (adminToken) { + if (staffToken) { const { data } = await getStaff({ - accessToken: adminToken, + accessToken: staffToken, }) adminData = data } diff --git a/app/routes/actions.admin.login.ts b/app/routes/actions.admin.login.ts index 6923511..778cb7a 100644 --- a/app/routes/actions.admin.login.ts +++ b/app/routes/actions.admin.login.ts @@ -3,10 +3,10 @@ import { data } from 'react-router' import { getValidatedFormData } from 'remix-hook-form' import { XiorError } from 'xior' -import { getUser } from '~/apis/news/get-user' -import { userLoginRequest } from '~/apis/news/login-user' -import { loginSchema, type TLoginSchema } from '~/layouts/news/form-login' -import { generateTokenCookie } from '~/utils/token' +import { getStaff } from '~/apis/admin/get-staff' +import { staffLoginRequest } from '~/apis/admin/login-staff' +import { loginSchema, type TLoginSchema } from '~/pages/admin-login' +import { generateStaffTokenCookie } from '~/utils/token' import type { Route } from './+types/actions.login' @@ -26,12 +26,12 @@ export const action = async ({ request }: Route.ActionArgs) => { return data({ success: false, errors, defaultValues }, { status: 400 }) } - const { data: loginData } = await userLoginRequest(payload) + const { data: loginData } = await staffLoginRequest(payload) const { token } = loginData - const { data: userData } = await getUser({ + const { data: staffData } = await getStaff({ accessToken: token, }) - const tokenCookie = generateTokenCookie({ + const tokenCookie = generateStaffTokenCookie({ token, }) @@ -41,7 +41,7 @@ export const action = async ({ request }: Route.ActionArgs) => { return data( { success: true, - user: userData, + staff: staffData, }, { headers, diff --git a/app/routes/actions.login.ts b/app/routes/actions.login.ts index 6923511..a04d136 100644 --- a/app/routes/actions.login.ts +++ b/app/routes/actions.login.ts @@ -6,7 +6,7 @@ import { XiorError } from 'xior' import { getUser } from '~/apis/news/get-user' import { userLoginRequest } from '~/apis/news/login-user' import { loginSchema, type TLoginSchema } from '~/layouts/news/form-login' -import { generateTokenCookie } from '~/utils/token' +import { generateUserTokenCookie } from '~/utils/token' import type { Route } from './+types/actions.login' @@ -31,7 +31,7 @@ export const action = async ({ request }: Route.ActionArgs) => { const { data: userData } = await getUser({ accessToken: token, }) - const tokenCookie = generateTokenCookie({ + const tokenCookie = generateUserTokenCookie({ token, }) diff --git a/app/routes/actions.register.ts b/app/routes/actions.register.ts index fac144b..f4ba8a2 100644 --- a/app/routes/actions.register.ts +++ b/app/routes/actions.register.ts @@ -9,7 +9,7 @@ import { registerSchema, type TRegisterSchema, } from '~/layouts/news/form-register' -import { generateTokenCookie } from '~/utils/token' +import { generateUserTokenCookie } from '~/utils/token' import type { Route } from './+types/actions.register' @@ -34,7 +34,7 @@ export const action = async ({ request }: Route.ActionArgs) => { const { data: userData } = await getUser({ accessToken: token, }) - const tokenCookie = generateTokenCookie({ + const tokenCookie = generateUserTokenCookie({ token, }) diff --git a/app/utils/token.ts b/app/utils/token.ts index 9bd4ebd..1069738 100644 --- a/app/utils/token.ts +++ b/app/utils/token.ts @@ -1,12 +1,15 @@ import { decodeJwt } from 'jose' -import { userTokenCookieConfig } from '~/libs/cookie.server' +import { + staffTokenCookieConfig, + userTokenCookieConfig, +} from '~/libs/cookie.server' type TTokenCookie = { token: string } -export const generateTokenCookie = (parameters: TTokenCookie) => { +export const generateUserTokenCookie = (parameters: TTokenCookie) => { const { token } = parameters const decodedToken = decodeJwt(token) @@ -19,3 +22,17 @@ export const generateTokenCookie = (parameters: TTokenCookie) => { expires: expirationDate, }) } + +export const generateStaffTokenCookie = (parameters: TTokenCookie) => { + const { token } = parameters + + const decodedToken = decodeJwt(token) + const decodedTokenExp = decodedToken.exp + const expirationDate = decodedTokenExp + ? new Date(decodedTokenExp * 1000) + : undefined + + return staffTokenCookieConfig.serialize(token, { + expires: expirationDate, + }) +} From 599a92fba380822c8094f2abf5ca2b85ed46022f Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 3 Mar 2025 17:33:52 +0800 Subject: [PATCH 4/6] feat: update staff API schema and refactor admin login paths --- app/apis/admin/get-staff.ts | 3 ++- app/configs/meta.ts | 6 +----- app/pages/admin-login/index.tsx | 3 --- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/apis/admin/get-staff.ts b/app/apis/admin/get-staff.ts index ff34ffd..fc414b7 100644 --- a/app/apis/admin/get-staff.ts +++ b/app/apis/admin/get-staff.ts @@ -6,7 +6,8 @@ const staffSchema = z.object({ data: z.object({ id: z.string(), email: z.string(), - username: z.string(), + name: z.string(), + profile_picture: z.string(), }), }) diff --git a/app/configs/meta.ts b/app/configs/meta.ts index 30585a8..1bca2b8 100644 --- a/app/configs/meta.ts +++ b/app/configs/meta.ts @@ -18,13 +18,9 @@ export const META_TITLE_CONFIG: TMetaTitleConfig = [ title: 'Home', }, { - path: '/lg-admin/auth/login', + path: '/lg-admin/login', title: 'Login', }, - { - path: '/lg-admin/auth/register', - title: 'Register', - }, ...ADMIN_MENU.flatMap((menu) => menu.items.map((item) => ({ path: item.url, title: item.title })), ), diff --git a/app/pages/admin-login/index.tsx b/app/pages/admin-login/index.tsx index 7389545..6ab389d 100644 --- a/app/pages/admin-login/index.tsx +++ b/app/pages/admin-login/index.tsx @@ -33,9 +33,6 @@ export const AdminLoginPage = () => { setDisabled(false) return } - - setDisabled(true) - setError(undefined) // eslint-disable-next-line react-hooks/exhaustive-deps }, [fetcher]) From 5007b8d4db1a5da62718fe0eb686a7ef32edc1f6 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 3 Mar 2025 17:48:24 +0800 Subject: [PATCH 5/6] feat: update cookie paths for staff authentication and implement redirect logic in admin login --- app/libs/cookie.server.ts | 2 +- app/libs/logout-header.server.ts | 2 +- app/routes/_admin.lg-admin._auth.login.tsx | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/libs/cookie.server.ts b/app/libs/cookie.server.ts index 557d52e..0870e2c 100644 --- a/app/libs/cookie.server.ts +++ b/app/libs/cookie.server.ts @@ -15,5 +15,5 @@ export const staffTokenCookieConfig = createCookie(STAFF_COOKIES.token, { sameSite: 'lax', secure: process.env.NODE_ENV === 'production', secrets: [process.env.VITE_SALT_KEY || 'default-secret'], - path: '/lg-admin', + path: '/', }) diff --git a/app/libs/logout-header.server.ts b/app/libs/logout-header.server.ts index 39563de..db19bd4 100644 --- a/app/libs/logout-header.server.ts +++ b/app/libs/logout-header.server.ts @@ -14,7 +14,7 @@ export const setStaffLogoutHeaders = () => { const responseHeaders = new Headers() responseHeaders.append( 'Set-Cookie', - `${STAFF_COOKIES.token}=; Path=/lg-admin; HttpOnly; SameSite=Strict; Max-Age=0`, + `${STAFF_COOKIES.token}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`, ) return responseHeaders diff --git a/app/routes/_admin.lg-admin._auth.login.tsx b/app/routes/_admin.lg-admin._auth.login.tsx index 0113752..0ee9f17 100644 --- a/app/routes/_admin.lg-admin._auth.login.tsx +++ b/app/routes/_admin.lg-admin._auth.login.tsx @@ -1,4 +1,17 @@ +import { redirect } from 'react-router' + +import { handleCookie } from '~/libs/cookies' import { AdminLoginPage } from '~/pages/admin-login' +import type { Route } from './+types/_admin.lg-admin._auth.login' + +export const loader = async ({ request }: Route.LoaderArgs) => { + const { staffToken } = await handleCookie(request) + + if (staffToken) { + throw redirect('/lg-admin') + } +} + const AuthLayout = () => export default AuthLayout From 1bebe61634c073b5a50f68bc87c698e6d97bef91 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Mon, 3 Mar 2025 17:50:10 +0800 Subject: [PATCH 6/6] feat: enhance admin login flow with conditional redirects based on authentication status --- app/routes/_admin.lg-admin._auth.login.tsx | 13 ------------- app/routes/_admin.lg-admin.tsx | 4 ++++ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/app/routes/_admin.lg-admin._auth.login.tsx b/app/routes/_admin.lg-admin._auth.login.tsx index 0ee9f17..0113752 100644 --- a/app/routes/_admin.lg-admin._auth.login.tsx +++ b/app/routes/_admin.lg-admin._auth.login.tsx @@ -1,17 +1,4 @@ -import { redirect } from 'react-router' - -import { handleCookie } from '~/libs/cookies' import { AdminLoginPage } from '~/pages/admin-login' -import type { Route } from './+types/_admin.lg-admin._auth.login' - -export const loader = async ({ request }: Route.LoaderArgs) => { - const { staffToken } = await handleCookie(request) - - if (staffToken) { - throw redirect('/lg-admin') - } -} - const AuthLayout = () => export default AuthLayout diff --git a/app/routes/_admin.lg-admin.tsx b/app/routes/_admin.lg-admin.tsx index da8f184..f7e87de 100644 --- a/app/routes/_admin.lg-admin.tsx +++ b/app/routes/_admin.lg-admin.tsx @@ -18,6 +18,10 @@ export const loader = async ({ request }: Route.LoaderArgs) => { throw redirect('/lg-admin/login') } + if (isAuthPage && staffToken) { + throw redirect('/lg-admin') + } + if (staffToken) { const { data } = await getStaff({ accessToken: staffToken,