public inbox for [email protected]
 help / color / mirror / Atom feed
From: Muhammad Rizki <[email protected]>
To: Ammar Faizi <[email protected]>
Cc: Kiizuha Kanazawa <[email protected]>,
	Alviro Iskandar Setiawan <[email protected]>,
	Dwiky Rizky Ananditya <[email protected]>,
	GNU/Weeb Mailing List <[email protected]>
Subject: [PATCH v1 09/16] feat: initial hooks, schemas, typings for the login functions
Date: Sun, 19 Jan 2025 07:39:02 +0700	[thread overview]
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>

From: Kiizuha Kanazawa <[email protected]>

added some lib for the login functionality:
- hooks/auth: this hooks include base logic for the auth.
- hooks/http: this hooks used for http request.
- schemas/login: this schema is to create a schema for the login form.
- typings/credential: this types stores user-related type
- typings/http: this types stores http-related type

Signed-off-by: Kiizuha Kanazawa <[email protected]>
---
 src/lib/hooks/auth.svelte.ts    |  64 ++++++++++++++++
 src/lib/hooks/http.svelte.ts    | 129 ++++++++++++++++++++++++++++++++
 src/lib/schemas/login.ts        |   6 ++
 src/lib/typings/credential.d.ts |   7 ++
 src/lib/typings/http.d.ts       |  27 +++++++
 src/lib/typings/index.ts        |  12 ++-
 6 files changed, 244 insertions(+), 1 deletion(-)
 create mode 100644 src/lib/hooks/auth.svelte.ts
 create mode 100644 src/lib/hooks/http.svelte.ts
 create mode 100644 src/lib/schemas/login.ts
 create mode 100644 src/lib/typings/credential.d.ts
 create mode 100644 src/lib/typings/http.d.ts

diff --git a/src/lib/hooks/auth.svelte.ts b/src/lib/hooks/auth.svelte.ts
new file mode 100644
index 0000000..ba357f7
--- /dev/null
+++ b/src/lib/hooks/auth.svelte.ts
@@ -0,0 +1,64 @@
+import type { LoginResponse, User } from "$typings";
+
+let data = $state<LoginResponse>({
+  token: "",
+  token_exp_at: 0
+});
+
+export function useAuth() {
+  return {
+    get token() {
+      return data.token;
+    },
+
+    get exp() {
+      return data.token_exp_at;
+    },
+
+    get user() {
+      return data.user_info;
+    },
+
+    set token(newValue: string) {
+      data.token = newValue;
+    },
+
+    refresh() {
+      const user = localStorage.getItem("gwm_uinfo");
+      data.user_info = JSON.parse(user!) as User;
+    },
+
+    save(newData: LoginResponse) {
+      data = newData;
+      localStorage.setItem("gwm_token", newData.token);
+      localStorage.setItem("gwm_token_exp_at", newData.token_exp_at.toString());
+      localStorage.setItem("gwm_uinfo", JSON.stringify(newData.user_info));
+    },
+
+    clear() {
+      localStorage.removeItem("gwm_token");
+      localStorage.removeItem("gwm_token_exp_at");
+      localStorage.removeItem("gwm_uinfo");
+    },
+
+    isValid() {
+      const token = localStorage.getItem("gwm_token");
+      const user = localStorage.getItem("gwm_uinfo");
+
+      if (!token || !user) {
+        this.clear();
+        return false;
+      }
+
+      const exp = Number(localStorage.getItem("gwm_token_exp_at"));
+      const unix = Math.round(new Date().getTime() / 1000);
+
+      if (unix >= exp) {
+        this.clear();
+        return false;
+      }
+
+      return true;
+    }
+  };
+}
diff --git a/src/lib/hooks/http.svelte.ts b/src/lib/hooks/http.svelte.ts
new file mode 100644
index 0000000..fd4114d
--- /dev/null
+++ b/src/lib/hooks/http.svelte.ts
@@ -0,0 +1,129 @@
+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];
+
+    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);
+    }
+  });
+
+  data = formData;
+  return data;
+};
+
+const GWM_API_URL = "https://mail.gnuweeb.org/api.php?action=";
+
+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);
+
+  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;
+    }
+
+    isLoading = true;
+
+    if (replace?.payload?.defaultPrevented != null) {
+      replace!.payload = null;
+    }
+
+    const initialData: RecordString = replace?.payload ?? payload ?? {};
+    let requestData: RecordString | FormData = initialData;
+
+    if (isFormData) {
+      requestData = transformToFormData(initialData, requestData);
+    }
+
+    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);
+
+        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
+  };
+};
diff --git a/src/lib/schemas/login.ts b/src/lib/schemas/login.ts
new file mode 100644
index 0000000..5cd5ef2
--- /dev/null
+++ b/src/lib/schemas/login.ts
@@ -0,0 +1,6 @@
+import { z } from "zod";
+
+export const loginSchema = z.object({
+  username_or_email: z.string(),
+  password: z.string()
+});
diff --git a/src/lib/typings/credential.d.ts b/src/lib/typings/credential.d.ts
new file mode 100644
index 0000000..df02c86
--- /dev/null
+++ b/src/lib/typings/credential.d.ts
@@ -0,0 +1,7 @@
+export interface User {
+  user_id: number;
+  full_name: string;
+  gender: string;
+  username: string;
+  role: string;
+}
diff --git a/src/lib/typings/http.d.ts b/src/lib/typings/http.d.ts
new file mode 100644
index 0000000..decf147
--- /dev/null
+++ b/src/lib/typings/http.d.ts
@@ -0,0 +1,27 @@
+import type { AxiosRequestConfig, AxiosResponse } from "axios";
+import type { RecordString } from "./common";
+import type { User } from "./credential";
+
+interface ResponseInterface<Data> {
+  code: number;
+  res?: Data;
+}
+
+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 LoginResponse {
+  token: string;
+  token_exp_at: number;
+  user_info?: User | null;
+}
diff --git a/src/lib/typings/index.ts b/src/lib/typings/index.ts
index 26ded78..6ae23a1 100644
--- a/src/lib/typings/index.ts
+++ b/src/lib/typings/index.ts
@@ -1,3 +1,13 @@
 import type { RecordString, Navigations, LabelAndValue, MailConfig } from "./common";
+import type { UseHttpProps, LoginResponse } from "./http";
+import type { User } from "./credential";
 
-export type { RecordString, Navigations, LabelAndValue, MailConfig };
+export type {
+  RecordString,
+  Navigations,
+  LabelAndValue,
+  MailConfig,
+  UseHttpProps,
+  LoginResponse,
+  User
+};
-- 
Muhammad Rizki


  parent reply	other threads:[~2025-01-19  0:40 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-01-19  0:38 [PATCH v1 00/16] Refactor GNU/Weeb Mail Site Muhammad Rizki
2025-01-19  0:38 ` [PATCH v1 01/16] feat(routes): configure page layout options Muhammad Rizki
2025-01-19  0:38 ` [PATCH v1 02/16] feat(fonts): add Google fonts Muhammad Rizki
2025-01-19  0:38 ` [PATCH v1 03/16] feat(components): add shadcn-svelte components Muhammad Rizki
2025-01-19  0:38 ` [PATCH v1 04/16] feat(deps): add `axios` as HTTP client Muhammad Rizki
2025-01-19  0:38 ` [PATCH v1 05/16] feat(deps): add `svelte-copy` dependency Muhammad Rizki
2025-01-19  0:38 ` [PATCH v1 06/16] feat(constants): add navigations and mail-config constant Muhammad Rizki
2025-01-19  0:39 ` [PATCH v1 07/16] feat(components): add loading spinner component Muhammad Rizki
2025-01-19  0:39 ` [PATCH v1 08/16] feat(components): add header component Muhammad Rizki
2025-01-19  0:39 ` Muhammad Rizki [this message]
2025-01-19  0:39 ` [PATCH v1 10/16] feat(components): add app-sidebar component Muhammad Rizki
2025-01-19  0:39 ` [PATCH v1 11/16] feat(routes): create initial settings page Muhammad Rizki
2025-01-19  0:39 ` [PATCH v1 12/16] feat(routes): add home page Muhammad Rizki
2025-01-19  0:39 ` [PATCH v1 13/16] feat(routes): add main layout for the protected routes Muhammad Rizki
2025-01-19  0:39 ` [PATCH v1 14/16] feat(routes): add login page Muhammad Rizki
2025-01-19  0:39 ` [PATCH v1 15/16] chore(.gitignore): add patch files ignore Muhammad Rizki
2025-01-19  0:39 ` [PATCH v1 16/16] chore: update README.md Muhammad Rizki

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] \
    [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