Update login pipeline. Limit login to only password
This commit is contained in:
@@ -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",
|
tableName: "media",
|
||||||
fields: [
|
fields: [
|
||||||
|
|||||||
+1
-19
@@ -1,7 +1,6 @@
|
|||||||
export const BunSQLiteTables = [
|
export const BunSQLiteTables = [
|
||||||
"users",
|
"users",
|
||||||
"user_types",
|
"user_types",
|
||||||
"sso_login_codes",
|
|
||||||
"media",
|
"media",
|
||||||
"media_paradigms",
|
"media_paradigms",
|
||||||
] as const
|
] as const
|
||||||
@@ -45,23 +44,6 @@ export type BUN_SQLITE_WGUI_USER_TYPES = {
|
|||||||
user_type?: "super_admin" | "admin" | "revoked" | "";
|
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 = {
|
export type BUN_SQLITE_WGUI_MEDIA = {
|
||||||
/**
|
/**
|
||||||
* The unique identifier of the record.
|
* The unique identifier of the record.
|
||||||
@@ -107,4 +89,4 @@ export type BUN_SQLITE_WGUI_MEDIA_PARADIGMS = {
|
|||||||
media_paradigm?: "user-profile-image" | "generic" | "event" | "";
|
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
|
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
|
||||||
@@ -32,7 +32,12 @@ export default async function submitUserForm({
|
|||||||
const new_user_data: BUN_SQLITE_WGUI_USERS = _.pick<
|
const new_user_data: BUN_SQLITE_WGUI_USERS = _.pick<
|
||||||
UserFormObject,
|
UserFormObject,
|
||||||
keyof UserFormObject
|
keyof UserFormObject
|
||||||
>(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);
|
setLoading(true);
|
||||||
|
|
||||||
@@ -40,7 +45,7 @@ export default async function submitUserForm({
|
|||||||
? await fetchApi<
|
? await fetchApi<
|
||||||
ApiReqParams,
|
ApiReqParams,
|
||||||
APIResponseObject<BUN_SQLITE_WGUI_USERS>
|
APIResponseObject<BUN_SQLITE_WGUI_USERS>
|
||||||
>(`/api/create-first-user.ts`, {
|
>(`/api/create-first-user`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
insert_data: [new_user_data],
|
insert_data: [new_user_data],
|
||||||
@@ -86,8 +91,6 @@ export default async function submitUserForm({
|
|||||||
reauth: is_user_settings,
|
reauth: is_user_settings,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("upload_image_res", upload_image_res);
|
|
||||||
|
|
||||||
if (!upload_image_res.success) {
|
if (!upload_image_res.success) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
upload_image_res.msg || "Couldn't upload Image",
|
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) {
|
if (create_or_update_user_res.success && target_user_id) {
|
||||||
const delete_user_types = await adminCrudHandler({
|
window.location.reload();
|
||||||
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<BUN_SQLITE_WGUI_USER_TYPES>({
|
|
||||||
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",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log("res", create_or_update_user_res);
|
console.log("res", create_or_update_user_res);
|
||||||
console.log("form", form);
|
console.log("form", form);
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { SiteData } from "./site-data";
|
|||||||
|
|
||||||
export const AppData = {
|
export const AppData = {
|
||||||
HashCost: 12,
|
HashCost: 12,
|
||||||
SSOAuthCookieName: `${SiteData["SiteSlug"]}_sso_auth`,
|
|
||||||
SSOCodeExpiryMinutes: 15,
|
|
||||||
ServerHealthMessage: "Server Running Well",
|
ServerHealthMessage: "Server Running Well",
|
||||||
AuthKeyCookieName: `${SiteData["SiteSlug"]}_auth_key`,
|
AuthKeyCookieName: `${SiteData["SiteSlug"]}_auth_key`,
|
||||||
AuthCSRFCookieName: `${SiteData["SiteSlug"]}_auth_csrf`,
|
AuthCSRFCookieName: `${SiteData["SiteSlug"]}_auth_csrf`,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import grabUsers from "../db/users/grab-users";
|
|||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
import type {
|
import type {
|
||||||
BUN_SQLITE_WGUI_MEDIA,
|
BUN_SQLITE_WGUI_MEDIA,
|
||||||
BUN_SQLITE_WGUI_SSO_LOGIN_CODES,
|
|
||||||
BUN_SQLITE_WGUI_USER_TYPES,
|
BUN_SQLITE_WGUI_USER_TYPES,
|
||||||
BunSQLiteTables,
|
BunSQLiteTables,
|
||||||
} from "@/db/types/db";
|
} 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(
|
console.log(
|
||||||
`User #${new_logged_in_user.id} [${new_logged_in_user.first_name} ${new_logged_in_user.last_name}] logged in successfully.`,
|
`User #${new_logged_in_user.id} [${new_logged_in_user.first_name} ${new_logged_in_user.last_name}] logged in successfully.`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<GoogleIDTokenPayload, "email" | "sub">> &
|
|
||||||
Pick<GoogleIDTokenPayload, "name" | "picture">;
|
|
||||||
|
|
||||||
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<T>(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<GoogleIDTokenUser> {
|
|
||||||
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<GoogleJWTHeader>(encodedHeader);
|
|
||||||
const payload = parseGoogleTokenPart<GoogleIDTokenPayload>(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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -19,24 +19,12 @@ export default function AdminAsideLinks() {
|
|||||||
strict: true,
|
strict: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "People",
|
title: "Clients",
|
||||||
url: "/admin/people",
|
url: "/admin/clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Events",
|
title: "Host",
|
||||||
url: "/admin/events",
|
url: "/admin/host",
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Media",
|
|
||||||
url: "/admin/media",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Board",
|
|
||||||
url: "/admin/board",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Locations",
|
|
||||||
url: "/admin/locations",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: <div className="h-10"></div>,
|
component: <div className="h-10"></div>,
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ const server: BunextPageServerFn<PagePropsType> = async ({ req, url }) => {
|
|||||||
props: {
|
props: {
|
||||||
environment: process.env.NODE_ENV,
|
environment: process.env.NODE_ENV,
|
||||||
envs: {
|
envs: {
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || "",
|
|
||||||
R2_PUBLIC_DOMAIN: process.env.R2_PUBLIC_DOMAIN || "",
|
R2_PUBLIC_DOMAIN: process.env.R2_PUBLIC_DOMAIN || "",
|
||||||
PAYSTACK_PUBLIC_KEY: process.env.PAYSTACK_PUBLIC_KEY || "",
|
PAYSTACK_PUBLIC_KEY: process.env.PAYSTACK_PUBLIC_KEY || "",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,13 +40,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 [
|
const [table, id] = params.query.paths.split("/") as [
|
||||||
TableType,
|
TableType,
|
||||||
string | number,
|
string | number,
|
||||||
@@ -63,10 +56,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
switch (table) {
|
switch (table) {
|
||||||
case "users":
|
|
||||||
return await users(crud_params);
|
|
||||||
case "user_types":
|
|
||||||
return await userTypes(crud_params);
|
|
||||||
case "media":
|
case "media":
|
||||||
return await media(crud_params);
|
return await media(crud_params);
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
|||||||
const { id, ids, media_paradigm, user_id } = body;
|
const { id, ids, media_paradigm, user_id } = body;
|
||||||
|
|
||||||
const can_delete_all_media = checkUserAccess({
|
const can_delete_all_media = checkUserAccess({
|
||||||
includes: ["admin", "board_member"],
|
includes: ["admin"],
|
||||||
user_types,
|
user_types,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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<APIResponseObject> = 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 = (
|
|
||||||
<div>
|
|
||||||
<div>Use this Code to Complete your login</div>
|
|
||||||
<h1>{sso_code}</h1>
|
|
||||||
<div>
|
|
||||||
Code expires in {AppData["SSOCodeExpiryMinutes"]} minutes.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const send_mail = await sendEmail({
|
|
||||||
content: email_component,
|
|
||||||
to:
|
|
||||||
process.env.NODE_ENV == "production"
|
|
||||||
? target_user.email
|
|
||||||
: "[email protected]",
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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<APIResponseObject> = 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
+32
-52
@@ -1,14 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
BUN_SQLITE_WGUI_SSO_LOGIN_CODES,
|
|
||||||
BUN_SQLITE_WGUI_USERS,
|
BUN_SQLITE_WGUI_USERS,
|
||||||
BunSQLiteTables,
|
BunSQLiteTables,
|
||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
import { AppData } from "@/src/data/app-data";
|
|
||||||
import loginUser from "@/src/functions/backend/auth/login-user";
|
import loginUser from "@/src/functions/backend/auth/login-user";
|
||||||
import type { ApiReqParams, SSOAuth } from "@/src/types";
|
import type { ApiReqParams } from "@/src/types";
|
||||||
import { getCookie } from "@/src/utils/cookies";
|
|
||||||
import { decrypt } from "@/src/utils/crypt";
|
|
||||||
import EJSON from "@/src/utils/ejson";
|
|
||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
import type {
|
import type {
|
||||||
APIResponseObject,
|
APIResponseObject,
|
||||||
@@ -16,51 +11,17 @@ import type {
|
|||||||
} from "@moduletrace/bunext/types";
|
} from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||||
req,
|
|
||||||
body,
|
body,
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const { sso_code } = body as ApiReqParams;
|
const { login } = body as ApiReqParams;
|
||||||
|
|
||||||
const sso_cookie = getCookie(req, AppData["SSOAuthCookieName"]);
|
if (!login?.email_or_username) {
|
||||||
|
throw new Error(`Please enter your email or username`);
|
||||||
if (!sso_cookie) {
|
|
||||||
throw new Error(`No SSO session found!`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sso_code) {
|
if (!login?.password) {
|
||||||
throw new Error(`No SSO code sent!`);
|
throw new Error(`Please enter your password`);
|
||||||
}
|
|
||||||
|
|
||||||
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.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const target_user = (
|
const target_user = (
|
||||||
@@ -71,16 +32,35 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
|||||||
table: "users",
|
table: "users",
|
||||||
query: {
|
query: {
|
||||||
query: {
|
query: {
|
||||||
email: decrypted_sso_object.email
|
email: {
|
||||||
? { value: decrypted_sso_object.email }
|
value: login.email_or_username,
|
||||||
: undefined,
|
},
|
||||||
|
username: {
|
||||||
|
value: login.email_or_username,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
searchOperator: "OR",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
).singleRes;
|
)?.singleRes;
|
||||||
|
|
||||||
if (!target_user?.id) {
|
if (!target_user?.id || !target_user.password) {
|
||||||
throw new Error(`This device wasn't used to get this SSO code`);
|
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({
|
return await loginUser({
|
||||||
@@ -92,4 +72,4 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
|||||||
msg: error.message,
|
msg: error.message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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 grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||||
import type { ApiReqParams, TableType } from "@/src/types";
|
import type { ApiReqParams, TableType } from "@/src/types";
|
||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
@@ -6,8 +10,6 @@ import type {
|
|||||||
APIResponseObject,
|
APIResponseObject,
|
||||||
BunextAPIRouteHandler,
|
BunextAPIRouteHandler,
|
||||||
} from "@moduletrace/bunext/types";
|
} from "@moduletrace/bunext/types";
|
||||||
import { hash } from "bun";
|
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
export const handler: BunextAPIRouteHandler<
|
export const handler: BunextAPIRouteHandler<
|
||||||
APIResponseObject<BUN_SQLITE_WGUI_USERS>
|
APIResponseObject<BUN_SQLITE_WGUI_USERS>
|
||||||
@@ -23,26 +25,69 @@ export const handler: BunextAPIRouteHandler<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("users_res", users_res);
|
|
||||||
|
|
||||||
if (users_res.singleRes?.id) {
|
if (users_res.singleRes?.id) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let final_insert_data = [
|
const final_insert_data = await Promise.all(
|
||||||
...(body?.insert_data?.map((data) => {
|
body?.insert_data?.map(async (data) => {
|
||||||
return { ...data, password: hash(data.password || "") };
|
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<BUN_SQLITE_WGUI_USERS, TableType>({
|
const POST = await BunSQLite.insert<BUN_SQLITE_WGUI_USERS, TableType>({
|
||||||
table: "users",
|
table: "users",
|
||||||
data: final_insert_data,
|
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) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -11,19 +11,20 @@ export default async function submitLoginForm({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||||
`/api/auth/get-login-code`,
|
`/api/auth/login`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
login: {
|
login: {
|
||||||
email_or_username: form.username_or_email,
|
email_or_username: form.username_or_email,
|
||||||
|
password: form.password,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
window.location.pathname = `/auth/sso`;
|
window.location.pathname = `/admin`;
|
||||||
} else {
|
} else {
|
||||||
setStatus({
|
setStatus({
|
||||||
error: true,
|
error: true,
|
||||||
@@ -34,4 +35,4 @@ export default async function submitLoginForm({
|
|||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<typeof useFormInit<LoginFormObject>>) {
|
|
||||||
const { pageProps } = useContext(AppContext);
|
|
||||||
|
|
||||||
const googleButtonRef = useRef<HTMLDivElement>(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<ApiReqParams, APIResponseObject>(
|
|
||||||
`/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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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<typeof useFormInit<LoginFormObject>>,
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
loading,
|
|
||||||
handleButtonClick,
|
|
||||||
googleClientId,
|
|
||||||
googleButtonRef,
|
|
||||||
googleReady,
|
|
||||||
} = useGoogleLoginInit(init);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row className="w-full relative justify-center">
|
|
||||||
<Button
|
|
||||||
title="Login with Google"
|
|
||||||
type="button"
|
|
||||||
variant="outlined"
|
|
||||||
color="gray"
|
|
||||||
className="w-full py-3 relative"
|
|
||||||
loading={loading}
|
|
||||||
onClick={handleButtonClick}
|
|
||||||
>
|
|
||||||
<Img
|
|
||||||
alt="Google Logo Icon"
|
|
||||||
src={`/icons/google.png`}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
<Row>
|
|
||||||
<span>Login with Google</span>
|
|
||||||
</Row>
|
|
||||||
</Button>
|
|
||||||
{googleClientId ? (
|
|
||||||
<div
|
|
||||||
ref={googleButtonRef}
|
|
||||||
aria-hidden
|
|
||||||
className={twMerge(
|
|
||||||
"absolute inset-0 z-10 opacity-0",
|
|
||||||
googleReady && !loading
|
|
||||||
? "pointer-events-auto"
|
|
||||||
: "pointer-events-none",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import Form from "@/src/components/twui/form/Form";
|
import Form from "@/src/components/twui/form/Form";
|
||||||
import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
|
import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
|
||||||
import Row from "@/src/components/twui/layout/Row";
|
import Tag from "@/src/components/twui/elements/Tag";
|
||||||
import Divider from "@/src/components/twui/layout/Divider";
|
|
||||||
import Span from "@/src/components/twui/layout/Span";
|
|
||||||
import useFormInit from "@/src/hooks/use-form-init";
|
import useFormInit from "@/src/hooks/use-form-init";
|
||||||
import { type LoginFormObject } from "@/src/types";
|
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 LoginFormEmailUsername from "./login-form-email-username";
|
||||||
|
import LoginFormPassword from "./login-form-password";
|
||||||
import submitLoginForm from "../../(functions)/submit-login-form";
|
import submitLoginForm from "../../(functions)/submit-login-form";
|
||||||
import LoginFormAction from "./login-form-action";
|
import LoginFormAction from "./login-form-action";
|
||||||
|
|
||||||
@@ -17,22 +14,18 @@ export default function LoginForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GoogleLogin {...init} />
|
|
||||||
<Row className="w-full flex-nowrap gap-4 -my-2">
|
|
||||||
<Divider />
|
|
||||||
<Span className="text-sm font-semibold opacity-50">OR</Span>
|
|
||||||
<Divider />
|
|
||||||
</Row>
|
|
||||||
<Form
|
<Form
|
||||||
className="w-full flex items-center justify-center relative gap-4"
|
className="w-full flex items-center justify-center relative gap-4"
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
submitLoginForm(init);
|
submitLoginForm(init);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{status?.error && <Tag color="error">{status.msg}</Tag>}
|
||||||
{loading && <LoadingOverlay />}
|
{loading && <LoadingOverlay />}
|
||||||
<LoginFormEmailUsername {...init} />
|
<LoginFormEmailUsername {...init} />
|
||||||
|
<LoginFormPassword {...init} />
|
||||||
<LoginFormAction {...init} />
|
<LoginFormAction {...init} />
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ export default function LoginFormAction({}: ReturnType<
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button title="Submit Login Form" type="submit" className="w-full">
|
<Button title="Submit Login Form" type="submit" className="w-full">
|
||||||
Get Login Code
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export default function LoginFormEmailUsername({
|
|||||||
}: ReturnType<typeof useFormInit<LoginFormObject>>) {
|
}: ReturnType<typeof useFormInit<LoginFormObject>>) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
|
||||||
placeholder="Email or Username"
|
placeholder="Email or Username"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
|
|||||||
@@ -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<typeof useFormInit<LoginFormObject>>) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
onChange={(e) => {
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
password: e.target.value,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
showLabel
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,21 +1,10 @@
|
|||||||
import Stack from "@/src/components/twui/layout/Stack";
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
import LoginForm from "../(partials)/login-form";
|
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() {
|
export default function LoginFormSection() {
|
||||||
return (
|
return (
|
||||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
<Stack className="gap-2 items-center">
|
|
||||||
<Span variant="faded">Received an SSO code already?</Span>
|
|
||||||
<ArrowedLink
|
|
||||||
link={{
|
|
||||||
title: "Complete Login",
|
|
||||||
url: "/auth/sso",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -15,10 +15,6 @@ export const server: BunextPageServerFn = async ({ req }) => {
|
|||||||
name: AppData["AuthCSRFCookieName"],
|
name: AppData["AuthCSRFCookieName"],
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: AppData["SSOAuthCookieName"],
|
|
||||||
httpOnly: true,
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
return new_res;
|
return new_res;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,43 +1,25 @@
|
|||||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||||
import Section from "@/src/components/twui/layout/Section";
|
|
||||||
import Loading from "@/src/components/twui/elements/Loading";
|
import Loading from "@/src/components/twui/elements/Loading";
|
||||||
import Row from "@/src/components/twui/layout/Row";
|
import Row from "@/src/components/twui/layout/Row";
|
||||||
import Span from "@/src/components/twui/layout/Span";
|
import Span from "@/src/components/twui/layout/Span";
|
||||||
import { useEffect } from "react";
|
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 { SiteData } from "@/src/data/site-data";
|
||||||
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
|
|
||||||
export default function LogoutPage() {
|
export default function LogoutPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.pathname = "/";
|
window.location.pathname = "/";
|
||||||
}, 1000);
|
}, 2000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section
|
<Stack className="items-center">
|
||||||
className={twMerge(
|
<Row>
|
||||||
"h-screen w-screen px-0 items-center justify-center",
|
<Loading />
|
||||||
)}
|
<Span>Logging out ...</Span>
|
||||||
>
|
</Row>
|
||||||
<Center>
|
</Stack>
|
||||||
<Paper className="w-auto p-10 items-center">
|
|
||||||
<Logo
|
|
||||||
text_props={{
|
|
||||||
className: "text-dark",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Row>
|
|
||||||
<Loading />
|
|
||||||
<Span>Logging out ...</Span>
|
|
||||||
</Row>
|
|
||||||
</Paper>
|
|
||||||
</Center>
|
|
||||||
</Section>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<SSOFormObject>();
|
|
||||||
const { form, loading, setForm, setLoading, status, setStatus } = init;
|
|
||||||
|
|
||||||
const submitSSOLoginForm = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
|
||||||
`/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 (
|
|
||||||
<>
|
|
||||||
<LoginShell status={status}>
|
|
||||||
<Form
|
|
||||||
className="w-full flex items-center justify-center relative gap-4"
|
|
||||||
onSubmit={submitSSOLoginForm}
|
|
||||||
>
|
|
||||||
{loading && <LoadingOverlay />}
|
|
||||||
|
|
||||||
<Input
|
|
||||||
placeholder="SSO Code"
|
|
||||||
onChange={(e) => {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
code: e.target.value,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
showLabel
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
title="Submit Login Form"
|
|
||||||
type="submit"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Complete Login
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</LoginShell>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<Section className="hero-section">
|
|
||||||
<Container className="relative z-10">
|
|
||||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
|
||||||
<Span className="htag">SSO Login</Span>
|
|
||||||
<H1 className="text-center font-bold">
|
|
||||||
Complete your login
|
|
||||||
</H1>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<Section className="py-0!">
|
|
||||||
<Container className="relative z-10 py-20 -mt-28">
|
|
||||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
|
||||||
<SSOForm />
|
|
||||||
<Stack className="gap-2 items-center">
|
|
||||||
<Span variant="faded">
|
|
||||||
Login Failed or Code Expired?
|
|
||||||
</Span>
|
|
||||||
<ArrowedLink
|
|
||||||
link={{
|
|
||||||
title: "Login Again",
|
|
||||||
url: "/auth/login",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<>
|
|
||||||
<Hero />
|
|
||||||
<SSOFormSection />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const meta: BunextPageModuleMeta = {
|
|
||||||
title: `Sign In | ${SiteData["SiteName"]}`,
|
|
||||||
description: `Sign in to your account to access the admin dashboard.`,
|
|
||||||
};
|
|
||||||
+1
-34
@@ -35,7 +35,6 @@ export type PagePropsType = {
|
|||||||
persons?: BUN_SQLITE_WGUI_USERS_JOIN[] | null;
|
persons?: BUN_SQLITE_WGUI_USERS_JOIN[] | null;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
envs?: {
|
envs?: {
|
||||||
GOOGLE_CLIENT_ID?: string;
|
|
||||||
R2_PUBLIC_DOMAIN?: string;
|
R2_PUBLIC_DOMAIN?: string;
|
||||||
PAYSTACK_PUBLIC_KEY?: string;
|
PAYSTACK_PUBLIC_KEY?: string;
|
||||||
};
|
};
|
||||||
@@ -157,10 +156,7 @@ export type UserType = (typeof UserTypes)[number]["value"];
|
|||||||
|
|
||||||
export type LoginFormObject = {
|
export type LoginFormObject = {
|
||||||
username_or_email?: string;
|
username_or_email?: string;
|
||||||
};
|
password?: string;
|
||||||
|
|
||||||
export type SSOFormObject = {
|
|
||||||
code?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContactFormObject = {
|
export type ContactFormObject = {
|
||||||
@@ -178,8 +174,6 @@ export type ApiReqParams<
|
|||||||
email_or_username?: string;
|
email_or_username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
google_token?: string;
|
|
||||||
sso_code?: string;
|
|
||||||
contact?: {
|
contact?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
@@ -228,33 +222,6 @@ export type ApiReqParams<
|
|||||||
media?: BUN_SQLITE_WGUI_MEDIA;
|
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<string, string | number | boolean>,
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSOAuth = {
|
|
||||||
user_id: number;
|
|
||||||
sso_code: string | number;
|
|
||||||
email?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UserAuthReturn = {
|
export type UserAuthReturn = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
msg?: string;
|
msg?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user