From: Muhammad Rizki <[email protected]>
To: Ammar Faizi <[email protected]>
Cc: Muhammad Rizki <[email protected]>,
Alviro Iskandar Setiawan <[email protected]>,
GNU/Weeb Mailing List <[email protected]>
Subject: [PATCH v1 05/13] refactor: update HTTP client, typings, and login method
Date: Sun, 23 Feb 2025 05:54:11 +0700 [thread overview]
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
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 <[email protected]>
---
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 <T>(prop: AxiosRequestConfig) => {
+ const onSuccess = (response: AxiosResponse<typing.ResponseAPI<T>>) => {
+ 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 = <Response = unknown, Formatter = unknown>({
- action,
- payload,
- params = {},
- method = "GET",
- timeout,
- beforeExecute = () => true,
- onComplete = () => true,
- formatter = () => true,
- onError = () => false,
- onFinally = () => false,
- isFormData = false,
- responseType
-}: UseHttpProps<Response, Formatter>) => {
- const url = GWM_API_URL + action;
- let isLoading = $state<boolean>(true);
- let data = $state<Formatter | null>(null);
+ config.params = { ...config.params, renew_token: 1 };
- formatter(data, null);
-
- let errors = $state<string | null>(null);
- let errorMessage = $state<string | null>(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<typing.ResponseAPI<typing.RenewTokenResponse>>;
+ 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<ResponseAPI<Response>>) => {
- 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<Data> {
+export interface ResponseAPI<Data> {
code: number;
- res?: Data;
+ res?: Data & { renew_token?: RenewTokenResponse };
}
-type ResponseAPI<ArrayType = null> = ResponseInterface<ArrayType>;
-
-export type UseHttpProps<Response, Formatter> = AxiosRequestConfig & {
- action: string;
- payload?: RecordString;
- isFormData?: boolean;
- beforeExecute?: () => boolean;
- onComplete?: (resp: AxiosResponse<ResponseAPI<Response>>) => void;
- formatter?: (data: Formatter | null, resp: AxiosResponse<ResponseAPI<Response>> | null) => void;
- onError?: (data: ResponseAPI<Response>, 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<typing.User>({
+ 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<LoginResponse, User>({
- action: "login",
+ const res = await http<typing.LoginResponse>({
+ 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");
+ });
</script>
<div class="mx-auto flex min-h-screen w-full items-center justify-center px-3 py-2">
@@ -62,11 +60,17 @@
<Card.Title class="text-2xl">GNU/Weeb Mail Login</Card.Title>
<Card.Description>Proceed login to manager your email account</Card.Description>
- {#if isError()}
+ {#if isError() && !isCredentialInvalid()}
<span class="text-sm font-medium text-destructive">
{$message}
</span>
{/if}
+
+ {#if !isError() && isCredentialInvalid()}
+ <span class="text-sm font-medium text-destructive">
+ Invalid credential, please login again.
+ </span>
+ {/if}
</Card.Header>
<Card.Content class="grid gap-4">
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
next prev parent reply other threads:[~2025-02-22 22:54 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-02-22 22:54 [PATCH v1 00/13] Add Profile & Account Management Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 01/13] fix(toaster): add Toaster component Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 02/13] chore(login): remove unnecessary default data Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 03/13] feat(constants): add settingsNav data Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 04/13] feat(typings/common): add disabled property for Navigations Muhammad Rizki
2025-02-22 22:54 ` Muhammad Rizki [this message]
2025-02-22 22:54 ` [PATCH v1 06/13] chore(schema): rename login schema Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 07/13] feat(ui): add avatar ui Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 08/13] chore(sidebar-menu): add active menu style Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 09/13] chore(ui/avatar): add select-none for avatar fallback Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 10/13] chore(deps): upgrade bits-ui version Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 11/13] feat(ui): add radio-group ui Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 12/13] feat(sidebar-menu): add Roundcube link Muhammad Rizki
2025-02-22 22:54 ` [PATCH v1 13/13] feat: add settings pages Muhammad Rizki
2025-02-23 8:27 ` [PATCH v1 00/13] Add Profile & Account Management Alviro Iskandar Setiawan
2025-02-23 8:43 ` Ammar Faizi
2025-02-23 8:52 ` Alviro Iskandar Setiawan
2025-02-23 8:55 ` Ammar Faizi
2025-02-23 8:57 ` Alviro Iskandar Setiawan
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox