From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server-vie001.gnuweeb.org X-Spam-Level: X-Spam-Status: No, score=-1.2 required=5.0 tests=ALL_TRUSTED,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,URIBL_DBL_BLOCKED_OPENDNS, URIBL_ZEN_BLOCKED_OPENDNS autolearn=ham autolearn_force=no version=3.4.6 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gnuweeb.org; s=default; t=1740264889; bh=RPc1bAv97MRsfaTKlqIrMLPcqwQ+ieJGUnGRhFDdwQY=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Transfer-Encoding:Message-ID:Date:From: Reply-To:Subject:To:Cc:In-Reply-To:References:Resent-Date: Resent-From:Resent-To:Resent-Cc:User-Agent:Content-Type: Content-Transfer-Encoding; b=egah+HZcpIQqC+ONS694VNOgMWpsvxQPRton7zEk2wgA0QxUhQEyXjFCMnPDKGKx+ 2/9i9BB8OAOb4iv3sAUaHBeQchrt2IiVeLHZ08NA3VXU6+h72/Yp8M8AIO2ux8vMFC f0tX0I9+JCd4lxzKor3kbBPDMGc0ay/j488L3g6R9yuW0iLB4/YzGbBKKjMaOfQY3u BnLfc28JrjEZDvhsAJ5hPBi5BjYgmZSMLFsJnJfALVP2Gh9iTYBIe2TqCIMAapm5xd ppKmUfQFAU8Ftkzm22U4o80Q9A5CFUFDsrf/SpVKOuNwIzq7bbKa5RQ6nln5eT5uYq PZAc3jj/XUA1Q== Received: from localhost.localdomain (unknown [101.128.125.35]) by server-vie001.gnuweeb.org (Postfix) with ESMTPSA id 22EA520744CB; Sat, 22 Feb 2025 22:54:47 +0000 (UTC) From: Muhammad Rizki To: Ammar Faizi Cc: Muhammad Rizki , Alviro Iskandar Setiawan , GNU/Weeb Mailing List Subject: [PATCH v1 05/13] refactor: update HTTP client, typings, and login method Date: Sun, 23 Feb 2025 05:54:11 +0700 Message-ID: <20250222225423.1377-6-kiizuha@gnuweeb.org> X-Mailer: git-send-email 2.45.2.windows.1 In-Reply-To: <20250222225423.1377-1-kiizuha@gnuweeb.org> References: <20250222225423.1377-1-kiizuha@gnuweeb.org> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit List-Id: Refactored the HTTP client, typings, and login query method. The new method is much simpler than previous one. Additionally, the refactored HTTP client now includes refresh token behavior under the hood. Added an invalid credential warning message to the login form. When a user accesses the page with an expired token, they will be redirected to the login page and shown a message indicating that the token has expired and they need to re-login. Signed-off-by: Muhammad Rizki --- src/lib/hooks/http.svelte.ts | 170 ++++++++++-------------------- src/lib/typings/http.d.ts | 22 ++-- src/lib/typings/index.ts | 5 +- src/routes/(protected)/+layout.ts | 14 ++- src/routes/+page.svelte | 48 +++++---- src/routes/+page.ts | 4 +- 6 files changed, 103 insertions(+), 160 deletions(-) diff --git a/src/lib/hooks/http.svelte.ts b/src/lib/hooks/http.svelte.ts index fd4114d..78273a1 100644 --- a/src/lib/hooks/http.svelte.ts +++ b/src/lib/hooks/http.svelte.ts @@ -1,129 +1,65 @@ -import axios, { AxiosError } from "axios"; -import { useAuth } from "./auth.svelte"; -import type { RecordString, UseHttpProps } from "$typings"; -import type { ResponseAPI } from "$typings/http"; - -const transformToFormData = (initialData: RecordString, requestData: RecordString | FormData) => { - const formData = new FormData(); - - let data = requestData; - - Object.keys(initialData).forEach((key) => { - const value = initialData[key]; +import * as typing from "$typings"; +import axios from "axios"; +import type { + AxiosError, + AxiosRequestConfig, + AxiosResponse, + InternalAxiosRequestConfig +} from "axios"; + +const client = axios.create({ + baseURL: "https://mail.gnuweeb.org/api.php" +}); + +const http = async (prop: AxiosRequestConfig) => { + const onSuccess = (response: AxiosResponse>) => { + return response; + }; - if (Array.isArray(value)) { - for (const val of value) { - formData.append( - key, - typeof val === "object" && !(val instanceof File) ? JSON.stringify(val) : val - ); - } - } else { - formData.append(key, value); - } - }); + const onError = (error: AxiosError) => { + return Promise.reject(error); + }; - data = formData; - return data; + return client(prop).then(onSuccess).catch(onError); }; -const GWM_API_URL = "https://mail.gnuweeb.org/api.php?action="; +client.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const accessToken = localStorage.getItem("gwm_token"); -export const useHttp = ({ - action, - payload, - params = {}, - method = "GET", - timeout, - beforeExecute = () => true, - onComplete = () => true, - formatter = () => true, - onError = () => false, - onFinally = () => false, - isFormData = false, - responseType -}: UseHttpProps) => { - const url = GWM_API_URL + action; - let isLoading = $state(true); - let data = $state(null); + config.params = { ...config.params, renew_token: 1 }; - formatter(data, null); - - let errors = $state(null); - let errorMessage = $state(null); - const auth = useAuth(); - - const execute = async (replace?: { - payload?: RecordString | null; - params?: RecordString | null; - }) => { - if (!beforeExecute()) { - onFinally(); - return; + if (accessToken && !config.headers.Authorization) { + config.headers.Authorization = `Bearer ${accessToken}`; } - isLoading = true; - - if (replace?.payload?.defaultPrevented != null) { - replace!.payload = null; + return config; + }, + (error: AxiosError) => { + return Promise.reject(error); + } +); + +client.interceptors.response.use( + (res: AxiosResponse) => { + if (res?.data.res?.renew_token) { + localStorage.setItem("gwm_token", res.data.res.renew_token.token); + localStorage.setItem("gwm_token_exp_at", res.data.res.renew_token.token_exp_at); } - - const initialData: RecordString = replace?.payload ?? payload ?? {}; - let requestData: RecordString | FormData = initialData; - - if (isFormData) { - requestData = transformToFormData(initialData, requestData); + return res; + }, + async (err: AxiosError) => { + const response = err.response as AxiosResponse>; + const status = response ? response.status : null; + + if (status === 403 && response?.data) { + localStorage.removeItem("gwm_token"); + localStorage.removeItem("gwm_token_exp_at"); + localStorage.removeItem("gwm_uinfo"); } - return await axios({ - url: url, - method, - data: requestData, - headers: { - Authorization: `Bearer ${auth.token}`, - "Content-Type": "application/json" - }, - params: replace?.params ?? params, - responseType: responseType, - timeout: timeout ?? 60000 - }) - .then((res) => { - isLoading = false; - data = res?.data; - - onComplete(res); - onFinally(); - formatter(data, res); + return response; + } +); - errors = null; - errorMessage = ""; - - return res; - }) - - .catch((res: AxiosError>) => { - isLoading = false; - errors = res.stack!; - - const response = res.response?.data; - - errorMessage = typeof response?.res === "string" ? response.res : "Something went wrong!"; - - onError(res.response!.data, errorMessage); - onFinally(); - - return res; - }); - }; - - const refetch = execute; - - return { - data, - isLoading, - refetch, - execute, - errorMessage, - errors - }; -}; +export default http; diff --git a/src/lib/typings/http.d.ts b/src/lib/typings/http.d.ts index decf147..98d64f5 100644 --- a/src/lib/typings/http.d.ts +++ b/src/lib/typings/http.d.ts @@ -1,24 +1,14 @@ -import type { AxiosRequestConfig, AxiosResponse } from "axios"; -import type { RecordString } from "./common"; import type { User } from "./credential"; -interface ResponseInterface { +export interface ResponseAPI { code: number; - res?: Data; + res?: Data & { renew_token?: RenewTokenResponse }; } -type ResponseAPI = ResponseInterface; - -export type UseHttpProps = AxiosRequestConfig & { - action: string; - payload?: RecordString; - isFormData?: boolean; - beforeExecute?: () => boolean; - onComplete?: (resp: AxiosResponse>) => void; - formatter?: (data: Formatter | null, resp: AxiosResponse> | null) => void; - onError?: (data: ResponseAPI, msg?: string | null) => void; - onFinally?: () => void; -}; +export interface RenewTokenResponse { + token: strign; + token_exp_at: number; +} export interface LoginResponse { token: string; diff --git a/src/lib/typings/index.ts b/src/lib/typings/index.ts index 6ae23a1..1855c2b 100644 --- a/src/lib/typings/index.ts +++ b/src/lib/typings/index.ts @@ -1,5 +1,5 @@ import type { RecordString, Navigations, LabelAndValue, MailConfig } from "./common"; -import type { UseHttpProps, LoginResponse } from "./http"; +import type { ResponseAPI, LoginResponse, RenewTokenResponse } from "./http"; import type { User } from "./credential"; export type { @@ -7,7 +7,8 @@ export type { Navigations, LabelAndValue, MailConfig, - UseHttpProps, + ResponseAPI, + RenewTokenResponse, LoginResponse, User }; diff --git a/src/routes/(protected)/+layout.ts b/src/routes/(protected)/+layout.ts index dd687a1..2d1813a 100644 --- a/src/routes/(protected)/+layout.ts +++ b/src/routes/(protected)/+layout.ts @@ -1,11 +1,21 @@ import { useAuth } from "$lib/hooks/auth.svelte"; import { redirect } from "@sveltejs/kit"; -import type { LayoutLoad } from "./$types"; +import type { LayoutLoad } from "../$types"; +import http from "$lib/hooks/http.svelte"; +import * as typing from "$typings"; export const load: LayoutLoad = async () => { const auth = useAuth(); - if (!auth.isValid()) return redirect(307, "/"); + if (!auth.isValid()) { + localStorage.setItem("gwm_invalid_creds", String(1)); + return redirect(307, "/"); + } + const { data } = await http({ + params: { action: "get_user_info" } + }); + + localStorage.setItem("gwm_uinfo", JSON.stringify(data.res)); auth.refresh(); }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 46ef170..8ae2203 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -6,9 +6,10 @@ import { Input } from "$components/ui/input"; import InputPassword from "$components/ui/input/input-password.svelte"; import { useAuth } from "$lib/hooks/auth.svelte"; - import { useHttp } from "$lib/hooks/http.svelte"; + import http from "$lib/hooks/http.svelte"; import { loginSchema } from "$lib/schemas/login"; - import type { LoginResponse, User } from "$typings"; + import * as typing from "$typings"; + import { onMount } from "svelte"; import { superForm, setError, setMessage } from "sveltekit-superforms"; import { zod } from "sveltekit-superforms/adapters"; @@ -21,38 +22,35 @@ validators: zod(loginSchema), async onUpdate({ form }) { - const http = useHttp({ - action: "login", + const res = await http({ + params: { action: "login" }, method: "POST", - - payload: { + data: { user: form.data.username_or_email, pass: form.data.password - }, - - formatter(data, resp) { - data = resp?.data.res?.user_info!; - }, - - onComplete(resp) { - auth.save(resp.data.res!); - }, - - onError(_, errorMessage) { - setError(form, "username_or_email", ""); - setError(form, "password", ""); - setMessage(form, errorMessage); } }); - await http.execute(); + if (res.status === 200) { + auth.save(res.data.res!); + } else { + setError(form, "username_or_email", ""); + setError(form, "password", ""); + setMessage(form, res.data.res); + } } }); const isError = () => Boolean($errors.username_or_email && $errors.password); const isValid = () => Boolean($formData.username_or_email && $formData.password); + const isCredentialInvalid = () => Boolean(data.isInvalidCreds && +data.isInvalidCreds); const { form: formData, errors, message, submitting, constraints, enhance } = form; + + onMount(() => { + if (!isCredentialInvalid()) return; + localStorage.removeItem("gwm_invalid_creds"); + });
@@ -62,11 +60,17 @@ GNU/Weeb Mail Login Proceed login to manager your email account - {#if isError()} + {#if isError() && !isCredentialInvalid()} {$message} {/if} + + {#if !isError() && isCredentialInvalid()} + + Invalid credential, please login again. + + {/if} diff --git a/src/routes/+page.ts b/src/routes/+page.ts index c3ac27a..a19ae0e 100644 --- a/src/routes/+page.ts +++ b/src/routes/+page.ts @@ -7,8 +7,10 @@ import { redirect } from "@sveltejs/kit"; export const load: PageLoad = async () => { const auth = useAuth(); + const isInvalidCreds = localStorage.getItem("gwm_invalid_creds"); + if (auth.isValid()) return redirect(307, "/home"); const form = await superValidate(zod(loginSchema)); - return { form }; + return { form, isInvalidCreds }; }; -- Muhammad Rizki