diff --git a/app/apis/admin/get-staff.ts b/app/apis/admin/get-staff.ts new file mode 100644 index 0000000..fc414b7 --- /dev/null +++ b/app/apis/admin/get-staff.ts @@ -0,0 +1,22 @@ +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(), + name: z.string(), + profile_picture: 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..64a8381 --- /dev/null +++ b/app/apis/admin/login-staff.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' + +import { HttpServer } from '~/libs/http-server' +import type { TLoginSchema } from '~/pages/admin-login' + +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/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/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/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/libs/cookie.server.ts b/app/libs/cookie.server.ts index a6c5ce4..0870e2c 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,10 +10,10 @@ 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', secrets: [process.env.VITE_SALT_KEY || 'default-secret'], - path: '/lg-admin', + path: '/', }) 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..db19bd4 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=/; HttpOnly; SameSite=Strict; Max-Age=0`, ) return responseHeaders diff --git a/app/pages/admin-login/index.tsx b/app/pages/admin-login/index.tsx new file mode 100644 index 0000000..6ab389d --- /dev/null +++ b/app/pages/admin-login/index.tsx @@ -0,0 +1,117 @@ +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 { 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 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 + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetcher]) + + return ( +
+
+
+ + {APP.title} + +
+

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

+
+ + + + + + + {error && ( +
{error}
+ )} + + {/* Lupa Kata Sandi */} +
+ Lupa Kata Sandi? + + Reset Kata Sandi + +
+ + +
+
+
+
+ {/* 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 565ed09..f7e87de 100644 --- a/app/routes/_admin.lg-admin.tsx +++ b/app/routes/_admin.lg-admin.tsx @@ -1,7 +1,38 @@ -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' + +import type { Route } from './+types/_admin.lg-admin' + +export const loader = async ({ request }: Route.LoaderArgs) => { + const { staffToken } = await handleCookie(request) + const { pathname } = new URL(request.url) + const isAuthPage = AUTH_PAGES.includes(pathname) + let adminData + + if (!isAuthPage && !staffToken) { + throw redirect('/lg-admin/login') + } + + if (isAuthPage && staffToken) { + throw redirect('/lg-admin') + } + + if (staffToken) { + const { data } = await getStaff({ + accessToken: staffToken, + }) + adminData = data + } + + return { + adminData, + } +} const AdminLayout = () => { return ( 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..778cb7a --- /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 { 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' + +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 staffLoginRequest(payload) + const { token } = loginData + const { data: staffData } = await getStaff({ + accessToken: token, + }) + const tokenCookie = generateStaffTokenCookie({ + token, + }) + + const headers = new Headers() + headers.append('Set-Cookie', await tokenCookie) + + return data( + { + success: true, + staff: staffData, + }, + { + 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 }, + ) + } +} diff --git a/app/routes/actions.login.ts b/app/routes/actions.login.ts index 28118c2..a04d136 100644 --- a/app/routes/actions.login.ts +++ b/app/routes/actions.login.ts @@ -4,9 +4,9 @@ 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' +import { generateUserTokenCookie } 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 newsLoginRequest(payload) + const { data: loginData } = await userLoginRequest(payload) const { token } = loginData 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 ae30723..f4ba8a2 100644 --- a/app/routes/actions.register.ts +++ b/app/routes/actions.register.ts @@ -4,12 +4,12 @@ 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, } from '~/layouts/news/form-register' -import { generateTokenCookie } from '~/utils/token' +import { generateUserTokenCookie } from '~/utils/token' import type { Route } from './+types/actions.register' @@ -29,12 +29,12 @@ 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, }) - 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, + }) +}