mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 04:21:52 +00:00
Merge pull request #65 from prowler-cloud/PRWLR-4883-Integrate-authentication-endpoint-OSS
Integrate authentication endpoint oss
This commit is contained in:
@@ -1,55 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { AuthError } from "next-auth";
|
||||
import { z } from "zod";
|
||||
|
||||
import { signIn, signOut } from "@/auth.config";
|
||||
import { authFormSchema } from "@/types";
|
||||
|
||||
const formSchemaSignIn = authFormSchema("sign-in");
|
||||
|
||||
const defaultValues: z.infer<typeof formSchemaSignIn> = {
|
||||
email: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
export async function authenticate(
|
||||
prevState: unknown,
|
||||
formData: z.infer<typeof formSchemaSignIn>,
|
||||
) {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await signIn("credentials", {
|
||||
...formData,
|
||||
redirect: false,
|
||||
});
|
||||
return {
|
||||
message: "Success",
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
switch (error.type) {
|
||||
case "CredentialsSignin":
|
||||
return {
|
||||
message: "Credentials error",
|
||||
errors: {
|
||||
...defaultValues,
|
||||
credentials: "Incorrect email or password",
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
message: "Unknown error",
|
||||
errors: {
|
||||
...defaultValues,
|
||||
unknown: "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function logOut() {
|
||||
await signOut();
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
"use server";
|
||||
|
||||
import { AuthError } from "next-auth";
|
||||
import { z } from "zod";
|
||||
|
||||
import { signIn, signOut } from "@/auth.config";
|
||||
import { authFormSchema } from "@/types";
|
||||
|
||||
const formSchemaSignIn = authFormSchema("sign-in");
|
||||
const formSchemaSignUp = authFormSchema("sign-up");
|
||||
|
||||
const defaultValues: z.infer<typeof formSchemaSignIn> = {
|
||||
email: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
export async function authenticate(
|
||||
prevState: unknown,
|
||||
formData: z.infer<typeof formSchemaSignIn>,
|
||||
) {
|
||||
try {
|
||||
await signIn("credentials", {
|
||||
...formData,
|
||||
redirect: false,
|
||||
});
|
||||
return {
|
||||
message: "Success",
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
switch (error.type) {
|
||||
case "CredentialsSignin":
|
||||
return {
|
||||
message: "Credentials error",
|
||||
errors: {
|
||||
...defaultValues,
|
||||
credentials: "Incorrect email or password",
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
message: "Unknown error",
|
||||
errors: {
|
||||
...defaultValues,
|
||||
unknown: "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createNewUser = async (
|
||||
formData: z.infer<typeof formSchemaSignUp>,
|
||||
) => {
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
const url = new URL(`${keyServer}/users`);
|
||||
|
||||
const bodyData = {
|
||||
data: {
|
||||
type: "User",
|
||||
attributes: {
|
||||
name: formData.name,
|
||||
company_name: formData.company,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.api+json",
|
||||
Accept: "application/vnd.api+json",
|
||||
},
|
||||
body: JSON.stringify(bodyData),
|
||||
});
|
||||
|
||||
const parsedResponse = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return parsedResponse;
|
||||
}
|
||||
|
||||
return parsedResponse;
|
||||
} catch (error) {
|
||||
return { errors: [{ detail: "Network error or server is unreachable" }] };
|
||||
}
|
||||
};
|
||||
|
||||
export const getToken = async (formData: z.infer<typeof formSchemaSignIn>) => {
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
const url = new URL(`${keyServer}/tokens`);
|
||||
|
||||
const bodyData = {
|
||||
data: {
|
||||
type: "Token",
|
||||
attributes: {
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.api+json",
|
||||
Accept: "application/vnd.api+json",
|
||||
},
|
||||
body: JSON.stringify(bodyData),
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const parsedResponse = await response.json();
|
||||
|
||||
const accessToken = parsedResponse.data.attributes.access;
|
||||
const refreshToken = parsedResponse.data.attributes.refresh;
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error("Error in trying to get token");
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserByMe = async (accessToken: string) => {
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
const url = new URL(`${keyServer}/users/me`);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/vnd.api+json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Error in trying to get user by me");
|
||||
|
||||
const parsedResponse = await response.json();
|
||||
|
||||
const name = parsedResponse.data.attributes.name;
|
||||
const email = parsedResponse.data.attributes.email;
|
||||
const company = parsedResponse.data.attributes.company_name;
|
||||
const dateJoined = parsedResponse.data.attributes.date_joined;
|
||||
return {
|
||||
name,
|
||||
email,
|
||||
company,
|
||||
dateJoined,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error("Error in trying to get user by me");
|
||||
}
|
||||
};
|
||||
|
||||
export async function logOut() {
|
||||
await signOut();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./auth";
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./auth";
|
||||
export * from "./providers";
|
||||
@@ -6,10 +6,6 @@ import { redirect } from "next/navigation";
|
||||
import { auth } from "@/auth.config";
|
||||
import { parseStringify } from "@/lib";
|
||||
|
||||
// Credentials for basic auth
|
||||
const username = process.env.API_USERNAME;
|
||||
const password = process.env.API_PASSWORD;
|
||||
|
||||
export const getProviders = async ({
|
||||
page = 1,
|
||||
query = "",
|
||||
@@ -17,12 +13,11 @@ export const getProviders = async ({
|
||||
filters = {},
|
||||
}) => {
|
||||
const session = await auth();
|
||||
const tenantId = session?.user.tenantId;
|
||||
|
||||
if (isNaN(Number(page)) || page < 1) redirect("/providers");
|
||||
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
const url = new URL(`${keyServer}/providers?sort=-inserted_at`);
|
||||
const url = new URL(`${keyServer}/providers`);
|
||||
|
||||
if (page) url.searchParams.append("page[number]", page.toString());
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
@@ -38,8 +33,8 @@ export const getProviders = async ({
|
||||
try {
|
||||
const providers = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"X-Tenant-ID": `${tenantId}`,
|
||||
Authorization: "Basic " + btoa(`${username}:${password}`),
|
||||
Accept: "application/vnd.api+json",
|
||||
Authorization: `Bearer ${session?.accessToken}`,
|
||||
},
|
||||
});
|
||||
const data = await providers.json();
|
||||
@@ -54,7 +49,6 @@ export const getProviders = async ({
|
||||
|
||||
export const getProvider = async (formData: FormData) => {
|
||||
const session = await auth();
|
||||
const tenantId = session?.user.tenantId;
|
||||
const providerId = formData.get("id");
|
||||
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
@@ -63,8 +57,8 @@ export const getProvider = async (formData: FormData) => {
|
||||
try {
|
||||
const providers = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"X-Tenant-ID": `${tenantId}`,
|
||||
Authorization: "Basic " + btoa(`${username}:${password}`),
|
||||
Accept: "application/vnd.api+json",
|
||||
Authorization: `Bearer ${session?.accessToken}`,
|
||||
},
|
||||
});
|
||||
const data = await providers.json();
|
||||
@@ -81,7 +75,6 @@ export const updateProvider = async (formData: FormData) => {
|
||||
const session = await auth();
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
|
||||
const tenantId = session?.user.tenantId;
|
||||
const providerId = formData.get("providerId");
|
||||
const providerAlias = formData.get("alias");
|
||||
|
||||
@@ -91,9 +84,9 @@ export const updateProvider = async (formData: FormData) => {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-Tenant-ID": `${tenantId}`,
|
||||
Authorization: "Basic " + btoa(`${username}:${password}`),
|
||||
"Content-Type": "application/vnd.api+json",
|
||||
Accept: "application/vnd.api+json",
|
||||
Authorization: `Bearer ${session?.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
@@ -120,8 +113,6 @@ export const addProvider = async (formData: FormData) => {
|
||||
const session = await auth();
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
|
||||
const tenantId = session?.user.tenantId;
|
||||
|
||||
const providerType = formData.get("providerType");
|
||||
const providerId = formData.get("providerId");
|
||||
const providerAlias = formData.get("providerAlias");
|
||||
@@ -132,9 +123,9 @@ export const addProvider = async (formData: FormData) => {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Tenant-ID": `${tenantId}`,
|
||||
Authorization: "Basic " + btoa(`${username}:${password}`),
|
||||
"Content-Type": "application/vnd.api+json",
|
||||
Accept: "application/vnd.api+json",
|
||||
Authorization: `Bearer ${session?.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
@@ -159,9 +150,8 @@ export const addProvider = async (formData: FormData) => {
|
||||
};
|
||||
|
||||
export const checkConnectionProvider = async (formData: FormData) => {
|
||||
const session = await auth();
|
||||
// const session = await auth();
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
const tenantId = session?.user.tenantId;
|
||||
|
||||
const providerId = formData.get("id");
|
||||
|
||||
@@ -171,8 +161,7 @@ export const checkConnectionProvider = async (formData: FormData) => {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Tenant-ID": `${tenantId}`,
|
||||
Authorization: "Basic " + btoa(`${username}:${password}`),
|
||||
Accept: "application/vnd.api+json",
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -188,7 +177,6 @@ export const checkConnectionProvider = async (formData: FormData) => {
|
||||
export const deleteProvider = async (formData: FormData) => {
|
||||
const session = await auth();
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
const tenantId = session?.user.tenantId;
|
||||
|
||||
const providerId = formData.get("id");
|
||||
const url = new URL(`${keyServer}/providers/${providerId}`);
|
||||
@@ -197,8 +185,8 @@ export const deleteProvider = async (formData: FormData) => {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-Tenant-ID": `${tenantId}`,
|
||||
Authorization: "Basic " + btoa(`${username}:${password}`),
|
||||
Accept: "application/vnd.api+json",
|
||||
Authorization: `Bearer ${session?.accessToken}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -211,7 +199,7 @@ export const deleteProvider = async (formData: FormData) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getErrorMessage = (error: unknown): string => {
|
||||
export const getErrorMessage = async (error: unknown): Promise<string> => {
|
||||
let message: string;
|
||||
|
||||
if (error instanceof Error) {
|
||||
@@ -1,6 +1,4 @@
|
||||
import React from "react";
|
||||
|
||||
import { AuthForm } from "@/components/auth";
|
||||
import { AuthForm } from "@/components/auth/oss";
|
||||
|
||||
const SignIn = () => {
|
||||
return <AuthForm type="sign-in" />;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React from "react";
|
||||
|
||||
import { AuthForm } from "@/components/auth";
|
||||
import { AuthForm } from "@/components/auth/oss";
|
||||
|
||||
const SignUp = () => {
|
||||
return <AuthForm type="sign-up" />;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Spacer } from "@nextui-org/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
// import { getUserByMe } from "@/actions/auth/auth";
|
||||
import { auth } from "@/auth.config";
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
@@ -13,12 +14,17 @@ export default async function Profile() {
|
||||
redirect("/sign-in");
|
||||
}
|
||||
|
||||
// const user = await getUserByMe();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="User Profile" icon="ci:users" />
|
||||
<Spacer y={4} />
|
||||
<Spacer y={6} />
|
||||
<pre>{JSON.stringify(session.user, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(session.userId, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(session.tenantId, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getProviders } from "@/actions";
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { FilterControls, filtersProviders } from "@/components/filters";
|
||||
import { AddProvider } from "@/components/providers";
|
||||
import {
|
||||
@@ -24,7 +24,7 @@ export default async function Providers({
|
||||
<Header title="Providers" icon="fluent:cloud-sync-24-regular" />
|
||||
|
||||
<Spacer y={4} />
|
||||
<FilterControls search providers date customFilters={filtersProviders} />
|
||||
<FilterControls search providers customFilters={filtersProviders} />
|
||||
<Spacer y={4} />
|
||||
|
||||
<AddProvider />
|
||||
@@ -43,7 +43,7 @@ const SSRDataTable = async ({
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
||||
const sort = searchParams.sort?.toString() || "";
|
||||
const sort = searchParams.sort?.toString() || "-inserted_at";
|
||||
|
||||
// Extract all filter parameters
|
||||
const filters = Object.fromEntries(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getUsers } from "@/actions/users";
|
||||
import { auth } from "@/auth.config";
|
||||
// import { auth } from "@/auth.config";
|
||||
import { Header } from "@/components/ui";
|
||||
import {
|
||||
AddUserModal,
|
||||
@@ -18,11 +18,6 @@ export default async function Users({
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const session = await auth();
|
||||
if (session?.user?.role !== "admin") {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import data from "../../../dataProviders.json";
|
||||
|
||||
export async function GET() {
|
||||
// Simulate fetching data with a delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
return NextResponse.json({ providers: data });
|
||||
}
|
||||
+149
-51
@@ -1,61 +1,68 @@
|
||||
import bcryptjs from "bcryptjs";
|
||||
import NextAuth, { type NextAuthConfig } from "next-auth";
|
||||
import { jwtDecode, JwtPayload } from "jwt-decode";
|
||||
import NextAuth, { type NextAuthConfig, User } from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { z } from "zod";
|
||||
|
||||
import { userMockData } from "./lib";
|
||||
import { getToken, getUserByMe } from "./actions/auth";
|
||||
|
||||
async function getUser(email: string, password: string): Promise<any | null> {
|
||||
// Check if the user exists in the userMockData array.
|
||||
const user = userMockData.find((user) => user.email === email);
|
||||
if (!user) return null;
|
||||
|
||||
if (!bcryptjs.compareSync(password, user.password)) return null;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
tenantId: user.tenantId,
|
||||
name: user.name,
|
||||
companyName: user.companyName,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
image: user.image,
|
||||
};
|
||||
interface CustomJwtPayload extends JwtPayload {
|
||||
user_id: string;
|
||||
tenant_id: string;
|
||||
}
|
||||
|
||||
const refreshAccessToken = async (token: JwtPayload) => {
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
const url = new URL(`${keyServer}/tokens/refresh`);
|
||||
|
||||
const bodyData = {
|
||||
data: {
|
||||
type: "TokenRefresh",
|
||||
attributes: {
|
||||
refresh: (token as any).refreshToken,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.api+json",
|
||||
Accept: "application/vnd.api+json",
|
||||
},
|
||||
body: JSON.stringify(bodyData),
|
||||
});
|
||||
const newTokens = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
accessToken: newTokens.data.attributes.access,
|
||||
refreshToken: newTokens.data.attributes.refresh,
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error refreshing access token:", error);
|
||||
return {
|
||||
error: "RefreshAccessTokenError",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const authConfig = {
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
// The session will be valid for 24 hours
|
||||
maxAge: 24 * 60 * 60,
|
||||
},
|
||||
pages: {
|
||||
signIn: "/sign-in",
|
||||
newUser: "/sign-up",
|
||||
},
|
||||
callbacks: {
|
||||
authorized({ auth, request: { nextUrl } }) {
|
||||
const isLoggedIn = !!auth?.user;
|
||||
const isOnDashboard = nextUrl.pathname.startsWith("/");
|
||||
if (isOnDashboard) {
|
||||
if (isLoggedIn) return true;
|
||||
return false; // Redirect unauthenticated users to login page
|
||||
} else if (isLoggedIn) {
|
||||
return Response.redirect(new URL("/", nextUrl));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.data = user;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
|
||||
session({ session, token }) {
|
||||
session.user = token.data as any;
|
||||
return session;
|
||||
},
|
||||
},
|
||||
providers: [
|
||||
Credentials({
|
||||
name: "credentials",
|
||||
@@ -67,21 +74,112 @@ export const authConfig = {
|
||||
const parsedCredentials = z
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
password: z.string().min(12),
|
||||
})
|
||||
.safeParse(credentials);
|
||||
|
||||
if (!parsedCredentials.success) {
|
||||
return null;
|
||||
}
|
||||
const { email, password } = parsedCredentials.data;
|
||||
if (!parsedCredentials.success) return null;
|
||||
|
||||
const user = await getUser(email, password);
|
||||
if (!user) return null;
|
||||
return user;
|
||||
const tokenResponse = await getToken(parsedCredentials.data);
|
||||
if (!tokenResponse) return null;
|
||||
|
||||
const userMeResponse = await getUserByMe(tokenResponse.accessToken);
|
||||
|
||||
const user = {
|
||||
name: userMeResponse.name,
|
||||
email: userMeResponse.email,
|
||||
company: userMeResponse?.company,
|
||||
dateJoined: userMeResponse.dateJoined,
|
||||
};
|
||||
|
||||
return {
|
||||
...user,
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
authorized({ auth, request: { nextUrl } }) {
|
||||
const isLoggedIn = !!auth?.user;
|
||||
const isOnDashboard = nextUrl.pathname.startsWith("/");
|
||||
const isSignUpPage = nextUrl.pathname === "/sign-up";
|
||||
|
||||
// Allow access to sign-up page
|
||||
if (isSignUpPage) return true;
|
||||
|
||||
if (isOnDashboard) {
|
||||
if (isLoggedIn) return true;
|
||||
return false; // Redirect users who are not logged in to the login page
|
||||
} else if (isLoggedIn) {
|
||||
return Response.redirect(new URL("/", nextUrl));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
jwt: async ({ token, account, user }) => {
|
||||
if (token?.accessToken) {
|
||||
const decodedToken = jwtDecode(
|
||||
token.accessToken as string,
|
||||
) as CustomJwtPayload;
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log("decodedToken", decodedToken);
|
||||
token.accessTokenExpires = (decodedToken.exp as number) * 1000;
|
||||
token.user_id = decodedToken.user_id;
|
||||
token.tenant_id = decodedToken.tenant_id;
|
||||
}
|
||||
|
||||
const userInfo = {
|
||||
name: user?.name,
|
||||
companyName: user?.company,
|
||||
email: user?.email,
|
||||
dateJoined: user?.dateJoined,
|
||||
};
|
||||
|
||||
if (account && user) {
|
||||
return {
|
||||
...token,
|
||||
userId: token.user_id,
|
||||
tenantId: token.tenant_id,
|
||||
accessToken: (user as User & { accessToken: JwtPayload }).accessToken,
|
||||
refreshToken: (user as User & { refreshToken: JwtPayload })
|
||||
.refreshToken,
|
||||
user: userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log(
|
||||
// "Access token expires",
|
||||
// token.accessTokenExpires,
|
||||
// new Date(Number(token.accessTokenExpires)),
|
||||
// );
|
||||
|
||||
// If the access token is not expired, return the token
|
||||
if (
|
||||
typeof token.accessTokenExpires === "number" &&
|
||||
Date.now() < token.accessTokenExpires
|
||||
)
|
||||
return token;
|
||||
|
||||
// If the access token is expired, try to refresh it
|
||||
return refreshAccessToken(token as JwtPayload);
|
||||
},
|
||||
|
||||
session: async ({ session, token }) => {
|
||||
if (token) {
|
||||
session.userId = token?.user_id as string;
|
||||
session.tenantId = token?.tenant_id as string;
|
||||
session.accessToken = token?.accessToken as string;
|
||||
session.refreshToken = token?.refreshToken as string;
|
||||
session.user = token.user as any;
|
||||
}
|
||||
|
||||
// console.log("session", session);
|
||||
return session;
|
||||
},
|
||||
},
|
||||
} satisfies NextAuthConfig;
|
||||
|
||||
export const { signIn, signOut, auth, handlers } = NextAuth(authConfig);
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button, CircularProgress } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
|
||||
export const AuthButton = ({ type }: { type: string }) => {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button color="primary" type="submit" aria-disabled={pending}>
|
||||
{pending ? (
|
||||
<CircularProgress aria-label="Loading..." size="sm" />
|
||||
) : type === "sign-in" ? (
|
||||
"Log In"
|
||||
) : (
|
||||
"Sign Up"
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,252 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { Button, Checkbox, Divider, Link } from "@nextui-org/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { authenticate } from "@/actions";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { authFormSchema } from "@/types";
|
||||
|
||||
import { NotificationIcon, ProwlerExtended } from "../icons";
|
||||
import { ThemeSwitch } from "../ThemeSwitch";
|
||||
import { CustomInput } from "../ui/custom";
|
||||
import { AuthButton } from "./AuthButton";
|
||||
|
||||
export const AuthForm = ({ type }: { type: string }) => {
|
||||
const formSchema = authFormSchema(type);
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const [state, dispatch] = useFormState(authenticate, undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.message === "Success") {
|
||||
router.push("/");
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||
// Do something with the form values
|
||||
// this will be type-safe and validated
|
||||
try {
|
||||
// Sign-up logic will be here.
|
||||
if (type === "sign-in") {
|
||||
dispatch({
|
||||
email: data.email.toLowerCase(),
|
||||
password: data.password,
|
||||
});
|
||||
}
|
||||
// if (type === "sign-up") {
|
||||
// const newUser = await signUp(data);
|
||||
// }
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-screen w-screen items-center justify-start overflow-hidden rounded-small bg-content1 p-2 sm:p-4 lg:p-8"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(https://nextuipro.nyc3.cdn.digitaloceanspaces.com/components-images/black-background-texture-2.jpg)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
{/* Brand Logo and ThemeSwitch */}
|
||||
<div className="absolute right-10 top-10">
|
||||
<div className="flex items-center gap-4 self-center">
|
||||
<ThemeSwitch aria-label="Toggle theme" />
|
||||
<ProwlerExtended />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonial */}
|
||||
<div className="absolute bottom-10 right-10 hidden md:block">
|
||||
<p className="text-md max-w-xl text-right text-white/60">
|
||||
<span className="font-medium">“</span>
|
||||
Open Cloud Security
|
||||
<span className="font-medium">”</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full max-w-sm flex-col gap-4 rounded-large bg-content1 px-8 pb-10 pt-6 shadow-small">
|
||||
<p className="pb-2 text-xl font-medium">
|
||||
{type === "sign-in" ? "Sign In" : "Sign Up"}
|
||||
</p>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-3"
|
||||
// Fix TS types.
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
{type === "sign-up" && (
|
||||
<>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder="Enter your name"
|
||||
/>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="companyName"
|
||||
type="text"
|
||||
label="Company Name"
|
||||
placeholder="Enter your company name"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
|
||||
<CustomInput control={form.control} name="password" password />
|
||||
|
||||
{type === "sign-in" && (
|
||||
<div className="flex items-center justify-between px-1 py-2">
|
||||
<Checkbox name="remember" size="sm">
|
||||
Remember me
|
||||
</Checkbox>
|
||||
<Link className="text-default-500" href="#">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{/* <Input
|
||||
isRequired
|
||||
endContent={
|
||||
<button type="button" onClick={toggleConfirmVisibility}>
|
||||
{isConfirmVisible ? (
|
||||
<Icon
|
||||
className="pointer-events-none text-2xl text-default-400"
|
||||
icon="solar:eye-closed-linear"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
className="pointer-events-none text-2xl text-default-400"
|
||||
icon="solar:eye-bold"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
label="Confirm Password"
|
||||
name="confirmPassword"
|
||||
placeholder="Confirm your password"
|
||||
type={isConfirmVisible ? "text" : "password"}
|
||||
variant="bordered"
|
||||
/> */}
|
||||
{type === "sign-up" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="termsAndConditions"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
isRequired
|
||||
className="py-4"
|
||||
size="sm"
|
||||
checked={field.value === "true"}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.checked ? "true" : "false")
|
||||
}
|
||||
>
|
||||
I agree with the
|
||||
<Link href="#" size="sm">
|
||||
Terms
|
||||
</Link>
|
||||
and
|
||||
<Link href="#" size="sm">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
<FormMessage className="text-system-error dark:text-system-error" />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state?.message === "Credentials error" && (
|
||||
<div className="flex flex-row items-center gap-2 text-system-error">
|
||||
<NotificationIcon size={16} />
|
||||
<p className="text-s">Incorrect email or password</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AuthButton type={type} />
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{type === "sign-in" && (
|
||||
<>
|
||||
<div className="flex items-center gap-4 py-2">
|
||||
<Divider className="flex-1" />
|
||||
<p className="shrink-0 text-tiny text-default-500">OR</p>
|
||||
<Divider className="flex-1" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
startContent={
|
||||
<Icon icon="flat-color-icons:google" width={24} />
|
||||
}
|
||||
variant="bordered"
|
||||
>
|
||||
Continue with Google
|
||||
</Button>
|
||||
<Button
|
||||
startContent={
|
||||
<Icon
|
||||
className="text-default-500"
|
||||
icon="fe:github"
|
||||
width={24}
|
||||
/>
|
||||
}
|
||||
variant="bordered"
|
||||
>
|
||||
Continue with Github
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{type === "sign-in" ? (
|
||||
<p className="text-center text-small">
|
||||
Need to create an account?
|
||||
<Link href="/sign-up">Sign Up</Link>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-center text-small">
|
||||
Already have an account?
|
||||
<Link href="/sign-in">Log In</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./AuthButton";
|
||||
export * from "./AuthForm";
|
||||
@@ -0,0 +1,294 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { Button, Checkbox, Divider, Link } from "@nextui-org/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { authenticate, createNewUser } from "@/actions/auth";
|
||||
import { NotificationIcon, ProwlerExtended } from "@/components/icons";
|
||||
import { ThemeSwitch } from "@/components/ThemeSwitch";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { ApiError, authFormSchema } from "@/types";
|
||||
|
||||
export const AuthForm = ({ type }: { type: string }) => {
|
||||
const formSchema = authFormSchema(type);
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
...(type === "sign-up" && {
|
||||
name: "",
|
||||
company: "",
|
||||
termsAndConditions: false,
|
||||
confirmPassword: "",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const [state, dispatch] = useFormState(authenticate, undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.message === "Success") {
|
||||
router.push("/");
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||
if (type === "sign-in") {
|
||||
setIsLoading(true);
|
||||
dispatch({
|
||||
email: data.email.toLowerCase(),
|
||||
password: data.password,
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
if (type === "sign-up") {
|
||||
setIsLoading(true);
|
||||
const newUser = await createNewUser(data);
|
||||
setIsLoading(false);
|
||||
|
||||
if (!newUser.errors) {
|
||||
router.push("/sign-in");
|
||||
}
|
||||
|
||||
if (newUser?.errors && newUser.errors.length > 0) {
|
||||
newUser.errors.forEach((error: ApiError) => {
|
||||
const errorMessage = error.detail;
|
||||
|
||||
switch (error.source.pointer) {
|
||||
case "/data/attributes/name":
|
||||
form.setError("name", { type: "server", message: errorMessage });
|
||||
break;
|
||||
case "/data/attributes/email":
|
||||
form.setError("email", { type: "server", message: errorMessage });
|
||||
break;
|
||||
case "/data/attributes/password":
|
||||
form.setError("password", {
|
||||
type: "server",
|
||||
message: errorMessage,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Oops! Something went wrong",
|
||||
description: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Success!",
|
||||
description: "The user was registered successfully.",
|
||||
});
|
||||
form.reset({
|
||||
name: "",
|
||||
company: "",
|
||||
email: "",
|
||||
password: "",
|
||||
termsAndConditions: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen w-screen">
|
||||
{/* Auth Form */}
|
||||
<div className="relative flex w-full items-center justify-center bg-background lg:w-full">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute h-full w-full bg-[radial-gradient(#94a3b8_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_10%,transparent_80%)]"></div>
|
||||
|
||||
<div className="dark:bg-prowler-black-900/90 relative z-10 flex w-full max-w-sm flex-col gap-4 rounded-large bg-white/90 px-8 py-10 shadow-small md:max-w-md">
|
||||
{/* Prowler Logo */}
|
||||
<div className="absolute -top-[100px] left-1/2 z-10 flex h-fit w-fit -translate-x-1/2">
|
||||
<ProwlerExtended width={300} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="pb-2 text-xl font-medium">
|
||||
{type === "sign-in" ? "Sign In" : "Sign Up"}
|
||||
</p>
|
||||
<ThemeSwitch aria-label="Toggle theme" />
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-3"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
{type === "sign-up" && (
|
||||
<>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="name"
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder="Enter your name"
|
||||
isInvalid={!!form.formState.errors.name}
|
||||
/>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="company"
|
||||
type="text"
|
||||
label="Company Name"
|
||||
placeholder="Enter your company name"
|
||||
isRequired={false}
|
||||
isInvalid={!!form.formState.errors.company}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="Enter your email"
|
||||
isInvalid={!!form.formState.errors.email}
|
||||
/>
|
||||
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="password"
|
||||
password
|
||||
isInvalid={!!form.formState.errors.password}
|
||||
/>
|
||||
|
||||
{type === "sign-in" && (
|
||||
<div className="flex items-center justify-between px-1 py-2">
|
||||
<Checkbox name="remember" size="sm">
|
||||
Remember me
|
||||
</Checkbox>
|
||||
<Link className="text-default-500" href="#">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{type === "sign-up" && (
|
||||
<>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
confirmPassword
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="termsAndConditions"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
isRequired
|
||||
className="py-4"
|
||||
size="sm"
|
||||
checked={field.value}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
>
|
||||
I agree with the
|
||||
<Link href="#" size="sm">
|
||||
Terms
|
||||
</Link>
|
||||
and
|
||||
<Link href="#" size="sm">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
<FormMessage className="text-system-error dark:text-system-error" />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state?.message === "Credentials error" && (
|
||||
<div className="flex flex-row items-center gap-2 text-system-error">
|
||||
<NotificationIcon size={16} />
|
||||
<p className="text-s">No user found</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel={type === "sign-in" ? "Log In" : "Sign Up"}
|
||||
ariaDisabled={isLoading}
|
||||
className="w-full"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="md"
|
||||
radius="md"
|
||||
isLoading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span>Loading</span>
|
||||
) : (
|
||||
<span>{type === "sign-in" ? "Log In" : "Sign Up"}</span>
|
||||
)}
|
||||
</CustomButton>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{type === "sign-in" && (
|
||||
<>
|
||||
<div className="flex items-center gap-4 py-2">
|
||||
<Divider className="flex-1" />
|
||||
<p className="shrink-0 text-tiny text-default-500">OR</p>
|
||||
<Divider className="flex-1" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
startContent={
|
||||
<Icon icon="flat-color-icons:google" width={24} />
|
||||
}
|
||||
variant="bordered"
|
||||
>
|
||||
Continue with Google
|
||||
</Button>
|
||||
<Button
|
||||
startContent={
|
||||
<Icon
|
||||
className="text-default-500"
|
||||
icon="fe:github"
|
||||
width={24}
|
||||
/>
|
||||
}
|
||||
variant="bordered"
|
||||
>
|
||||
Continue with Github
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{type === "sign-in" ? (
|
||||
<p className="text-center text-small">
|
||||
Need to create an account?
|
||||
<Link href="/sign-up">Sign Up</Link>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-center text-small">
|
||||
Already have an account?
|
||||
<Link href="/sign-in">Log In</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./auth-form";
|
||||
@@ -1,9 +1,4 @@
|
||||
export const filtersProviders = [
|
||||
{
|
||||
key: "provider__in",
|
||||
labelCheckboxGroup: "Select a Provider",
|
||||
values: ["aws", "gcp", "azure", "kubernetes"],
|
||||
},
|
||||
{
|
||||
key: "connected",
|
||||
labelCheckboxGroup: "Connection",
|
||||
|
||||
@@ -7,26 +7,27 @@ export const ProwlerExtended: React.FC<IconSvgProps> = ({
|
||||
width = 164,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1233.67 204.4"
|
||||
fill="none"
|
||||
height={size || height}
|
||||
width={size || width}
|
||||
color="evenodd"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
className="cls-1"
|
||||
d="M1169.38,132.04c20.76-12.21,34.44-34.9,34.44-59.79,0-38.18-31.06-69.25-69.25-69.25l-216.9.23v145.17h-64.8V3h-79.95s-47.14,95.97-47.14,95.97V3h-52.09s-47.14,95.97-47.14,95.97V3h-53.48v69.6C560.37,30.64,521.34,0,475.28,0c-42.63,0-79.24,26.25-94.54,63.43-4.35-34.03-33.48-60.43-68.67-60.43h-100.01v47.43C202.9,22.91,176.91,3,146.35,3H0s46.34,46.33,46.34,46.33v151.64h53.47v-76.68l17.21,17.21h29.33c30.56,0,56.54-19.91,65.71-47.43v106.91h53.48v-81.51l76.01,81.51h69.62l-64.29-68.94c11.14-6.56,20.22-16.15,26.26-27.46,1.27,55.26,46.58,99.82,102.14,99.82,46.06,0,85.09-30.64,97.81-72.6v69.18h60.88l38.34-78.06v78.06h60.88l66.2-134.78v135.69h95.41l22.86-22.86v22.86h95.05l21.84-21.84v20.93h53.48v-81.5l76.01,81.5h69.62l-64.29-68.94ZM146.35,88.02h-46.54v-31.54h46.54c8.7,0,15.77,7.07,15.77,15.77s-7.07,15.77-15.77,15.77ZM312.07,88.02l-46.54-.18v-31.36h46.54c8.7,0,15.77,7.07,15.77,15.77s-7.08,15.77-15.77,15.77ZM475.28,150.92c-26.86,0-48.72-21.86-48.72-48.72s21.86-48.72,48.72-48.72,48.72,21.86,48.72,48.72-21.86,48.72-48.72,48.72ZM1034.56,148.41h-63.41v-20.35h42.91v-50.88h-42.91v-20.46h63.41v91.69ZM1134.57,88.02l-46.54-.18v-31.36h46.54c8.7,0,15.77,7.07,15.77,15.77s-7.07,15.77-15.77,15.77Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1233.67 204.4"
|
||||
fill="none"
|
||||
height={size || height}
|
||||
width={size || width}
|
||||
color="evenodd"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
className="cls-1"
|
||||
d="M1169.38 132.04c20.76-12.21 34.44-34.9 34.44-59.79 0-38.18-31.06-69.25-69.25-69.25l-216.9.23V148.4h-64.8V3h-79.95l-47.14 95.97V3h-52.09l-47.14 95.97V3h-53.48v69.6C560.37 30.64 521.34 0 475.28 0c-42.63 0-79.24 26.25-94.54 63.43C376.39 29.4 347.26 3 312.07 3H212.06v47.43C202.9 22.91 176.91 3 146.35 3H0l46.34 46.33v151.64h53.47v-76.68l17.21 17.21h29.33c30.56 0 56.54-19.91 65.71-47.43v106.91h53.48v-81.51l76.01 81.51h69.62l-64.29-68.94c11.14-6.56 20.22-16.15 26.26-27.46 1.27 55.26 46.58 99.82 102.14 99.82 46.06 0 85.09-30.64 97.81-72.6v69.18h60.88l38.34-78.06v78.06h60.88l66.2-134.78v135.69h95.41l22.86-22.86v22.86h95.05l21.84-21.84v20.93h53.48v-81.5l76.01 81.5h69.62l-64.29-68.94ZM146.35 88.02H99.81V56.48h46.54c8.7 0 15.77 7.07 15.77 15.77s-7.07 15.77-15.77 15.77Zm165.72 0-46.54-.18V56.48h46.54c8.7 0 15.77 7.07 15.77 15.77s-7.08 15.77-15.77 15.77Zm163.21 62.9c-26.86 0-48.72-21.86-48.72-48.72s21.86-48.72 48.72-48.72S524 75.34 524 102.2s-21.86 48.72-48.72 48.72Zm559.28-2.51h-63.41v-20.35h42.91V77.18h-42.91V56.72h63.41v91.69Zm100.01-60.39-46.54-.18V56.48h46.54c8.7 0 15.77 7.07 15.77 15.77s-7.07 15.77-15.77 15.77Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProwlerShort: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useRef } from "react";
|
||||
|
||||
import { checkConnectionProvider } from "@/actions";
|
||||
import { checkConnectionProvider } from "@/actions/providers";
|
||||
|
||||
import { CustomButtonClientAction } from "../ui/custom";
|
||||
import { useToast } from "../ui/toast";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dispatch, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { addProvider } from "@/actions";
|
||||
import { addProvider } from "@/actions/providers";
|
||||
import { SaveIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, { Dispatch, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { deleteProvider } from "@/actions";
|
||||
import { deleteProvider } from "@/actions/providers";
|
||||
import { DeleteIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dispatch, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { updateProvider } from "@/actions";
|
||||
import { updateProvider } from "@/actions/providers";
|
||||
import { SaveIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ColumnsProvider: ColumnDef<ProviderProps>[] = [
|
||||
{
|
||||
accessorKey: "uid",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={"Id"} param="provider_id" />
|
||||
<DataTableColumnHeader column={column} title={"Id"} param="uid" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
|
||||
@@ -16,6 +16,7 @@ interface CustomInputProps<T extends FieldValues> {
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
password?: boolean;
|
||||
confirmPassword?: boolean;
|
||||
isRequired?: boolean;
|
||||
isInvalid?: boolean;
|
||||
}
|
||||
@@ -28,24 +29,53 @@ export const CustomInput = <T extends FieldValues>({
|
||||
labelPlacement = "inside",
|
||||
placeholder,
|
||||
variant = "bordered",
|
||||
confirmPassword = false,
|
||||
password = false,
|
||||
isRequired = true,
|
||||
isInvalid,
|
||||
}: CustomInputProps<T>) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] =
|
||||
useState(false);
|
||||
|
||||
const toggleVisibility = () => setIsVisible(!isVisible);
|
||||
const inputLabel = confirmPassword
|
||||
? "Confirm Password"
|
||||
: password
|
||||
? "Password"
|
||||
: label;
|
||||
|
||||
const inputLabel = password ? "Password" : label;
|
||||
const inputType = password ? (isVisible ? "text" : "password") : type;
|
||||
const inputPlaceholder = password ? "Enter your password" : placeholder;
|
||||
const inputIsRequired = password ? true : isRequired;
|
||||
const inputPlaceholder = confirmPassword
|
||||
? "Confirm Password"
|
||||
: password
|
||||
? "Password"
|
||||
: placeholder;
|
||||
|
||||
const endContent = password && (
|
||||
const inputType =
|
||||
password || confirmPassword
|
||||
? isPasswordVisible || isConfirmPasswordVisible
|
||||
? "text"
|
||||
: "password"
|
||||
: type;
|
||||
const inputIsRequired = password || confirmPassword ? true : isRequired;
|
||||
|
||||
const toggleVisibility = () => {
|
||||
if (password) {
|
||||
setIsPasswordVisible(!isPasswordVisible);
|
||||
} else if (confirmPassword) {
|
||||
setIsConfirmPasswordVisible(!isConfirmPasswordVisible);
|
||||
}
|
||||
};
|
||||
|
||||
const endContent = (password || confirmPassword) && (
|
||||
<button type="button" onClick={toggleVisibility}>
|
||||
<Icon
|
||||
className="pointer-events-none text-2xl text-default-400"
|
||||
icon={isVisible ? "solar:eye-closed-linear" : "solar:eye-bold"}
|
||||
icon={
|
||||
(password && isPasswordVisible) ||
|
||||
(confirmPassword && isConfirmPasswordVisible)
|
||||
? "solar:eye-closed-linear"
|
||||
: "solar:eye-bold"
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
@@ -58,6 +88,7 @@ export const CustomInput = <T extends FieldValues>({
|
||||
<>
|
||||
<FormControl>
|
||||
<Input
|
||||
id={name}
|
||||
isRequired={inputIsRequired}
|
||||
label={inputLabel}
|
||||
labelPlacement={labelPlacement}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const buttonClasses = {
|
||||
base: "px-4 inline-flex items-center justify-center relative z-0 text-center whitespace-nowrap",
|
||||
primary: "bg-default-100 hover:bg-default-200 text-default-800",
|
||||
secondary: "bg-prowler-grey-light dark:bg-prowler-grey-medium text-white",
|
||||
action: "text-white bg-prowler-blue-smoky dark:bg-prowler-grey-medium",
|
||||
action: "text-white bg-prowler-grey-medium dark:bg-prowler-grey-medium",
|
||||
dashed:
|
||||
"border border-default border-dashed bg-transparent justify-center whitespace-nowrap font-medium shadow-sm hover:border-solid hover:bg-default-100 active:bg-default-200 active:border-solid",
|
||||
transparent: "border-0 border-transparent bg-transparent",
|
||||
@@ -19,6 +19,7 @@ export const buttonClasses = {
|
||||
interface ButtonProps {
|
||||
type?: "button" | "submit" | "reset";
|
||||
ariaLabel: string;
|
||||
ariaDisabled?: boolean;
|
||||
className?: string;
|
||||
variant?:
|
||||
| "solid"
|
||||
@@ -52,6 +53,7 @@ interface ButtonProps {
|
||||
export const CustomButton = ({
|
||||
type = "button",
|
||||
ariaLabel,
|
||||
ariaDisabled,
|
||||
className,
|
||||
variant = "solid",
|
||||
color = "primary",
|
||||
@@ -69,6 +71,7 @@ export const CustomButton = ({
|
||||
<Button
|
||||
type={type}
|
||||
aria-label={ariaLabel}
|
||||
aria-disabled={ariaDisabled}
|
||||
onPress={onPress}
|
||||
variant={variant as NextUIVariants}
|
||||
color={color as NextUIColors}
|
||||
@@ -89,7 +92,16 @@ export const CustomButton = ({
|
||||
endContent={endContent}
|
||||
size={size}
|
||||
radius={radius}
|
||||
spinner={<CircularProgress aria-label="Loading..." size="sm" />}
|
||||
spinner={
|
||||
<CircularProgress
|
||||
classNames={{
|
||||
svg: "w-6 h-6 drop-shadow-md",
|
||||
indicator: "stroke-white",
|
||||
track: "stroke-white/10",
|
||||
}}
|
||||
aria-label="Loading..."
|
||||
/>
|
||||
}
|
||||
isLoading={isLoading}
|
||||
isIconOnly={isIconOnly}
|
||||
{...props}
|
||||
|
||||
@@ -6,10 +6,10 @@ import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import React, { useCallback } from "react";
|
||||
import React, { Suspense, useCallback } from "react";
|
||||
import { useMediaQuery } from "usehooks-ts";
|
||||
|
||||
import { logOut } from "@/actions";
|
||||
import { logOut } from "@/actions/auth";
|
||||
import { useUIStore } from "@/store";
|
||||
|
||||
import {
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "../../icons/prowler/ProwlerIcons";
|
||||
import { ThemeSwitch } from "../../ThemeSwitch";
|
||||
import Sidebar from "./Sidebar";
|
||||
import { sectionItems, sectionItemsWithTeams } from "./SidebarItems";
|
||||
import { sectionItemsWithTeams } from "./SidebarItems";
|
||||
import { UserAvatar } from "./UserAvatar";
|
||||
|
||||
export const SidebarWrap = () => {
|
||||
@@ -72,22 +72,20 @@ export const SidebarWrap = () => {
|
||||
<Spacer y={8} />
|
||||
|
||||
<Link href={"/profile"}>
|
||||
<UserAvatar
|
||||
userName={session?.user?.name ?? "Guest"}
|
||||
position={session?.user?.companyName ?? "Company Name"}
|
||||
isCompact={isCompact}
|
||||
/>{" "}
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<UserAvatar
|
||||
userName={session?.user.name ?? "Guest"}
|
||||
position={session?.user.companyName ?? "Company Name"}
|
||||
isCompact={isCompact}
|
||||
/>
|
||||
</Suspense>
|
||||
</Link>
|
||||
|
||||
<ScrollShadow hideScrollBar className="-mr-6 h-full max-h-full py-6 pr-6">
|
||||
<Sidebar
|
||||
defaultSelectedKey="overview"
|
||||
isCompact={isCompact}
|
||||
items={
|
||||
session?.user?.role === "admin"
|
||||
? sectionItemsWithTeams
|
||||
: sectionItems
|
||||
}
|
||||
items={sectionItemsWithTeams}
|
||||
selectedKeys={[currentPath]}
|
||||
/>
|
||||
</ScrollShadow>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
{
|
||||
"links": {
|
||||
"first": "https://api.prowler.com/api/v1/providers?page%5Bnumber%5D=1",
|
||||
"last": "https://api.prowler.com/api/v1/providers?page%5Bnumber%5D=1",
|
||||
"next": null,
|
||||
"prev": null
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"id": "5fd8f121-269e-4715-84cf-f92373f15dfa",
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": "aws",
|
||||
"provider_id": "1234567890",
|
||||
"alias": "mock_aws_connected",
|
||||
"status": "cancelled",
|
||||
"resources": 101,
|
||||
"connection": {
|
||||
"connected": true,
|
||||
"last_checked_at": "2024-07-17T09:55:14.191475Z"
|
||||
},
|
||||
"scanner_args": {
|
||||
"only_logs": true,
|
||||
"excluded_checks": [
|
||||
"awslambda_function_no_secrets_in_code",
|
||||
"cloudwatch_log_group_no_secrets_in_logs"
|
||||
],
|
||||
"aws_retries_max_attempts": 5
|
||||
},
|
||||
"inserted_at": "2024-07-17T09:55:14.191475Z",
|
||||
"updated_at": "2024-07-17T09:55:14.191475Z",
|
||||
"created_by": {
|
||||
"object": "user",
|
||||
"id": "eea048ab-7cb3-47eb-9e5e-dce591ade41f"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "16aaeb4e-d3cd-4bb6-86f8-6c39cf93821e",
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": "azure",
|
||||
"provider_id": "1234567891",
|
||||
"alias": "mock_aws_not_connected",
|
||||
"status": "pending",
|
||||
"resources": 222,
|
||||
"connection": {
|
||||
"connected": false,
|
||||
"last_checked_at": "2024-07-17T09:55:18.987425Z"
|
||||
},
|
||||
"scanner_args": {
|
||||
"only_logs": true,
|
||||
"excluded_checks": [
|
||||
"awslambda_function_no_secrets_in_code",
|
||||
"cloudwatch_log_group_no_secrets_in_logs"
|
||||
],
|
||||
"aws_retries_max_attempts": 5
|
||||
},
|
||||
"inserted_at": "2024-07-17T09:50:18.987425Z",
|
||||
"updated_at": "2024-07-17T09:55:18.987425Z",
|
||||
"created_by": {
|
||||
"object": "user",
|
||||
"id": "a8f5e964-5964-4aaf-9176-844e2c3b0716"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "63f16b03-7849-4054-b40b-300e331f46f0",
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": "gcp",
|
||||
"provider_id": "1234567895",
|
||||
"alias": "mock_gcp",
|
||||
"status": "completed",
|
||||
"resources": 143,
|
||||
"connection": {
|
||||
"connected": true,
|
||||
"last_checked_at": "2024-07-17T09:55:18.987425Z"
|
||||
},
|
||||
"scanner_args": {
|
||||
"only_logs": true,
|
||||
"excluded_checks": [
|
||||
"apikeys_key_exists",
|
||||
"cloudsql_instance_public_ip"
|
||||
],
|
||||
"excluded_services": ["kms"]
|
||||
},
|
||||
"inserted_at": "2024-07-17T09:50:18.987425Z",
|
||||
"updated_at": "2024-07-17T09:55:18.987425Z",
|
||||
"created_by": {
|
||||
"object": "user",
|
||||
"id": "eea048ab-7cb3-47eb-9e5e-dce591ade41f"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"count": 3
|
||||
},
|
||||
"version": "v1"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./custom";
|
||||
export * from "./seed";
|
||||
export * from "./utils";
|
||||
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import bcryptjs from "bcryptjs";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export const userMockData = [
|
||||
{
|
||||
id: uuidv4(), // Generate a unique UUID.
|
||||
tenantId: "12646005-9067-4d2a-a098-8bb378604362",
|
||||
email: "admin@prowler.com",
|
||||
name: "Admin Prowler",
|
||||
companyName: "Prowler",
|
||||
password: bcryptjs.hashSync("123123", 10),
|
||||
role: "admin",
|
||||
image: null,
|
||||
},
|
||||
{
|
||||
id: uuidv4(), // Generate a unique UUID.
|
||||
tenantId: "12646005-9067-4d2a-a098-8bb378604362",
|
||||
email: "user@prowler.com",
|
||||
name: "User Prowler",
|
||||
companyName: "Prowler",
|
||||
password: bcryptjs.hashSync("123123", 10),
|
||||
role: "user",
|
||||
image: null,
|
||||
},
|
||||
];
|
||||
Vendored
+15
-7
@@ -1,15 +1,23 @@
|
||||
import { DefaultSession } from "next-auth";
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
interface User extends NextAuthUser {
|
||||
name: string;
|
||||
email: string;
|
||||
company?: string;
|
||||
dateJoined: string;
|
||||
}
|
||||
|
||||
interface Session extends DefaultSession {
|
||||
user: {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
firstName: string;
|
||||
companyName: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
image?: string;
|
||||
companyName?: string;
|
||||
dateJoined: string;
|
||||
} & DefaultSession["user"];
|
||||
userId: string;
|
||||
tenantId: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+15
-3
@@ -32,6 +32,8 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"framer-motion": "~11.1.1",
|
||||
"intl-messageformat": "^10.5.0",
|
||||
"jose": "^5.9.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.417.0",
|
||||
"next": "^14.2.12",
|
||||
"next-auth": "^5.0.0-beta.20",
|
||||
@@ -9423,9 +9425,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.7.0.tgz",
|
||||
"integrity": "sha512-3P9qfTYDVnNn642LCAqIKbTGb9a1TBxZ9ti5zEVEr48aDdflgRjhspWFb6WM4PzAfFbGMJYC4+803v8riCRAKw==",
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.3.tgz",
|
||||
"integrity": "sha512-egLIoYSpcd+QUF+UHgobt5YzI2Pkw/H39ou9suW687MY6PmCwPmkNV/4TNjn1p2tX5xO3j0d0sq5hiYE24bSlg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@@ -9507,6 +9510,15 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"framer-motion": "~11.1.1",
|
||||
"intl-messageformat": "^10.5.0",
|
||||
"jose": "^5.9.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.417.0",
|
||||
"next": "^14.2.12",
|
||||
"next-auth": "^5.0.0-beta.20",
|
||||
|
||||
+20
-18
@@ -20,33 +20,34 @@ module.exports = {
|
||||
extend: {
|
||||
colors: {
|
||||
prowler: {
|
||||
blue: {
|
||||
theme: {
|
||||
midnight: "#030921",
|
||||
pale: "#f3fcff",
|
||||
smoky: "#7b8390",
|
||||
},
|
||||
grey: {
|
||||
medium: "#353a4d",
|
||||
light: "#868994",
|
||||
},
|
||||
green: {
|
||||
DEFAULT: "#9FD655",
|
||||
medium: "#09BF3D",
|
||||
},
|
||||
theme: {
|
||||
green: "#6af400",
|
||||
purple: "#5001d0",
|
||||
coral: "#ff5356",
|
||||
orange: "#f69000",
|
||||
yellow: "#ffdf16",
|
||||
},
|
||||
dark: {
|
||||
title: "#E2E8F0",
|
||||
text: "#94a3b8" /* primary default for dark mode */,
|
||||
blue: {
|
||||
800: "#1e293bff",
|
||||
},
|
||||
light: {
|
||||
title: "#1e293bff",
|
||||
text: "#64748b" /* primary default for light mode */,
|
||||
grey: {
|
||||
medium: "#353a4d",
|
||||
light: "#868994",
|
||||
600: "#64748b",
|
||||
},
|
||||
green: {
|
||||
DEFAULT: "#9FD655",
|
||||
medium: "#09BF3D",
|
||||
},
|
||||
black: {
|
||||
DEFAULT: "#000",
|
||||
900: "#18181A",
|
||||
},
|
||||
white: {
|
||||
DEFAULT: "#FFF",
|
||||
900: "#18181A",
|
||||
},
|
||||
},
|
||||
system: {
|
||||
@@ -82,6 +83,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
danger: "#E11D48",
|
||||
action: "#353a4d",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)"],
|
||||
|
||||
+38
-22
@@ -1,27 +1,43 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const authFormSchema = (type: string) =>
|
||||
z.object({
|
||||
// Sign Up
|
||||
companyName: type === "sign-in" ? z.string().optional() : z.string().min(3),
|
||||
firstName:
|
||||
type === "sign-in"
|
||||
? z.string().optional()
|
||||
: z
|
||||
.string()
|
||||
.min(3, {
|
||||
message: "The name must be at least 3 characters.",
|
||||
})
|
||||
.max(20),
|
||||
termsAndConditions:
|
||||
type === "sign-in"
|
||||
? z.enum(["true"]).optional()
|
||||
: z.enum(["true"], {
|
||||
errorMap: () => ({
|
||||
z
|
||||
.object({
|
||||
// Sign Up
|
||||
company:
|
||||
type === "sign-in" ? z.string().optional() : z.string().optional(),
|
||||
name:
|
||||
type === "sign-in"
|
||||
? z.string().optional()
|
||||
: z
|
||||
.string()
|
||||
.min(3, {
|
||||
message: "The name must be at least 3 characters.",
|
||||
})
|
||||
.max(20),
|
||||
confirmPassword:
|
||||
type === "sign-in"
|
||||
? z.string().optional()
|
||||
: z.string().min(12, {
|
||||
message: "It must contain at least 12 characters.",
|
||||
}),
|
||||
termsAndConditions:
|
||||
type === "sign-in"
|
||||
? z.boolean().optional()
|
||||
: z.boolean().refine((value) => value === true, {
|
||||
message: "You must accept the terms and conditions.",
|
||||
}),
|
||||
}),
|
||||
// both
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
});
|
||||
|
||||
// Fields for Sign In and Sign Up
|
||||
email: z.string().email(),
|
||||
password: z.string().min(12, {
|
||||
message: "It must contain at least 12 characters.",
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
(data) => type === "sign-in" || data.password === data.confirmPassword,
|
||||
{
|
||||
message: "The password must match",
|
||||
path: ["confirmPassword"],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -29,6 +29,16 @@ export type NextUIColors =
|
||||
export interface SearchParamsProps {
|
||||
[key: string]: string | string[] | undefined;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
detail: string;
|
||||
status: string;
|
||||
source: {
|
||||
pointer: string;
|
||||
};
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface ProviderProps {
|
||||
id: string;
|
||||
type: "providers";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./users";
|
||||
@@ -0,0 +1,52 @@
|
||||
export interface UserAttributes {
|
||||
name: string;
|
||||
email: string;
|
||||
company_name: string;
|
||||
date_joined: string;
|
||||
}
|
||||
|
||||
export interface Membership {
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface MembershipMeta {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface UserRelationships {
|
||||
memberships: {
|
||||
meta: MembershipMeta;
|
||||
data: Membership[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
type: string;
|
||||
id: string;
|
||||
attributes: UserAttributes;
|
||||
relationships: UserRelationships;
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface UserProps {
|
||||
data: UserData;
|
||||
meta: Meta;
|
||||
}
|
||||
|
||||
export interface TokenAttributes {
|
||||
refreshToken: string;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface TokenData {
|
||||
type: string;
|
||||
attributes: TokenAttributes;
|
||||
}
|
||||
|
||||
export interface SignInResponse {
|
||||
data: TokenData;
|
||||
}
|
||||
Reference in New Issue
Block a user