diff --git a/actions/auth.ts b/actions/auth.ts
deleted file mode 100644
index 2cdf70bfd2..0000000000
--- a/actions/auth.ts
+++ /dev/null
@@ -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 = {
- email: "",
- password: "",
-};
-
-export async function authenticate(
- prevState: unknown,
- formData: z.infer,
-) {
- 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();
-}
diff --git a/actions/auth/auth.ts b/actions/auth/auth.ts
new file mode 100644
index 0000000000..394bdf4f3e
--- /dev/null
+++ b/actions/auth/auth.ts
@@ -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 = {
+ email: "",
+ password: "",
+};
+
+export async function authenticate(
+ prevState: unknown,
+ formData: z.infer,
+) {
+ 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,
+) => {
+ 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) => {
+ 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();
+}
diff --git a/actions/auth/index.ts b/actions/auth/index.ts
new file mode 100644
index 0000000000..97ccf76494
--- /dev/null
+++ b/actions/auth/index.ts
@@ -0,0 +1 @@
+export * from "./auth";
diff --git a/actions/index.ts b/actions/providers/index.ts
similarity index 54%
rename from actions/index.ts
rename to actions/providers/index.ts
index 0085735bb3..5532383f5f 100644
--- a/actions/index.ts
+++ b/actions/providers/index.ts
@@ -1,2 +1 @@
-export * from "./auth";
export * from "./providers";
diff --git a/actions/providers.ts b/actions/providers/providers.ts
similarity index 80%
rename from actions/providers.ts
rename to actions/providers/providers.ts
index a35bbb1d98..200bc79bd6 100644
--- a/actions/providers.ts
+++ b/actions/providers/providers.ts
@@ -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 => {
let message: string;
if (error instanceof Error) {
diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx
index f012c3f358..b5bd978029 100644
--- a/app/(auth)/sign-in/page.tsx
+++ b/app/(auth)/sign-in/page.tsx
@@ -1,6 +1,4 @@
-import React from "react";
-
-import { AuthForm } from "@/components/auth";
+import { AuthForm } from "@/components/auth/oss";
const SignIn = () => {
return ;
diff --git a/app/(auth)/sign-up/page.tsx b/app/(auth)/sign-up/page.tsx
index dfe04cadeb..8e525507b8 100644
--- a/app/(auth)/sign-up/page.tsx
+++ b/app/(auth)/sign-up/page.tsx
@@ -1,6 +1,4 @@
-import React from "react";
-
-import { AuthForm } from "@/components/auth";
+import { AuthForm } from "@/components/auth/oss";
const SignUp = () => {
return ;
diff --git a/app/(prowler)/profile/page.tsx b/app/(prowler)/profile/page.tsx
index 76c72a8800..0cfb4f8ab2 100644
--- a/app/(prowler)/profile/page.tsx
+++ b/app/(prowler)/profile/page.tsx
@@ -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 (
<>
{JSON.stringify(session.user, null, 2)}
+ {JSON.stringify(session.userId, null, 2)}
+ {JSON.stringify(session.tenantId, null, 2)}
+ {JSON.stringify(session, null, 2)}
>
);
}
diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx
index a10cd9a618..b999671859 100644
--- a/app/(prowler)/providers/page.tsx
+++ b/app/(prowler)/providers/page.tsx
@@ -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({
-
+
@@ -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(
diff --git a/app/(prowler)/users/page.tsx b/app/(prowler)/users/page.tsx
index 0a2f240f57..34a887589f 100644
--- a/app/(prowler)/users/page.tsx
+++ b/app/(prowler)/users/page.tsx
@@ -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 (
diff --git a/app/api/providers/route.ts b/app/api/providers/route.ts
deleted file mode 100644
index ef9b5995e2..0000000000
--- a/app/api/providers/route.ts
+++ /dev/null
@@ -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 });
-}
diff --git a/auth.config.ts b/auth.config.ts
index 55a81edcca..4639f8bf37 100644
--- a/auth.config.ts
+++ b/auth.config.ts
@@ -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 {
- // 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);
diff --git a/components/auth/AuthButton.tsx b/components/auth/AuthButton.tsx
deleted file mode 100644
index 0eda81782a..0000000000
--- a/components/auth/AuthButton.tsx
+++ /dev/null
@@ -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 (
-
- );
-};
diff --git a/components/auth/AuthForm.tsx b/components/auth/AuthForm.tsx
deleted file mode 100644
index b1bbcb866a..0000000000
--- a/components/auth/AuthForm.tsx
+++ /dev/null
@@ -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>({
- 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) => {
- // 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 (
-
- {/* Brand Logo and ThemeSwitch */}
-
-
- {/* Testimonial */}
-
-
- “
- Open Cloud Security
- ”
-
-
-
-
-
- {type === "sign-in" ? "Sign In" : "Sign Up"}
-
-
-
-
-
- {type === "sign-in" && (
- <>
-
-
-
- }
- variant="bordered"
- >
- Continue with Google
-
-
- }
- variant="bordered"
- >
- Continue with Github
-
-
- >
- )}
- {type === "sign-in" ? (
-
- Need to create an account?
- Sign Up
-
- ) : (
-
- Already have an account?
- Log In
-
- )}
-
-
- );
-};
diff --git a/components/auth/index.ts b/components/auth/index.ts
deleted file mode 100644
index 26161c473c..0000000000
--- a/components/auth/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./AuthButton";
-export * from "./AuthForm";
diff --git a/components/auth/oss/auth-form.tsx b/components/auth/oss/auth-form.tsx
new file mode 100644
index 0000000000..451e043c0b
--- /dev/null
+++ b/components/auth/oss/auth-form.tsx
@@ -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>({
+ 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) => {
+ 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 (
+
+ {/* Auth Form */}
+
+ {/* Background Pattern */}
+
+
+
+ {/* Prowler Logo */}
+
+
+
+ {type === "sign-in" ? "Sign In" : "Sign Up"}
+
+
+
+
+
+
+
+ {type === "sign-in" && (
+ <>
+
+
+
+ }
+ variant="bordered"
+ >
+ Continue with Google
+
+
+ }
+ variant="bordered"
+ >
+ Continue with Github
+
+
+ >
+ )}
+ {type === "sign-in" ? (
+
+ Need to create an account?
+ Sign Up
+
+ ) : (
+
+ Already have an account?
+ Log In
+
+ )}
+
+
+
+ );
+};
diff --git a/components/auth/oss/index.ts b/components/auth/oss/index.ts
new file mode 100644
index 0000000000..82fc6feefa
--- /dev/null
+++ b/components/auth/oss/index.ts
@@ -0,0 +1 @@
+export * from "./auth-form";
diff --git a/components/filters/data-filters.ts b/components/filters/data-filters.ts
index 9aea8e9dd3..df1e01cea0 100644
--- a/components/filters/data-filters.ts
+++ b/components/filters/data-filters.ts
@@ -1,9 +1,4 @@
export const filtersProviders = [
- {
- key: "provider__in",
- labelCheckboxGroup: "Select a Provider",
- values: ["aws", "gcp", "azure", "kubernetes"],
- },
{
key: "connected",
labelCheckboxGroup: "Connection",
diff --git a/components/icons/prowler/ProwlerIcons.tsx b/components/icons/prowler/ProwlerIcons.tsx
index 0229c6e0b5..425788e88b 100644
--- a/components/icons/prowler/ProwlerIcons.tsx
+++ b/components/icons/prowler/ProwlerIcons.tsx
@@ -7,26 +7,27 @@ export const ProwlerExtended: React.FC = ({
width = 164,
height,
...props
-}) => (
-
-);
+}) => {
+ return (
+
+ );
+};
export const ProwlerShort: React.FC = ({
size,
diff --git a/components/providers/CheckConnectionProvider.tsx b/components/providers/CheckConnectionProvider.tsx
index 2fadafad04..d5bc9e727d 100644
--- a/components/providers/CheckConnectionProvider.tsx
+++ b/components/providers/CheckConnectionProvider.tsx
@@ -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";
diff --git a/components/providers/forms/add-form.tsx b/components/providers/forms/add-form.tsx
index f30855d2d0..4c063c7aef 100644
--- a/components/providers/forms/add-form.tsx
+++ b/components/providers/forms/add-form.tsx
@@ -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";
diff --git a/components/providers/forms/delete-form.tsx b/components/providers/forms/delete-form.tsx
index 434f453279..e08ac4ada5 100644
--- a/components/providers/forms/delete-form.tsx
+++ b/components/providers/forms/delete-form.tsx
@@ -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";
diff --git a/components/providers/forms/edit-form.tsx b/components/providers/forms/edit-form.tsx
index 669e4a9184..bae117b7da 100644
--- a/components/providers/forms/edit-form.tsx
+++ b/components/providers/forms/edit-form.tsx
@@ -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";
diff --git a/components/providers/table/columns-provider.tsx b/components/providers/table/columns-provider.tsx
index fd190a811a..065b6d5863 100644
--- a/components/providers/table/columns-provider.tsx
+++ b/components/providers/table/columns-provider.tsx
@@ -42,7 +42,7 @@ export const ColumnsProvider: ColumnDef[] = [
{
accessorKey: "uid",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const {
diff --git a/components/ui/custom/CustomInput.tsx b/components/ui/custom/CustomInput.tsx
index 81a91316d0..26cc17cd9b 100644
--- a/components/ui/custom/CustomInput.tsx
+++ b/components/ui/custom/CustomInput.tsx
@@ -16,6 +16,7 @@ interface CustomInputProps {
type?: string;
placeholder?: string;
password?: boolean;
+ confirmPassword?: boolean;
isRequired?: boolean;
isInvalid?: boolean;
}
@@ -28,24 +29,53 @@ export const CustomInput = ({
labelPlacement = "inside",
placeholder,
variant = "bordered",
+ confirmPassword = false,
password = false,
isRequired = true,
isInvalid,
}: CustomInputProps) => {
- 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) && (
);
@@ -58,6 +88,7 @@ export const CustomInput = ({
<>
}
+ spinner={
+
+ }
isLoading={isLoading}
isIconOnly={isIconOnly}
{...props}
diff --git a/components/ui/sidebar/SidebarWrap.tsx b/components/ui/sidebar/SidebarWrap.tsx
index 949cd59da0..72451eb3fe 100644
--- a/components/ui/sidebar/SidebarWrap.tsx
+++ b/components/ui/sidebar/SidebarWrap.tsx
@@ -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 = () => {
- {" "}
+ Loading...
}>
+
+
diff --git a/dataProviders.json b/dataProviders.json
deleted file mode 100644
index ab8a058620..0000000000
--- a/dataProviders.json
+++ /dev/null
@@ -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"
- }
-}
diff --git a/lib/index.ts b/lib/index.ts
index 8edc14574b..38d384fb6e 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -1,3 +1,2 @@
export * from "./custom";
-export * from "./seed";
export * from "./utils";
diff --git a/lib/seed.ts b/lib/seed.ts
deleted file mode 100644
index 1ff00ee60b..0000000000
--- a/lib/seed.ts
+++ /dev/null
@@ -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,
- },
-];
diff --git a/nextauth.d.ts b/nextauth.d.ts
index 2fe2480ce5..d5b6f472f9 100644
--- a/nextauth.d.ts
+++ b/nextauth.d.ts
@@ -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;
}
}
diff --git a/package-lock.json b/package-lock.json
index 9d3fc5073c..1f8510886b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 468ce86d58..67ac210f66 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/tailwind.config.js b/tailwind.config.js
index 6d2ff06563..31bdbfd39e 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -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)"],
diff --git a/types/authFormSchema.ts b/types/authFormSchema.ts
index 3cd577de69..6f3ab5a087 100644
--- a/types/authFormSchema.ts
+++ b/types/authFormSchema.ts
@@ -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"],
+ },
+ );
diff --git a/types/components.ts b/types/components.ts
index 28ffbe7355..02018469dc 100644
--- a/types/components.ts
+++ b/types/components.ts
@@ -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";
diff --git a/types/users/index.ts b/types/users/index.ts
new file mode 100644
index 0000000000..ddf77b4624
--- /dev/null
+++ b/types/users/index.ts
@@ -0,0 +1 @@
+export * from "./users";
diff --git a/types/users/users.ts b/types/users/users.ts
new file mode 100644
index 0000000000..61a92ae70e
--- /dev/null
+++ b/types/users/users.ts
@@ -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;
+}