Merge pull request #65 from prowler-cloud/PRWLR-4883-Integrate-authentication-endpoint-OSS

Integrate authentication endpoint oss
This commit is contained in:
Pablo Lara
2024-10-08 09:30:44 +02:00
committed by GitHub
38 changed files with 880 additions and 667 deletions
-55
View File
@@ -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();
}
+166
View File
@@ -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();
}
+1
View File
@@ -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 -3
View File
@@ -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 -3
View File
@@ -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" />;
+6
View File
@@ -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>
</>
);
}
+3 -3
View File
@@ -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(
+1 -6
View File
@@ -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 (
-10
View File
@@ -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
View File
@@ -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);
-21
View File
@@ -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>
);
};
-252
View File
@@ -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&nbsp;
<Link href="#" size="sm">
Terms
</Link>
&nbsp; and&nbsp;
<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?&nbsp;
<Link href="/sign-up">Sign Up</Link>
</p>
) : (
<p className="text-center text-small">
Already have an account?&nbsp;
<Link href="/sign-in">Log In</Link>
</p>
)}
</div>
</div>
);
};
-2
View File
@@ -1,2 +0,0 @@
export * from "./AuthButton";
export * from "./AuthForm";
+294
View File
@@ -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&nbsp;
<Link href="#" size="sm">
Terms
</Link>
&nbsp; and&nbsp;
<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?&nbsp;
<Link href="/sign-up">Sign Up</Link>
</p>
) : (
<p className="text-center text-small">
Already have an account?&nbsp;
<Link href="/sign-in">Log In</Link>
</p>
)}
</div>
</div>
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export * from "./auth-form";
-5
View File
@@ -1,9 +1,4 @@
export const filtersProviders = [
{
key: "provider__in",
labelCheckboxGroup: "Select a Provider",
values: ["aws", "gcp", "azure", "kubernetes"],
},
{
key: "connected",
labelCheckboxGroup: "Connection",
+21 -20
View File
@@ -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";
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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 {
+39 -8
View File
@@ -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}
+14 -2
View File
@@ -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}
+11 -13
View File
@@ -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>
-105
View File
@@ -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
View File
@@ -1,3 +1,2 @@
export * from "./custom";
export * from "./seed";
export * from "./utils";
-25
View File
@@ -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,
},
];
+15 -7
View File
@@ -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;
}
}
+15 -3
View File
@@ -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",
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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"],
},
);
+10
View File
@@ -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";
+1
View File
@@ -0,0 +1 @@
export * from "./users";
+52
View File
@@ -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;
}