From 9cfbde9c9a71346a11c2e56e6bce3807225da9b6 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sat, 12 Sep 2026 14:20:07 +0100 Subject: [PATCH] Update login pipeline. Limit login to only password --- db/schema.ts | 14 -- db/types/db.ts | 20 +- db/wgui | Bin 61440 -> 61440 bytes .../general/user-form/submit-user-form.ts | 50 +---- src/data/app-data.ts | 2 - src/functions/backend/auth/login-user.ts | 15 -- .../backend/auth/verify-google-id-token.ts | 190 ----------------- .../(partials)/admin-aside-links-dict.tsx | 20 +- src/pages/__root.server.ts | 1 - src/pages/api/admin/crud/[[...paths]].ts | 11 - src/pages/api/admin/delete-media.ts | 2 +- src/pages/api/auth/get-login-code.tsx | 133 ------------ src/pages/api/auth/google-login.ts | 74 ------- src/pages/api/auth/login.ts | 84 +++----- src/pages/api/create-first-user.ts | 67 +++++- .../login/(functions)/submit-login-form.ts | 7 +- .../login/(hooks)/use-google-login-init.tsx | 200 ------------------ .../(partials)/login-form/google-login.tsx | 54 ----- .../login/(partials)/login-form/index.tsx | 17 +- .../login-form/login-form-action.tsx | 2 +- .../login-form/login-form-email-username.tsx | 1 - .../login-form/login-form-password.tsx | 23 ++ .../login/(sections)/login-form-section.tsx | 13 +- src/pages/auth/logout/index.server.ts | 4 - src/pages/auth/logout/index.tsx | 34 +-- .../auth/sso/(partials)/sso-form/index.tsx | 73 ------- src/pages/auth/sso/(sections)/hero.tsx | 20 -- .../auth/sso/(sections)/sso-form-section.tsx | 29 --- src/pages/auth/sso/index.tsx | 18 -- src/types/index.ts | 35 +-- 30 files changed, 145 insertions(+), 1068 deletions(-) delete mode 100644 src/functions/backend/auth/verify-google-id-token.ts delete mode 100644 src/pages/api/auth/get-login-code.tsx delete mode 100644 src/pages/api/auth/google-login.ts delete mode 100644 src/pages/auth/login/(hooks)/use-google-login-init.tsx delete mode 100644 src/pages/auth/login/(partials)/login-form/google-login.tsx create mode 100644 src/pages/auth/login/(partials)/login-form/login-form-password.tsx delete mode 100644 src/pages/auth/sso/(partials)/sso-form/index.tsx delete mode 100644 src/pages/auth/sso/(sections)/hero.tsx delete mode 100644 src/pages/auth/sso/(sections)/sso-form-section.tsx delete mode 100644 src/pages/auth/sso/index.tsx diff --git a/db/schema.ts b/db/schema.ts index 57c943d..fc69fa0 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -56,20 +56,6 @@ const schema: BUN_SQLITE_DatabaseSchemaType = { }, ], }, - { - tableName: "sso_login_codes", - fields: [ - { - fieldName: "user_id", - dataType: "INTEGER", - unique: true, - }, - { - fieldName: "code", - dataType: "TEXT", - }, - ], - }, { tableName: "media", fields: [ diff --git a/db/types/db.ts b/db/types/db.ts index 6880e95..d149d5f 100644 --- a/db/types/db.ts +++ b/db/types/db.ts @@ -1,7 +1,6 @@ export const BunSQLiteTables = [ "users", "user_types", - "sso_login_codes", "media", "media_paradigms", ] as const @@ -45,23 +44,6 @@ export type BUN_SQLITE_WGUI_USER_TYPES = { user_type?: "super_admin" | "admin" | "revoked" | ""; } -export type BUN_SQLITE_WGUI_SSO_LOGIN_CODES = { - /** - * The unique identifier of the record. - */ - id?: number | ""; - /** - * The time when the record was created. (Unix Timestamp) - */ - created_at?: number | ""; - /** - * The time when the record was updated. (Unix Timestamp) - */ - updated_at?: number | ""; - user_id?: number | ""; - code?: string; -} - export type BUN_SQLITE_WGUI_MEDIA = { /** * The unique identifier of the record. @@ -107,4 +89,4 @@ export type BUN_SQLITE_WGUI_MEDIA_PARADIGMS = { media_paradigm?: "user-profile-image" | "generic" | "event" | ""; } -export type BUN_SQLITE_WGUI_ALL_TYPEDEFS = BUN_SQLITE_WGUI_USERS & BUN_SQLITE_WGUI_USER_TYPES & BUN_SQLITE_WGUI_SSO_LOGIN_CODES & BUN_SQLITE_WGUI_MEDIA & BUN_SQLITE_WGUI_MEDIA_PARADIGMS \ No newline at end of file +export type BUN_SQLITE_WGUI_ALL_TYPEDEFS = BUN_SQLITE_WGUI_USERS & BUN_SQLITE_WGUI_USER_TYPES & BUN_SQLITE_WGUI_MEDIA & BUN_SQLITE_WGUI_MEDIA_PARADIGMS \ No newline at end of file diff --git a/db/wgui b/db/wgui index fb5d26325f3134ea58b7039d3a746e4afe80fa1d..96680d7fe091e94b8c919a7e4f141578fb8678ed 100644 GIT binary patch delta 313 zcmZp8z})bFd4e>f@(form, ["first_name", "last_name", "email", "username", "bio"]); + >( + form, + is_first_user + ? ["first_name", "last_name", "email", "username", "password"] + : ["first_name", "last_name", "email", "username", "bio"], + ); setLoading(true); @@ -40,7 +45,7 @@ export default async function submitUserForm({ ? await fetchApi< ApiReqParams, APIResponseObject - >(`/api/create-first-user.ts`, { + >(`/api/create-first-user`, { method: "POST", body: { insert_data: [new_user_data], @@ -86,8 +91,6 @@ export default async function submitUserForm({ reauth: is_user_settings, }); - console.log("upload_image_res", upload_image_res); - if (!upload_image_res.success) { throw new Error( upload_image_res.msg || "Couldn't upload Image", @@ -96,44 +99,7 @@ export default async function submitUserForm({ } if (create_or_update_user_res.success && target_user_id) { - const delete_user_types = await adminCrudHandler({ - action: "delete", - table: "user_types", - sql_query: { - query: { - user_id: { - value: target_user_id, - }, - }, - }, - }); - - const user_types = form.user_types; - - if (user_types?.[0]) { - const add_user_types = - await adminCrudHandler({ - action: "insert", - table: "user_types", - insert_data: user_types.map((ut) => ({ - user_id: target_user_id, - user_type: ut, - })), - }); - - if (add_user_types.success) { - if (existing_full?.id) { - window.location.reload(); - } else { - window.location.pathname = `/admin/people/${target_user_id}`; - } - } else { - console.log("add_user_types", add_user_types); - throw new Error( - add_user_types.msg || "Couldn't add user types", - ); - } - } + window.location.reload(); } else { console.log("res", create_or_update_user_res); console.log("form", form); diff --git a/src/data/app-data.ts b/src/data/app-data.ts index 1f46e6d..81abc2c 100644 --- a/src/data/app-data.ts +++ b/src/data/app-data.ts @@ -2,8 +2,6 @@ import { SiteData } from "./site-data"; export const AppData = { HashCost: 12, - SSOAuthCookieName: `${SiteData["SiteSlug"]}_sso_auth`, - SSOCodeExpiryMinutes: 15, ServerHealthMessage: "Server Running Well", AuthKeyCookieName: `${SiteData["SiteSlug"]}_auth_key`, AuthCSRFCookieName: `${SiteData["SiteSlug"]}_auth_csrf`, diff --git a/src/functions/backend/auth/login-user.ts b/src/functions/backend/auth/login-user.ts index 8797ee3..0fd45c4 100644 --- a/src/functions/backend/auth/login-user.ts +++ b/src/functions/backend/auth/login-user.ts @@ -5,7 +5,6 @@ import grabUsers from "../db/users/grab-users"; import BunSQLite from "@moduletrace/bun-sqlite"; import type { BUN_SQLITE_WGUI_MEDIA, - BUN_SQLITE_WGUI_SSO_LOGIN_CODES, BUN_SQLITE_WGUI_USER_TYPES, BunSQLiteTables, } from "@/db/types/db"; @@ -112,20 +111,6 @@ export default async function loginUser({ }, ]); - const delete_all_user_sso_codes = await BunSQLite.delete< - BUN_SQLITE_WGUI_SSO_LOGIN_CODES, - (typeof BunSQLiteTables)[number] - >({ - table: "sso_login_codes", - query: { - query: { - user_id: { - value: new_logged_in_user.id, - }, - }, - }, - }); - console.log( `User #${new_logged_in_user.id} [${new_logged_in_user.first_name} ${new_logged_in_user.last_name}] logged in successfully.`, ); diff --git a/src/functions/backend/auth/verify-google-id-token.ts b/src/functions/backend/auth/verify-google-id-token.ts deleted file mode 100644 index 7c1b07d..0000000 --- a/src/functions/backend/auth/verify-google-id-token.ts +++ /dev/null @@ -1,190 +0,0 @@ -type GoogleJWTHeader = { - alg?: string; - kid?: string; - typ?: string; -}; - -type GoogleJWKS = { - keys?: GoogleJWK[]; -}; - -type GoogleJWK = JsonWebKey & { - kid?: string; -}; - -type GoogleIDTokenPayload = { - aud?: string | string[]; - email?: string; - email_verified?: boolean | string; - exp?: number | string; - iss?: string; - name?: string; - picture?: string; - sub?: string; -}; - -type Params = { - clientId: string; - idToken: string; -}; - -type GoogleIDTokenUser = Required> & - Pick; - -const GoogleJWKSUrl = "https://www.googleapis.com/oauth2/v3/certs"; - -let cachedGoogleKeys: { - expiresAt: number; - keys: GoogleJWK[]; -} | null = null; - -function decodeBase64Url(value: string) { - const normalizedValue = value.replace(/-/g, "+").replace(/_/g, "/"); - const padding = normalizedValue.length % 4; - const paddedValue = - padding === 0 - ? normalizedValue - : normalizedValue.padEnd( - normalizedValue.length + (4 - padding), - "=", - ); - - return Buffer.from(paddedValue, "base64"); -} - -function parseGoogleTokenPart(value: string): T { - return JSON.parse(decodeBase64Url(value).toString("utf8")) as T; -} - -async function getGoogleSigningKeys() { - if (cachedGoogleKeys && cachedGoogleKeys.expiresAt > Date.now()) { - return cachedGoogleKeys.keys; - } - - const response = await fetch(GoogleJWKSUrl); - - if (!response.ok) { - throw new Error(`Couldn't fetch Google's signing keys`); - } - - const data = (await response.json()) as GoogleJWKS; - - if (!data.keys?.length) { - throw new Error(`Google didn't return any signing keys`); - } - - const maxAge = - Number( - response.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1], - ) || 300; - - cachedGoogleKeys = { - expiresAt: Date.now() + maxAge * 1000, - keys: data.keys, - }; - - return data.keys; -} - -function hasMatchingAudience( - aud: GoogleIDTokenPayload["aud"], - clientId: string, -) { - if (Array.isArray(aud)) { - return aud.includes(clientId); - } - - return aud === clientId; -} - -export default async function verifyGoogleIdToken({ - clientId, - idToken, -}: Params): Promise { - const tokenParts = idToken.split("."); - - if (tokenParts.length !== 3) { - throw new Error(`Invalid Google token`); - } - - const [encodedHeader, encodedPayload, encodedSignature] = tokenParts; - - if (!encodedHeader) { - throw new Error(`No encodedHeader`); - } - - if (!encodedPayload) { - throw new Error(`No encodedPayload`); - } - - if (!encodedSignature) { - throw new Error(`No encodedSignature`); - } - - const header = parseGoogleTokenPart(encodedHeader); - const payload = parseGoogleTokenPart(encodedPayload); - - if (header.alg !== "RS256" || !header.kid) { - throw new Error(`Unsupported Google token signature`); - } - - const signingKeys = await getGoogleSigningKeys(); - const signingKey = signingKeys.find((key) => key.kid === header.kid); - - if (!signingKey) { - throw new Error(`Unknown Google signing key`); - } - - const importedKey = await crypto.subtle.importKey( - "jwk", - signingKey, - { - name: "RSASSA-PKCS1-v1_5", - hash: "SHA-256", - }, - false, - ["verify"], - ); - - const isValidSignature = await crypto.subtle.verify( - "RSASSA-PKCS1-v1_5", - importedKey, - decodeBase64Url(encodedSignature), - new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`), - ); - - if (!isValidSignature) { - throw new Error(`Invalid Google token signature`); - } - - if (!hasMatchingAudience(payload.aud, clientId)) { - throw new Error(`Google token was issued for a different app`); - } - - if ( - payload.iss !== "https://accounts.google.com" && - payload.iss !== "accounts.google.com" - ) { - throw new Error(`Invalid Google token issuer`); - } - - const expiresAt = Number(payload.exp || 0) * 1000; - - if (!expiresAt || expiresAt <= Date.now()) { - throw new Error(`Google token has expired`); - } - - const emailVerified = - payload.email_verified === true || payload.email_verified === "true"; - - if (!payload.email || !emailVerified || !payload.sub) { - throw new Error(`Google account email is not verified`); - } - - return { - email: payload.email, - sub: payload.sub, - name: payload.name, - picture: payload.picture, - }; -} diff --git a/src/layouts/admin/(partials)/admin-aside-links-dict.tsx b/src/layouts/admin/(partials)/admin-aside-links-dict.tsx index d9d89b2..a790502 100644 --- a/src/layouts/admin/(partials)/admin-aside-links-dict.tsx +++ b/src/layouts/admin/(partials)/admin-aside-links-dict.tsx @@ -19,24 +19,12 @@ export default function AdminAsideLinks() { strict: true, }, { - title: "People", - url: "/admin/people", + title: "Clients", + url: "/admin/clients", }, { - title: "Events", - url: "/admin/events", - }, - { - title: "Media", - url: "/admin/media", - }, - { - title: "Board", - url: "/admin/board", - }, - { - title: "Locations", - url: "/admin/locations", + title: "Host", + url: "/admin/host", }, { component:
, diff --git a/src/pages/__root.server.ts b/src/pages/__root.server.ts index 19e2891..6249256 100644 --- a/src/pages/__root.server.ts +++ b/src/pages/__root.server.ts @@ -31,7 +31,6 @@ const server: BunextPageServerFn = async ({ req, url }) => { props: { environment: process.env.NODE_ENV, envs: { - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || "", R2_PUBLIC_DOMAIN: process.env.R2_PUBLIC_DOMAIN || "", PAYSTACK_PUBLIC_KEY: process.env.PAYSTACK_PUBLIC_KEY || "", }, diff --git a/src/pages/api/admin/crud/[[...paths]].ts b/src/pages/api/admin/crud/[[...paths]].ts index 1f6fe49..16e7a32 100644 --- a/src/pages/api/admin/crud/[[...paths]].ts +++ b/src/pages/api/admin/crud/[[...paths]].ts @@ -40,13 +40,6 @@ export const handler: BunextAPIRouteHandler = async ( } try { - if ( - user_types.find((ty) => ty.user_type == "read_only") && - !req.method.match(/get/) - ) { - throw new Error(`This user can only read records.`); - } - const [table, id] = params.query.paths.split("/") as [ TableType, string | number, @@ -63,10 +56,6 @@ export const handler: BunextAPIRouteHandler = async ( }; switch (table) { - case "users": - return await users(crud_params); - case "user_types": - return await userTypes(crud_params); case "media": return await media(crud_params); diff --git a/src/pages/api/admin/delete-media.ts b/src/pages/api/admin/delete-media.ts index c80a1dd..3113ddb 100644 --- a/src/pages/api/admin/delete-media.ts +++ b/src/pages/api/admin/delete-media.ts @@ -34,7 +34,7 @@ export const handler: BunextAPIRouteHandler = async ( const { id, ids, media_paradigm, user_id } = body; const can_delete_all_media = checkUserAccess({ - includes: ["admin", "board_member"], + includes: ["admin"], user_types, }); diff --git a/src/pages/api/auth/get-login-code.tsx b/src/pages/api/auth/get-login-code.tsx deleted file mode 100644 index dc2d53d..0000000 --- a/src/pages/api/auth/get-login-code.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import type { - BUN_SQLITE_WGUI_SSO_LOGIN_CODES, - BUN_SQLITE_WGUI_USERS, - BunSQLiteTables, -} from "@/db/types/db"; -import { AppData } from "@/src/data/app-data"; -import { SiteData } from "@/src/data/site-data"; -import sendEmail from "@/src/functions/backend/email/send-email"; -import type { ApiReqParams, SSOAuth } from "@/src/types"; -import { setCookies } from "@/src/utils/cookies"; -import { encrypt } from "@/src/utils/crypt"; -import EJSON from "@/src/utils/ejson"; -import BunSQLite from "@moduletrace/bun-sqlite"; -import BunSQLiteB from "@moduletrace/bun-sqlite"; -import type { - APIResponseObject, - BunextAPIRouteHandler, -} from "@moduletrace/bunext/types"; - -export const handler: BunextAPIRouteHandler = async ({ - req, - body, -}) => { - try { - const { login } = body as ApiReqParams; - - if (!login?.email_or_username) { - throw new Error(`Please pass an email or phone number`); - } - - const target_user = ( - await BunSQLiteB.select< - BUN_SQLITE_WGUI_USERS, - (typeof BunSQLiteTables)[number] - >({ - table: "users", - query: { - query: { - email: { - value: login.email_or_username, - }, - username: { - value: login.email_or_username, - }, - }, - searchOperator: "OR", - }, - }) - )?.singleRes; - - if (!target_user?.id || !target_user.email) { - throw new Error(`User not Found!`); - } - - const sso_code = Math.random().toString().slice(2, 8); - - const email_component = ( -
-
Use this Code to Complete your login
-

{sso_code}

-
- Code expires in {AppData["SSOCodeExpiryMinutes"]} minutes. -
-
- ); - - const send_mail = await sendEmail({ - content: email_component, - to: - process.env.NODE_ENV == "production" - ? target_user.email - : "benoti.san@gmail.com", - subject: `Use one-time-code to complete your login.`, - text: `Complete your login to the SBF portal`, - title: `${SiteData["SiteName"]} SSO`, - options: { - priority: "high", - }, - }); - - if (!send_mail.success) { - return send_mail; - } - - const record_sso = await BunSQLite.insert< - BUN_SQLITE_WGUI_SSO_LOGIN_CODES, - (typeof BunSQLiteTables)[number] - >({ - data: [ - { - code: sso_code, - user_id: target_user.id, - }, - ], - table: "sso_login_codes", - update_on_duplicate: true, - }); - - const sso_auth: SSOAuth = { - user_id: target_user.id, - sso_code, - email: target_user.email, - }; - - const sso_auth_string = EJSON.stringify(sso_auth); - if (!sso_auth_string) { - throw new Error(`Couldn't Stringify SSO Auth`); - } - - const encrypted_sso_auth_string = await encrypt(sso_auth_string); - - return { - success: true, - bunext_api_route_res_transform_fn(res) { - const new_res = res.clone(); - setCookies(new_res, [ - { - name: AppData["SSOAuthCookieName"], - value: encrypted_sso_auth_string, - }, - ]); - return new_res; - }, - }; - } catch (error: any) { - console.log("error", error.message); - - return { - success: false, - msg: error.message, - }; - } -}; diff --git a/src/pages/api/auth/google-login.ts b/src/pages/api/auth/google-login.ts deleted file mode 100644 index 1de4336..0000000 --- a/src/pages/api/auth/google-login.ts +++ /dev/null @@ -1,74 +0,0 @@ -import loginUser from "@/src/functions/backend/auth/login-user"; -import verifyGoogleIdToken from "@/src/functions/backend/auth/verify-google-id-token"; -import grabUsers from "@/src/functions/backend/db/users/grab-users"; -import uploadAndRecordMedia from "@/src/functions/backend/db/users/media/upload-and-record-media"; -import type { ApiReqParams } from "@/src/types"; -import type { - APIResponseObject, - BunextAPIRouteHandler, -} from "@moduletrace/bunext/types"; - -export const handler: BunextAPIRouteHandler = async ({ - body, -}) => { - try { - const { google_token } = body as ApiReqParams; - - if (!google_token) { - throw new Error(`No Google token was provided`); - } - - const googleClientId = process.env.GOOGLE_CLIENT_ID; - - if (!googleClientId) { - throw new Error(`Google login is not configured on this server`); - } - - const googleUser = await verifyGoogleIdToken({ - clientId: googleClientId, - idToken: google_token, - }); - - const target_user_res = await grabUsers({ - query: { - search_term_email: googleUser.email, - }, - exact_match: true, - }); - - const target_user = target_user_res.singleRes; - - if (!target_user?.id) { - throw new Error(`No account exists for ${googleUser.email}`); - } - - if (googleUser.picture && !target_user.profile_media_id) { - const fullResUrl = googleUser.picture.replace(/=s\d+-c$/, "=s0"); - - const res = await fetch(fullResUrl); - - if (res.ok) { - const bytes = new Uint8Array(await res.arrayBuffer()); - - await uploadAndRecordMedia({ - data: bytes, - user: target_user, - media_paradigms: ["user-profile-image"], - media_type: "image", - is_primary: true, - media_name: `user-${target_user.id}-profile`, - media_mime_type: "jpeg", - }); - } - } - - return await loginUser({ - user_id: target_user.id, - }); - } catch (error: any) { - return { - success: false, - msg: error.message, - }; - } -}; diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts index 1e76ee7..c922835 100644 --- a/src/pages/api/auth/login.ts +++ b/src/pages/api/auth/login.ts @@ -1,14 +1,9 @@ import type { - BUN_SQLITE_WGUI_SSO_LOGIN_CODES, BUN_SQLITE_WGUI_USERS, BunSQLiteTables, } from "@/db/types/db"; -import { AppData } from "@/src/data/app-data"; import loginUser from "@/src/functions/backend/auth/login-user"; -import type { ApiReqParams, SSOAuth } from "@/src/types"; -import { getCookie } from "@/src/utils/cookies"; -import { decrypt } from "@/src/utils/crypt"; -import EJSON from "@/src/utils/ejson"; +import type { ApiReqParams } from "@/src/types"; import BunSQLite from "@moduletrace/bun-sqlite"; import type { APIResponseObject, @@ -16,51 +11,17 @@ import type { } from "@moduletrace/bunext/types"; export const handler: BunextAPIRouteHandler = async ({ - req, body, }) => { try { - const { sso_code } = body as ApiReqParams; + const { login } = body as ApiReqParams; - const sso_cookie = getCookie(req, AppData["SSOAuthCookieName"]); - - if (!sso_cookie) { - throw new Error(`No SSO session found!`); + if (!login?.email_or_username) { + throw new Error(`Please enter your email or username`); } - if (!sso_code) { - throw new Error(`No SSO code sent!`); - } - - const decrypted_sso_json = await decrypt(sso_cookie); - const decrypted_sso_object = EJSON.parse(decrypted_sso_json) as SSOAuth; - - const target_sso = ( - await BunSQLite.select< - BUN_SQLITE_WGUI_SSO_LOGIN_CODES, - (typeof BunSQLiteTables)[number] - >({ - table: "sso_login_codes", - query: { - query: { - code: { - value: sso_code, - }, - }, - }, - }) - ).singleRes; - - if (!target_sso?.code) { - throw new Error(`Invalid Code!`); - } - - const now = Date.now(); - const time_elapsed = now - Number(target_sso.updated_at); - const expirty_time = AppData["SSOCodeExpiryMinutes"] * 60 * 1000; - - if (time_elapsed > expirty_time) { - throw new Error(`Code Expired. Please Login again.`); + if (!login?.password) { + throw new Error(`Please enter your password`); } const target_user = ( @@ -71,16 +32,35 @@ export const handler: BunextAPIRouteHandler = async ({ table: "users", query: { query: { - email: decrypted_sso_object.email - ? { value: decrypted_sso_object.email } - : undefined, + email: { + value: login.email_or_username, + }, + username: { + value: login.email_or_username, + }, }, + searchOperator: "OR", }, }) - ).singleRes; + )?.singleRes; - if (!target_user?.id) { - throw new Error(`This device wasn't used to get this SSO code`); + if (!target_user?.id || !target_user.password) { + throw new Error(`Invalid email or password`); + } + + let password_matches = false; + + try { + password_matches = await Bun.password.verify( + login.password, + target_user.password, + ); + } catch (error) { + password_matches = false; + } + + if (!password_matches) { + throw new Error(`Invalid email or password`); } return await loginUser({ @@ -92,4 +72,4 @@ export const handler: BunextAPIRouteHandler = async ({ msg: error.message, }; } -}; +}; \ No newline at end of file diff --git a/src/pages/api/create-first-user.ts b/src/pages/api/create-first-user.ts index f2c97fe..6bfb241 100644 --- a/src/pages/api/create-first-user.ts +++ b/src/pages/api/create-first-user.ts @@ -1,4 +1,8 @@ -import type { BUN_SQLITE_WGUI_USERS } from "@/db/types/db"; +import type { + BUN_SQLITE_WGUI_USER_TYPES, + BUN_SQLITE_WGUI_USERS, +} from "@/db/types/db"; +import { AppData } from "@/src/data/app-data"; import grabUsers from "@/src/functions/backend/db/users/grab-users"; import type { ApiReqParams, TableType } from "@/src/types"; import BunSQLite from "@moduletrace/bun-sqlite"; @@ -6,8 +10,6 @@ import type { APIResponseObject, BunextAPIRouteHandler, } from "@moduletrace/bunext/types"; -import { hash } from "bun"; -import _ from "lodash"; export const handler: BunextAPIRouteHandler< APIResponseObject @@ -23,26 +25,69 @@ export const handler: BunextAPIRouteHandler< }, }); - console.log("users_res", users_res); - if (users_res.singleRes?.id) { return { success: false, }; } - let final_insert_data = [ - ...(body?.insert_data?.map((data) => { - return { ...data, password: hash(data.password || "") }; - }) || []), - ]; + const final_insert_data = await Promise.all( + body?.insert_data?.map(async (data) => { + if (!data.password) { + throw new Error( + `Password is required to create the first user.`, + ); + } + + return { + ...data, + password: await Bun.password.hash(data.password, { + algorithm: "bcrypt", + cost: AppData["HashCost"], + }), + id: 1, + }; + }) || [], + ); const POST = await BunSQLite.insert({ table: "users", data: final_insert_data, }); - return POST; + const new_users_res = await grabUsers({ + query: { + sql_query: { + limit: 1, + }, + }, + }); + + if (!new_users_res.singleRes?.id) { + throw new Error( + `Couldn't find newly created user record! Try again.`, + ); + } + + const add_super_admin_user_types = await BunSQLite.insert< + BUN_SQLITE_WGUI_USER_TYPES, + TableType + >({ + table: "user_types", + data: [ + { + user_type: "super_admin", + user_id: new_users_res.singleRes.id, + }, + ], + }); + + console.log("add_super_admin_user_types", add_super_admin_user_types); + + return { + ...POST, + singleRes: new_users_res.singleRes, + }; } catch (error: any) { return { success: false, diff --git a/src/pages/auth/login/(functions)/submit-login-form.ts b/src/pages/auth/login/(functions)/submit-login-form.ts index f4516ea..fd8b760 100644 --- a/src/pages/auth/login/(functions)/submit-login-form.ts +++ b/src/pages/auth/login/(functions)/submit-login-form.ts @@ -11,19 +11,20 @@ export default async function submitLoginForm({ setLoading(true); const res = await fetchApi( - `/api/auth/get-login-code`, + `/api/auth/login`, { method: "POST", body: { login: { email_or_username: form.username_or_email, + password: form.password, }, }, }, ); if (res.success) { - window.location.pathname = `/auth/sso`; + window.location.pathname = `/admin`; } else { setStatus({ error: true, @@ -34,4 +35,4 @@ export default async function submitLoginForm({ setLoading(false); } -} +} \ No newline at end of file diff --git a/src/pages/auth/login/(hooks)/use-google-login-init.tsx b/src/pages/auth/login/(hooks)/use-google-login-init.tsx deleted file mode 100644 index 8e104be..0000000 --- a/src/pages/auth/login/(hooks)/use-google-login-init.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import fetchApi from "@/src/components/twui/utils/fetch/fetchApi"; -import type useFormInit from "@/src/hooks/use-form-init"; -import { AppContext } from "@/src/pages/__root"; -import type { ApiReqParams, GoogleWindow, LoginFormObject } from "@/src/types"; -import type { APIResponseObject } from "@moduletrace/bunext/types"; -import { useContext, useEffect, useRef, useState } from "react"; - -const GoogleScriptSelector = - 'script[src="https://accounts.google.com/gsi/client"]'; - -export default function useGoogleLoginInit({ - loading, - setStatus, - status, - setLoading, -}: ReturnType>) { - const { pageProps } = useContext(AppContext); - - const googleButtonRef = useRef(null); - const googleInitializedRef = useRef(false); - const [scriptLoaded, setScriptLoaded] = useState(false); - const [buttonWidth, setButtonWidth] = useState(0); - const [googleReady, setGoogleReady] = useState(false); - - const googleClientId = pageProps?.envs?.GOOGLE_CLIENT_ID; - - const submitGoogleLogin = async (google_token: string) => { - setLoading(true); - - const res = await fetchApi( - `/api/auth/google-login`, - { - method: "POST", - body: { - google_token, - }, - }, - ); - - if (res.success) { - window.location.pathname = `/admin`; - } else { - setStatus({ - error: true, - msg: res.msg || "Google Login Failed", - }); - setLoading(false); - } - }; - - useEffect(() => { - if (!googleClientId) { - return; - } - - const clientWindow = window as GoogleWindow; - - if (clientWindow.google?.accounts?.id) { - setScriptLoaded(true); - return; - } - - const existingScript = document.querySelector( - GoogleScriptSelector, - ) as HTMLScriptElement | null; - - const handleLoad = () => { - setScriptLoaded(true); - setStatus(undefined); - }; - - const handleError = () => { - setStatus({ - error: true, - msg: "Couldn't load Google login.", - }); - }; - - if (existingScript) { - existingScript.addEventListener("load", handleLoad); - existingScript.addEventListener("error", handleError); - - return () => { - existingScript.removeEventListener("load", handleLoad); - existingScript.removeEventListener("error", handleError); - }; - } - - const script = document.createElement("script"); - script.src = "https://accounts.google.com/gsi/client"; - script.async = true; - script.defer = true; - script.addEventListener("load", handleLoad); - script.addEventListener("error", handleError); - - document.head.appendChild(script); - - return () => { - script.removeEventListener("load", handleLoad); - script.removeEventListener("error", handleError); - }; - }, [status]); - - useEffect(() => { - if (!googleButtonRef.current || typeof ResizeObserver === "undefined") { - return; - } - - const resizeObserver = new ResizeObserver((entries) => { - const nextWidth = Math.round(entries[0]?.contentRect.width || 0); - - if (!nextWidth) { - return; - } - - setButtonWidth((prev) => (prev === nextWidth ? prev : nextWidth)); - }); - - resizeObserver.observe(googleButtonRef.current); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - useEffect(() => { - if ( - !googleClientId || - !scriptLoaded || - !googleButtonRef.current || - !buttonWidth - ) { - return; - } - - const googleId = (window as GoogleWindow).google?.accounts?.id; - - if (!googleId) { - return; - } - - if (!googleInitializedRef.current) { - googleId.initialize({ - client_id: googleClientId, - callback: async ({ credential }) => { - if (!credential) { - setStatus({ - error: true, - msg: "Google login did not return a token.", - }); - return; - } - - await submitGoogleLogin(credential); - }, - }); - - googleInitializedRef.current = true; - } - - googleButtonRef.current.innerHTML = ""; - googleId.renderButton(googleButtonRef.current, { - theme: "outline", - size: "large", - text: "signin_with", - shape: "rectangular", - width: Math.min(buttonWidth, 400), - }); - setGoogleReady(true); - }, [buttonWidth, scriptLoaded, status, submitGoogleLogin]); - - const handleButtonClick = () => { - if (loading) { - return; - } - - if (!googleClientId) { - setStatus({ - error: true, - msg: "Google login is not configured yet.", - }); - return; - } - - if (!googleInitializedRef.current) { - setStatus({ - error: true, - msg: "Google login is still loading. Please try again.", - }); - } - }; - - return { - loading, - handleButtonClick, - googleClientId, - googleButtonRef, - googleReady, - }; -} diff --git a/src/pages/auth/login/(partials)/login-form/google-login.tsx b/src/pages/auth/login/(partials)/login-form/google-login.tsx deleted file mode 100644 index d6e4316..0000000 --- a/src/pages/auth/login/(partials)/login-form/google-login.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import Button from "@/src/components/twui/layout/Button"; -import Img from "@/src/components/twui/layout/Img"; -import Row from "@/src/components/twui/layout/Row"; -import type useFormInit from "@/src/hooks/use-form-init"; -import type { LoginFormObject } from "@/src/types"; -import { twMerge } from "tailwind-merge"; -import useGoogleLoginInit from "../../(hooks)/use-google-login-init"; - -export default function GoogleLogin( - init: ReturnType>, -) { - const { - loading, - handleButtonClick, - googleClientId, - googleButtonRef, - googleReady, - } = useGoogleLoginInit(init); - - return ( - - - {googleClientId ? ( -
- ) : null} - - ); -} diff --git a/src/pages/auth/login/(partials)/login-form/index.tsx b/src/pages/auth/login/(partials)/login-form/index.tsx index 0c96156..5d681c3 100644 --- a/src/pages/auth/login/(partials)/login-form/index.tsx +++ b/src/pages/auth/login/(partials)/login-form/index.tsx @@ -1,13 +1,10 @@ import Form from "@/src/components/twui/form/Form"; import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay"; -import Row from "@/src/components/twui/layout/Row"; -import Divider from "@/src/components/twui/layout/Divider"; -import Span from "@/src/components/twui/layout/Span"; +import Tag from "@/src/components/twui/elements/Tag"; import useFormInit from "@/src/hooks/use-form-init"; import { type LoginFormObject } from "@/src/types"; -import LoginShell from "@/src/components/general/login-shell"; -import GoogleLogin from "./google-login"; import LoginFormEmailUsername from "./login-form-email-username"; +import LoginFormPassword from "./login-form-password"; import submitLoginForm from "../../(functions)/submit-login-form"; import LoginFormAction from "./login-form-action"; @@ -17,22 +14,18 @@ export default function LoginForm() { return ( <> - - - - OR - -
{ submitLoginForm(init); }} > + {status?.error && {status.msg}} {loading && } + ); -} +} \ No newline at end of file diff --git a/src/pages/auth/login/(partials)/login-form/login-form-action.tsx b/src/pages/auth/login/(partials)/login-form/login-form-action.tsx index 3588e7c..1f9315e 100644 --- a/src/pages/auth/login/(partials)/login-form/login-form-action.tsx +++ b/src/pages/auth/login/(partials)/login-form/login-form-action.tsx @@ -9,7 +9,7 @@ export default function LoginFormAction({}: ReturnType< return ( <> ); diff --git a/src/pages/auth/login/(partials)/login-form/login-form-email-username.tsx b/src/pages/auth/login/(partials)/login-form/login-form-email-username.tsx index ba4b30e..f145662 100644 --- a/src/pages/auth/login/(partials)/login-form/login-form-email-username.tsx +++ b/src/pages/auth/login/(partials)/login-form/login-form-email-username.tsx @@ -7,7 +7,6 @@ export default function LoginFormEmailUsername({ }: ReturnType>) { return ( { setForm((prev) => ({ diff --git a/src/pages/auth/login/(partials)/login-form/login-form-password.tsx b/src/pages/auth/login/(partials)/login-form/login-form-password.tsx new file mode 100644 index 0000000..fa3e6d1 --- /dev/null +++ b/src/pages/auth/login/(partials)/login-form/login-form-password.tsx @@ -0,0 +1,23 @@ +import Input from "@/src/components/twui/form/Input"; +import useFormInit from "@/src/hooks/use-form-init"; +import { type LoginFormObject } from "@/src/types"; + +export default function LoginFormPassword({ + setForm, +}: ReturnType>) { + return ( + { + setForm((prev) => ({ + ...prev, + password: e.target.value, + })); + }} + showLabel + required + /> + ); +} \ No newline at end of file diff --git a/src/pages/auth/login/(sections)/login-form-section.tsx b/src/pages/auth/login/(sections)/login-form-section.tsx index e642f5b..2d12377 100644 --- a/src/pages/auth/login/(sections)/login-form-section.tsx +++ b/src/pages/auth/login/(sections)/login-form-section.tsx @@ -1,21 +1,10 @@ import Stack from "@/src/components/twui/layout/Stack"; import LoginForm from "../(partials)/login-form"; -import Span from "@/src/components/twui/layout/Span"; -import ArrowedLink from "@/src/components/twui/layout/ArrowedLink"; export default function LoginFormSection() { return ( - - Received an SSO code already? - - ); -} +} \ No newline at end of file diff --git a/src/pages/auth/logout/index.server.ts b/src/pages/auth/logout/index.server.ts index 389533d..3218170 100644 --- a/src/pages/auth/logout/index.server.ts +++ b/src/pages/auth/logout/index.server.ts @@ -15,10 +15,6 @@ export const server: BunextPageServerFn = async ({ req }) => { name: AppData["AuthCSRFCookieName"], httpOnly: true, }, - { - name: AppData["SSOAuthCookieName"], - httpOnly: true, - }, ]); return new_res; }, diff --git a/src/pages/auth/logout/index.tsx b/src/pages/auth/logout/index.tsx index dfdc60a..1d185d4 100644 --- a/src/pages/auth/logout/index.tsx +++ b/src/pages/auth/logout/index.tsx @@ -1,43 +1,25 @@ import type { BunextPageModuleMeta } from "@moduletrace/bunext/types"; -import Section from "@/src/components/twui/layout/Section"; import Loading from "@/src/components/twui/elements/Loading"; import Row from "@/src/components/twui/layout/Row"; import Span from "@/src/components/twui/layout/Span"; import { useEffect } from "react"; -import { twMerge } from "tailwind-merge"; -import Logo from "@/src/components/general/logo"; -import Paper from "@/src/components/twui/elements/Paper"; -import Container from "@/src/components/twui/layout/Container"; -import Center from "@/src/components/twui/layout/Center"; import { SiteData } from "@/src/data/site-data"; +import Stack from "@/src/components/twui/layout/Stack"; export default function LogoutPage() { useEffect(() => { setTimeout(() => { window.location.pathname = "/"; - }, 1000); + }, 2000); }, []); return ( -
-
- - - - - Logging out ... - - -
-
+ + + + Logging out ... + + ); } diff --git a/src/pages/auth/sso/(partials)/sso-form/index.tsx b/src/pages/auth/sso/(partials)/sso-form/index.tsx deleted file mode 100644 index b284eca..0000000 --- a/src/pages/auth/sso/(partials)/sso-form/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import Form from "@/src/components/twui/form/Form"; -import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay"; -import Input from "@/src/components/twui/form/Input"; -import Button from "@/src/components/twui/layout/Button"; -import useFormInit from "@/src/hooks/use-form-init"; -import fetchApi from "@/src/components/twui/utils/fetch/fetchApi"; -import { type ApiReqParams, type SSOFormObject } from "@/src/types"; -import type { APIResponseObject } from "@moduletrace/bunext/types"; -import LoginShell from "@/src/components/general/login-shell"; - -export default function SSOForm() { - const init = useFormInit(); - const { form, loading, setForm, setLoading, status, setStatus } = init; - - const submitSSOLoginForm = async () => { - setLoading(true); - - const res = await fetchApi( - `/api/auth/login`, - { - method: "POST", - body: { - sso_code: form.code, - }, - }, - ); - - if (res.success) { - window.location.pathname = `/admin`; - } else { - setStatus({ - error: true, - msg: res.msg || "Login Failed", - }); - console.log("res", res); - console.log("form", form); - - setLoading(false); - } - }; - - return ( - <> - -
- {loading && } - - { - setForm((prev) => ({ - ...prev, - code: e.target.value, - })); - }} - showLabel - required - /> - - -
- - ); -} diff --git a/src/pages/auth/sso/(sections)/hero.tsx b/src/pages/auth/sso/(sections)/hero.tsx deleted file mode 100644 index 3ca2ea1..0000000 --- a/src/pages/auth/sso/(sections)/hero.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import Section from "@/src/components/twui/layout/Section"; -import Container from "@/src/components/twui/layout/Container"; -import Stack from "@/src/components/twui/layout/Stack"; -import H1 from "@/src/components/twui/layout/H1"; -import Span from "@/src/components/twui/layout/Span"; - -export default function Hero() { - return ( -
- - - SSO Login -

- Complete your login -

-
-
-
- ); -} diff --git a/src/pages/auth/sso/(sections)/sso-form-section.tsx b/src/pages/auth/sso/(sections)/sso-form-section.tsx deleted file mode 100644 index 2e5029f..0000000 --- a/src/pages/auth/sso/(sections)/sso-form-section.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import Section from "@/src/components/twui/layout/Section"; -import Container from "@/src/components/twui/layout/Container"; -import Stack from "@/src/components/twui/layout/Stack"; -import SSOForm from "../(partials)/sso-form"; -import Span from "@/src/components/twui/layout/Span"; -import ArrowedLink from "@/src/components/twui/layout/ArrowedLink"; - -export default function SSOFormSection() { - return ( -
- - - - - - Login Failed or Code Expired? - - - - - -
- ); -} diff --git a/src/pages/auth/sso/index.tsx b/src/pages/auth/sso/index.tsx deleted file mode 100644 index 6b8346b..0000000 --- a/src/pages/auth/sso/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { BunextPageModuleMeta } from "@moduletrace/bunext/types"; -import Hero from "./(sections)/hero"; -import SSOFormSection from "./(sections)/sso-form-section"; -import { SiteData } from "@/src/data/site-data"; - -export default function SSOPage() { - return ( - <> - - - - ); -} - -export const meta: BunextPageModuleMeta = { - title: `Sign In | ${SiteData["SiteName"]}`, - description: `Sign in to your account to access the admin dashboard.`, -}; diff --git a/src/types/index.ts b/src/types/index.ts index 84028b7..ddea4bb 100755 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,7 +35,6 @@ export type PagePropsType = { persons?: BUN_SQLITE_WGUI_USERS_JOIN[] | null; environment?: string; envs?: { - GOOGLE_CLIENT_ID?: string; R2_PUBLIC_DOMAIN?: string; PAYSTACK_PUBLIC_KEY?: string; }; @@ -157,10 +156,7 @@ export type UserType = (typeof UserTypes)[number]["value"]; export type LoginFormObject = { username_or_email?: string; -}; - -export type SSOFormObject = { - code?: string; + password?: string; }; export type ContactFormObject = { @@ -178,8 +174,6 @@ export type ApiReqParams< email_or_username?: string; password?: string; }; - google_token?: string; - sso_code?: string; contact?: { name?: string; email?: string; @@ -228,33 +222,6 @@ export type ApiReqParams< media?: BUN_SQLITE_WGUI_MEDIA; }; -export type GoogleCredentialResponse = { - credential?: string; -}; - -export type GoogleWindow = globalThis.Window & { - google?: { - accounts?: { - id?: { - initialize: (params: { - client_id: string; - callback: (response: GoogleCredentialResponse) => void; - }) => void; - renderButton: ( - parent: HTMLElement, - options: Record, - ) => void; - }; - }; - }; -}; - -export type SSOAuth = { - user_id: number; - sso_code: string | number; - email?: string | null; -}; - export type UserAuthReturn = { success: boolean; msg?: string;