mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-04-20 03:50:20 +00:00
refactor: update authentication flow to use NextAuth, replace better-auth with axios for API calls, and clean up unused code
This commit is contained in:
parent
0fd269df31
commit
020d74c5e2
@ -1,10 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import prisma from "@/lib/db";
|
|
||||||
import { VerifyUserDetails } from "@/lib/person";
|
import { VerifyUserDetails } from "@/lib/person";
|
||||||
import { signUpFormSchema } from "@/lib/schemas";
|
import { signUpFormSchema } from "@/lib/schemas";
|
||||||
import { phoneNumber } from "better-auth/plugins";
|
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -34,7 +31,7 @@ export async function signin(previousState: ActionState, formData: FormData) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const FORMATTED_MOBILE_NUMBER: string = `${phoneNumber.split("-").join("")}`;
|
const FORMATTED_MOBILE_NUMBER: string = `${phoneNumber.split("-").join("")}`;
|
||||||
console.log(FORMATTED_MOBILE_NUMBER);
|
console.log({ FORMATTED_MOBILE_NUMBER });
|
||||||
const userExistsResponse = await fetch(
|
const userExistsResponse = await fetch(
|
||||||
`${process.env.SARLINK_API_BASE_URL}/auth/mobile/`,
|
`${process.env.SARLINK_API_BASE_URL}/auth/mobile/`,
|
||||||
{
|
{
|
||||||
@ -48,7 +45,7 @@ export async function signin(previousState: ActionState, formData: FormData) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const userExists = await userExistsResponse.json();
|
const userExists = await userExistsResponse.json();
|
||||||
console.log(userExists.non_field_errors);
|
console.log("user exists", userExists);
|
||||||
if (userExists?.non_field_errors) {
|
if (userExists?.non_field_errors) {
|
||||||
return redirect(`/signup?phone_number=${phoneNumber}`);
|
return redirect(`/signup?phone_number=${phoneNumber}`);
|
||||||
}
|
}
|
||||||
@ -75,7 +72,6 @@ export async function signup(_actionState: ActionState, formData: FormData) {
|
|||||||
const data = Object.fromEntries(formData.entries());
|
const data = Object.fromEntries(formData.entries());
|
||||||
const parsedData = signUpFormSchema.safeParse(data);
|
const parsedData = signUpFormSchema.safeParse(data);
|
||||||
// get phone number from /signup?phone_number=999-1231
|
// get phone number from /signup?phone_number=999-1231
|
||||||
const headersList = await headers();
|
|
||||||
|
|
||||||
console.log("DATA ON SERVER SIDE", data);
|
console.log("DATA ON SERVER SIDE", data);
|
||||||
|
|
||||||
@ -87,83 +83,82 @@ export async function signup(_actionState: ActionState, formData: FormData) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const idCardExists = await prisma.user.findFirst({
|
// const idCardExists = await prisma.user.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
id_card: parsedData.data.id_card,
|
// id_card: parsedData.data.id_card,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (idCardExists) {
|
// if (idCardExists) {
|
||||||
return {
|
// return {
|
||||||
message: "ID card already exists.",
|
// message: "ID card already exists.",
|
||||||
payload: formData,
|
// payload: formData,
|
||||||
db_error: "id_card",
|
// db_error: "id_card",
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
const phoneNumberExists = await prisma.user.findFirst({
|
// const phoneNumberExists = await prisma.user.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
phoneNumber: parsedData.data.phone_number,
|
// phoneNumber: parsedData.data.phone_number,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (phoneNumberExists) {
|
// if (phoneNumberExists) {
|
||||||
return {
|
// return {
|
||||||
message: "Phone number already exists.",
|
// message: "Phone number already exists.",
|
||||||
payload: formData,
|
// payload: formData,
|
||||||
db_error: "phone_number",
|
// db_error: "phone_number",
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
const newUser = await prisma.user.create({
|
// const newUser = await prisma.user.create({
|
||||||
data: {
|
// data: {
|
||||||
name: parsedData.data.name,
|
// name: parsedData.data.name,
|
||||||
islandId: parsedData.data.island_id,
|
// islandId: parsedData.data.island_id,
|
||||||
atollId: parsedData.data.atoll_id,
|
// atollId: parsedData.data.atoll_id,
|
||||||
address: parsedData.data.address,
|
// address: parsedData.data.address,
|
||||||
id_card: parsedData.data.id_card,
|
// id_card: parsedData.data.id_card,
|
||||||
dob: new Date(parsedData.data.dob),
|
// dob: new Date(parsedData.data.dob),
|
||||||
role: "USER",
|
// role: "USER",
|
||||||
accNo: parsedData.data.accNo,
|
// accNo: parsedData.data.accNo,
|
||||||
phoneNumber: parsedData.data.phone_number,
|
// phoneNumber: parsedData.data.phone_number,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
const isValidPerson = await VerifyUserDetails({ user: newUser });
|
// const isValidPerson = await VerifyUserDetails({ user: newUser });
|
||||||
|
|
||||||
if (!isValidPerson) {
|
// if (!isValidPerson) {
|
||||||
await SendUserRejectionDetailSMS({
|
// await SendUserRejectionDetailSMS({
|
||||||
details: `
|
// details: `
|
||||||
A new user has requested for verification. \n
|
// A new user has requested for verification. \n
|
||||||
USER DETAILS:
|
// USER DETAILS:
|
||||||
Name: ${parsedData.data.name}
|
// Name: ${parsedData.data.name}
|
||||||
Address: ${parsedData.data.address}
|
// Address: ${parsedData.data.address}
|
||||||
ID Card: ${parsedData.data.id_card}
|
// ID Card: ${parsedData.data.id_card}
|
||||||
DOB: ${parsedData.data.dob.toLocaleDateString("en-US", {
|
// DOB: ${parsedData.data.dob.toLocaleDateString("en-US", {
|
||||||
month: "short",
|
// month: "short",
|
||||||
day: "2-digit",
|
// day: "2-digit",
|
||||||
year: "numeric",
|
// year: "numeric",
|
||||||
})}
|
// })}
|
||||||
ACC No: ${parsedData.data.accNo}\n\nVerify the user with the following link: ${process.env.BETTER_AUTH_URL}/users/${newUser.id}/verify
|
// ACC No: ${parsedData.data.accNo}\n\nVerify the user with the following link: ${process.env.BETTER_AUTH_URL}/users/${newUser.id}/verify
|
||||||
`,
|
// `,
|
||||||
phoneNumber: process.env.ADMIN_PHONENUMBER ?? "",
|
// phoneNumber: process.env.ADMIN_PHONENUMBER ?? "",
|
||||||
});
|
// });
|
||||||
return {
|
// return {
|
||||||
message:
|
// message:
|
||||||
"Your account has been requested for verification. Please wait for a response from admin.",
|
// "Your account has been requested for verification. Please wait for a response from admin.",
|
||||||
payload: formData,
|
// payload: formData,
|
||||||
db_error: "invalidPersonValidation",
|
// db_error: "invalidPersonValidation",
|
||||||
};
|
// };
|
||||||
}
|
|
||||||
|
|
||||||
if (isValidPerson) {
|
// if (isValidPerson) {
|
||||||
await authClient.phoneNumber.sendOtp({
|
// await authClient.phoneNumber.sendOtp({
|
||||||
phoneNumber: newUser.phoneNumber,
|
// phoneNumber: newUser.phoneNumber,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
redirect(
|
// redirect(
|
||||||
`/verify-otp?phone_number=${encodeURIComponent(newUser.phoneNumber)}`,
|
// `/verify-otp?phone_number=${encodeURIComponent(newUser.phoneNumber)}`,
|
||||||
);
|
// );
|
||||||
return { message: "User created successfully" };
|
// return { message: "User created successfully" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendOtp = async (phoneNumber: string, code: string) => {
|
export const sendOtp = async (phoneNumber: string, code: string) => {
|
||||||
|
@ -1,85 +1,79 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import prisma from "@/lib/db";
|
|
||||||
import { VerifyUserDetails } from "@/lib/person";
|
import { VerifyUserDetails } from "@/lib/person";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { CreateClient } from "./ninja/client";
|
import { CreateClient } from "./ninja/client";
|
||||||
|
|
||||||
export async function VerifyUser(userId: string) {
|
export async function VerifyUser(userId: string) {
|
||||||
const user = await prisma.user.findUnique({
|
// const user = await prisma.user.findUnique({
|
||||||
where: {
|
// where: {
|
||||||
id: userId,
|
// id: userId,
|
||||||
},
|
// },
|
||||||
include: {
|
// include: {
|
||||||
atoll: true,
|
// atoll: true,
|
||||||
island: true,
|
// island: true,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
if (!user) {
|
// if (!user) {
|
||||||
throw new Error("User not found");
|
// throw new Error("User not found");
|
||||||
}
|
// }
|
||||||
const isValidPerson = await VerifyUserDetails({ user });
|
// const isValidPerson = await VerifyUserDetails({ user });
|
||||||
|
// if (!isValidPerson)
|
||||||
if (!isValidPerson)
|
// throw new Error("The user details does not match national data.");
|
||||||
throw new Error("The user details does not match national data.");
|
// if (isValidPerson) {
|
||||||
|
// await prisma.user.update({
|
||||||
if (isValidPerson) {
|
// where: {
|
||||||
await prisma.user.update({
|
// id: userId,
|
||||||
where: {
|
// },
|
||||||
id: userId,
|
// data: {
|
||||||
},
|
// verified: true,
|
||||||
data: {
|
// },
|
||||||
verified: true,
|
// });
|
||||||
},
|
// const ninjaClient = await CreateClient({
|
||||||
});
|
// group_settings_id: "",
|
||||||
|
// address1: "",
|
||||||
const ninjaClient = await CreateClient({
|
// city: user.atoll?.name || "",
|
||||||
group_settings_id: "",
|
// state: user.island?.name || "",
|
||||||
address1: "",
|
// postal_code: "",
|
||||||
city: user.atoll?.name || "",
|
// country_id: "462",
|
||||||
state: user.island?.name || "",
|
// address2: user.address || "",
|
||||||
postal_code: "",
|
// contacts: {
|
||||||
country_id: "462",
|
// first_name: user.name?.split(" ")[0] || "",
|
||||||
address2: user.address || "",
|
// last_name: user.name?.split(" ")[1] || "",
|
||||||
contacts: {
|
// email: user.email || "",
|
||||||
first_name: user.name?.split(" ")[0] || "",
|
// phone: user.phoneNumber || "",
|
||||||
last_name: user.name?.split(" ")[1] || "",
|
// send_email: false,
|
||||||
email: user.email || "",
|
// custom_value1: user.dob?.toISOString().split("T")[0] || "",
|
||||||
phone: user.phoneNumber || "",
|
// custom_value2: user.id_card || "",
|
||||||
send_email: false,
|
// custom_value3: "",
|
||||||
custom_value1: user.dob?.toISOString().split("T")[0] || "",
|
// },
|
||||||
custom_value2: user.id_card || "",
|
// });
|
||||||
custom_value3: "",
|
// }
|
||||||
},
|
// revalidatePath("/users");
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
revalidatePath("/users");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function Rejectuser({
|
export async function Rejectuser({
|
||||||
userId,
|
userId,
|
||||||
reason,
|
reason,
|
||||||
}: { userId: string; reason: string }) {
|
}: { userId: string; reason: string }) {
|
||||||
const user = await prisma.user.findUnique({
|
// const user = await prisma.user.findUnique({
|
||||||
where: {
|
// where: {
|
||||||
id: userId,
|
// id: userId,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
if (!user) {
|
// if (!user) {
|
||||||
throw new Error("User not found");
|
// throw new Error("User not found");
|
||||||
}
|
// }
|
||||||
|
|
||||||
await SendUserRejectionDetailSMS({
|
// await SendUserRejectionDetailSMS({
|
||||||
details: reason,
|
// details: reason,
|
||||||
phoneNumber: user.phoneNumber,
|
// phoneNumber: user.phoneNumber,
|
||||||
});
|
// });
|
||||||
await prisma.user.delete({
|
// await prisma.user.delete({
|
||||||
where: {
|
// where: {
|
||||||
id: userId,
|
// id: userId,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
revalidatePath("/users");
|
revalidatePath("/users");
|
||||||
redirect("/users");
|
redirect("/users");
|
||||||
}
|
}
|
||||||
@ -117,13 +111,13 @@ export async function AddDevice({
|
|||||||
mac_address,
|
mac_address,
|
||||||
user_id,
|
user_id,
|
||||||
}: { name: string; mac_address: string; user_id: string }) {
|
}: { name: string; mac_address: string; user_id: string }) {
|
||||||
const newDevice = await prisma.device.create({
|
// const newDevice = await prisma.device.create({
|
||||||
data: {
|
// data: {
|
||||||
name: name,
|
// name: name,
|
||||||
mac: mac_address,
|
// mac: mac_address,
|
||||||
userId: user_id,
|
// userId: user_id,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
revalidatePath("/devices");
|
revalidatePath("/devices");
|
||||||
return newDevice;
|
// return newDevice;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import LoginForm from "@/components/auth/login-form";
|
import LoginForm from "@/components/auth/login-form";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/app/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default async function LoginPage() {
|
export default async function LoginPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-black w-full h-screen flex items-center justify-center font-sans">
|
<div className="dark:bg-black w-full h-screen flex items-center justify-center font-sans">
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full ">
|
<div className="flex flex-col items-center justify-center w-full h-full ">
|
||||||
|
@ -1,51 +1,53 @@
|
|||||||
import DevicesToPay from "@/components/devices-to-pay";
|
import DevicesToPay from "@/components/devices-to-pay";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/app/auth";
|
||||||
import prisma from "@/lib/db";
|
import prisma from "@/lib/db";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
export default async function PaymentPage({
|
export default async function PaymentPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ paymentId: string }>;
|
params: Promise<{ paymentId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: await headers()
|
headers: await headers(),
|
||||||
})
|
});
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: session?.session.userId
|
id: session?.session.userId,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
const paymentId = (await params).paymentId;
|
const paymentId = (await params).paymentId;
|
||||||
const payment = await prisma.payment.findUnique({
|
const payment = await prisma.payment.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: paymentId,
|
id: paymentId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
devices: true,
|
devices: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
|
<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
|
||||||
<h3 className="text-sarLinkOrange text-2xl">
|
<h3 className="text-sarLinkOrange text-2xl">Payment</h3>
|
||||||
Payment
|
<span
|
||||||
</h3>
|
className={cn(
|
||||||
<span className={cn("text-sm border px-4 py-2 rounded-md uppercase font-semibold", payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-700")}>
|
"text-sm border px-4 py-2 rounded-md uppercase font-semibold",
|
||||||
{payment?.paid ? "Paid" : "Pending"}
|
payment?.paid
|
||||||
</span>
|
? "text-green-500 bg-green-500/20"
|
||||||
</div>
|
: "text-yellow-500 bg-yellow-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{payment?.paid ? "Paid" : "Pending"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="user-filters"
|
id="user-filters"
|
||||||
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||||
>
|
>
|
||||||
<DevicesToPay
|
<DevicesToPay user={user || undefined} payment={payment || undefined} />
|
||||||
user={user || undefined}
|
</div>
|
||||||
payment={payment || undefined}
|
</div>
|
||||||
/>
|
);
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/app/auth";
|
||||||
import { toNextJsHandler } from "better-auth/next-js";
|
import { toNextJsHandler } from "better-auth/next-js";
|
||||||
|
|
||||||
export const { GET, POST } = toNextJsHandler(auth.handler);
|
export const { GET, POST } = toNextJsHandler(auth.handler);
|
||||||
|
108
app/auth.ts
Normal file
108
app/auth.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { logout } from "@/queries/authentication";
|
||||||
|
import type { NextAuthOptions } from "next-auth";
|
||||||
|
import type { JWT } from "next-auth/jwt";
|
||||||
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
pages: {
|
||||||
|
signIn: "/auth/signin",
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
maxAge: 30 * 60, // 30 mins
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
signOut({ token }) {
|
||||||
|
const apitoken = token.apiToken;
|
||||||
|
console.log("apitoken", apitoken);
|
||||||
|
logout({ token: apitoken as string });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
providers: [
|
||||||
|
CredentialsProvider({
|
||||||
|
name: "Credentials",
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "text", placeholder: "jsmith" },
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
async authorize(credentials) {
|
||||||
|
const { email, password } = credentials as {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
console.log("email and password", email, password);
|
||||||
|
const res = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/auth/login/`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: email,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log("status", res.status);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
console.log({ data });
|
||||||
|
switch (res.status) {
|
||||||
|
case 200:
|
||||||
|
return { ...data.user, apiToken: data.token, expiry: data.expiry };
|
||||||
|
case 400:
|
||||||
|
throw new Error(
|
||||||
|
JSON.stringify({ message: data.message, status: res.status }),
|
||||||
|
);
|
||||||
|
case 429:
|
||||||
|
throw new Error(
|
||||||
|
JSON.stringify({ message: data.message, status: res.status }),
|
||||||
|
);
|
||||||
|
case 403:
|
||||||
|
throw new Error(
|
||||||
|
JSON.stringify({ message: data.error, status: res.status }),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
JSON.stringify({
|
||||||
|
message: "FATAL: Unexprted Error occured!",
|
||||||
|
status: res.status,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
redirect: async ({ url, baseUrl }) => {
|
||||||
|
// Allows relative callback URLs
|
||||||
|
if (url.startsWith("/")) return `${baseUrl}${url}`;
|
||||||
|
return baseUrl;
|
||||||
|
},
|
||||||
|
session: async ({ session, token }) => {
|
||||||
|
const sanitizedToken = Object.keys(token).reduce((p, c) => {
|
||||||
|
// strip unnecessary properties
|
||||||
|
if (c !== "iat" && c !== "exp" && c !== "jti" && c !== "apiToken") {
|
||||||
|
Object.assign(p, { [c]: token[c] });
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}, {});
|
||||||
|
// session.expires = token.expiry
|
||||||
|
return {
|
||||||
|
...session,
|
||||||
|
user: sanitizedToken,
|
||||||
|
apiToken: token.apiToken,
|
||||||
|
// expires: token.expiry,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
jwt: ({ token, user }) => {
|
||||||
|
if (typeof user !== "undefined") {
|
||||||
|
// user has just signed in so the user object is populated
|
||||||
|
return user as unknown as JWT;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
|
};
|
@ -1,14 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/app/auth";
|
||||||
import prisma from "@/lib/db";
|
import prisma from "@/lib/db";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -17,180 +17,188 @@ import DeviceCard from "../device-card";
|
|||||||
import Pagination from "../pagination";
|
import Pagination from "../pagination";
|
||||||
|
|
||||||
export async function AdminDevicesTable({
|
export async function AdminDevicesTable({
|
||||||
searchParams,
|
searchParams,
|
||||||
parentalControl,
|
parentalControl,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
query: string;
|
query: string;
|
||||||
page: number;
|
page: number;
|
||||||
sortBy: string;
|
sortBy: string;
|
||||||
}>;
|
}>;
|
||||||
parentalControl?: boolean;
|
parentalControl?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: await headers()
|
headers: await headers(),
|
||||||
})
|
});
|
||||||
const isAdmin = session?.user.role === "ADMIN"
|
const isAdmin = session?.user.role === "ADMIN";
|
||||||
const query = (await searchParams)?.query || "";
|
const query = (await searchParams)?.query || "";
|
||||||
const page = (await searchParams)?.page;
|
const page = (await searchParams)?.page;
|
||||||
const sortBy = (await searchParams)?.sortBy || "asc";
|
const sortBy = (await searchParams)?.sortBy || "asc";
|
||||||
const totalDevices = await prisma.device.count({
|
const totalDevices = await prisma.device.count({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
name: {
|
name: {
|
||||||
contains: query || "",
|
contains: query || "",
|
||||||
mode: "insensitive",
|
mode: "insensitive",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mac: {
|
mac: {
|
||||||
contains: query || "",
|
contains: query || "",
|
||||||
mode: "insensitive",
|
mode: "insensitive",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalPages = Math.ceil(totalDevices / 10);
|
const totalPages = Math.ceil(totalDevices / 10);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
const offset = (Number(page) - 1) * limit || 0;
|
const offset = (Number(page) - 1) * limit || 0;
|
||||||
|
|
||||||
const devices = await prisma.device.findMany({
|
const devices = await prisma.device.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
name: {
|
name: {
|
||||||
contains: query || "",
|
contains: query || "",
|
||||||
mode: "insensitive",
|
mode: "insensitive",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mac: {
|
mac: {
|
||||||
contains: query || "",
|
contains: query || "",
|
||||||
mode: "insensitive",
|
mode: "insensitive",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
User: true,
|
User: true,
|
||||||
payments: true,
|
payments: true,
|
||||||
},
|
},
|
||||||
skip: offset,
|
skip: offset,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
name: `${sortBy}` as "asc" | "desc",
|
name: `${sortBy}` as "asc" | "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{devices.length === 0 ? (
|
{devices.length === 0 ? (
|
||||||
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
|
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
|
||||||
<h3>No devices yet.</h3>
|
<h3>No devices yet.</h3>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
<Table className="overflow-scroll">
|
<Table className="overflow-scroll">
|
||||||
<TableCaption>Table of all devices.</TableCaption>
|
<TableCaption>Table of all devices.</TableCaption>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Device Name</TableHead>
|
<TableHead>Device Name</TableHead>
|
||||||
<TableHead>User</TableHead>
|
<TableHead>User</TableHead>
|
||||||
<TableHead>MAC Address</TableHead>
|
<TableHead>MAC Address</TableHead>
|
||||||
<TableHead>isActive</TableHead>
|
<TableHead>isActive</TableHead>
|
||||||
<TableHead>blocked</TableHead>
|
<TableHead>blocked</TableHead>
|
||||||
<TableHead>blockedBy</TableHead>
|
<TableHead>blockedBy</TableHead>
|
||||||
<TableHead>expiryDate</TableHead>
|
<TableHead>expiryDate</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="overflow-scroll">
|
<TableBody className="overflow-scroll">
|
||||||
{devices.map((device) => (
|
{devices.map((device) => (
|
||||||
<TableRow key={device.id}>
|
<TableRow key={device.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-col items-start">
|
<div className="flex flex-col items-start">
|
||||||
<Link
|
<Link
|
||||||
className="font-medium hover:underline"
|
className="font-medium hover:underline"
|
||||||
href={`/devices/${device.id}`}
|
href={`/devices/${device.id}`}
|
||||||
>
|
>
|
||||||
{device.name}
|
{device.name}
|
||||||
</Link>
|
</Link>
|
||||||
{device.isActive && (
|
{device.isActive && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Active until{" "}
|
||||||
|
{new Date().toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<span className="text-muted-foreground">
|
{device.blocked && (
|
||||||
Active until{" "}
|
<div className="p-2 rounded border my-2">
|
||||||
{new Date().toLocaleDateString("en-US", {
|
<span>Comment: </span>
|
||||||
month: "short",
|
<p className="text-neutral-500">
|
||||||
day: "2-digit",
|
blocked because he was watching youtube
|
||||||
year: "numeric",
|
</p>
|
||||||
})}
|
</div>
|
||||||
</span>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{device.User?.name}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
{device.blocked && (
|
<TableCell className="font-medium">{device.mac}</TableCell>
|
||||||
<div className="p-2 rounded border my-2">
|
<TableCell>
|
||||||
<span>Comment: </span>
|
{device.isActive ? "Active" : "Inactive"}
|
||||||
<p className="text-neutral-500">
|
</TableCell>
|
||||||
blocked because he was watching youtube
|
<TableCell>
|
||||||
</p>
|
{device.blocked ? "Blocked" : "Not Blocked"}
|
||||||
</div>
|
</TableCell>
|
||||||
)}
|
<TableCell>
|
||||||
|
{device.blocked ? device.blockedBy : ""}
|
||||||
</div>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell className="font-medium">{device.User?.name}</TableCell>
|
{new Date().toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
<TableCell className="font-medium">{device.mac}</TableCell>
|
day: "2-digit",
|
||||||
<TableCell>
|
year: "numeric",
|
||||||
{device.isActive ? "Active" : "Inactive"}
|
})}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{device.blocked ? "Blocked" : "Not Blocked"}
|
<BlockDeviceDialog
|
||||||
</TableCell>
|
admin={isAdmin}
|
||||||
<TableCell>
|
type={device.blocked ? "unblock" : "block"}
|
||||||
{device.blocked ? device.blockedBy : ""}
|
device={device}
|
||||||
</TableCell>
|
/>
|
||||||
<TableCell>
|
</TableCell>
|
||||||
{new Date().toLocaleDateString("en-US", {
|
</TableRow>
|
||||||
month: "short",
|
))}
|
||||||
day: "2-digit",
|
</TableBody>
|
||||||
year: "numeric",
|
<TableFooter>
|
||||||
})}
|
<TableRow>
|
||||||
</TableCell>
|
<TableCell colSpan={7}>
|
||||||
<TableCell>
|
{query.length > 0 && (
|
||||||
<BlockDeviceDialog admin={isAdmin} type={device.blocked ? "unblock" : "block"} device={device} />
|
<p className="text-sm text-muted-foreground">
|
||||||
</TableCell>
|
Showing {devices.length} locations for "{query}
|
||||||
</TableRow>
|
"
|
||||||
))}
|
</p>
|
||||||
</TableBody>
|
)}
|
||||||
<TableFooter>
|
</TableCell>
|
||||||
<TableRow>
|
<TableCell className="text-muted-foreground">
|
||||||
<TableCell colSpan={7}>
|
{totalDevices} devices
|
||||||
{query.length > 0 && (
|
</TableCell>
|
||||||
<p className="text-sm text-muted-foreground">
|
</TableRow>
|
||||||
Showing {devices.length} locations for "{query}
|
</TableFooter>
|
||||||
"
|
</Table>
|
||||||
</p>
|
<Pagination totalPages={totalPages} currentPage={page} />
|
||||||
)}
|
</div>
|
||||||
</TableCell>
|
<div className="sm:hidden my-4">
|
||||||
<TableCell className="text-muted-foreground">
|
{devices.map((device) => (
|
||||||
{totalDevices} devices
|
<DeviceCard
|
||||||
</TableCell>
|
parentalControl={parentalControl}
|
||||||
</TableRow>
|
key={device.id}
|
||||||
</TableFooter>
|
device={device}
|
||||||
</Table>
|
/>
|
||||||
<Pagination totalPages={totalPages} currentPage={page} />
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden my-4">
|
</>
|
||||||
{devices.map((device) => (
|
)}
|
||||||
<DeviceCard parentalControl={parentalControl} key={device.id} device={device} />
|
</div>
|
||||||
))}
|
);
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/app/auth";
|
||||||
import prisma from "@/lib/db";
|
import prisma from "@/lib/db";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { AccountPopover } from "./account-popver";
|
import { AccountPopover } from "./account-popver";
|
||||||
@ -19,7 +19,7 @@ export async function ApplicationLayout({
|
|||||||
children,
|
children,
|
||||||
}: { children: React.ReactNode }) {
|
}: { children: React.ReactNode }) {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: await headers()
|
headers: await headers(),
|
||||||
});
|
});
|
||||||
const billFormula = await prisma.billFormula.findFirst();
|
const billFormula = await prisma.billFormula.findFirst();
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
|
@ -23,6 +23,7 @@ export default function LoginForm() {
|
|||||||
<PhoneInput
|
<PhoneInput
|
||||||
id="phone-number"
|
id="phone-number"
|
||||||
name="phoneNumber"
|
name="phoneNumber"
|
||||||
|
className="b0rder"
|
||||||
maxLength={8}
|
maxLength={8}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
placeholder="Enter phone number"
|
placeholder="Enter phone number"
|
||||||
@ -32,11 +33,7 @@ export default function LoginForm() {
|
|||||||
{state.status === "error" && (
|
{state.status === "error" && (
|
||||||
<p className="text-red-500 text-sm">{state.message}</p>
|
<p className="text-red-500 text-sm">{state.message}</p>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button className="" disabled={isPending} type="submit">
|
||||||
className=""
|
|
||||||
disabled={isPending}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{isPending ? <Loader2 className="animate-spin" /> : "Request OTP"}
|
{isPending ? <Loader2 className="animate-spin" /> : "Request OTP"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/app/auth";
|
||||||
import prisma from "@/lib/db";
|
import prisma from "@/lib/db";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import ClickableRow from "./clickable-row";
|
import ClickableRow from "./clickable-row";
|
||||||
@ -27,9 +27,9 @@ export async function DevicesTable({
|
|||||||
parentalControl?: boolean;
|
parentalControl?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: await headers()
|
headers: await headers(),
|
||||||
})
|
});
|
||||||
const isAdmin = session?.user.role === "ADMIN"
|
const isAdmin = session?.user.role === "ADMIN";
|
||||||
const query = (await searchParams)?.query || "";
|
const query = (await searchParams)?.query || "";
|
||||||
const page = (await searchParams)?.page;
|
const page = (await searchParams)?.page;
|
||||||
const sortBy = (await searchParams)?.sortBy || "asc";
|
const sortBy = (await searchParams)?.sortBy || "asc";
|
||||||
@ -53,12 +53,16 @@ export async function DevicesTable({
|
|||||||
NOT: {
|
NOT: {
|
||||||
payments: {
|
payments: {
|
||||||
some: {
|
some: {
|
||||||
paid: false
|
paid: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
isActive: isAdmin ? undefined : parentalControl,
|
isActive: isAdmin ? undefined : parentalControl,
|
||||||
blocked: isAdmin ? undefined : parentalControl !== undefined ? undefined : false,
|
blocked: isAdmin
|
||||||
|
? undefined
|
||||||
|
: parentalControl !== undefined
|
||||||
|
? undefined
|
||||||
|
: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -86,8 +90,8 @@ export async function DevicesTable({
|
|||||||
NOT: {
|
NOT: {
|
||||||
payments: {
|
payments: {
|
||||||
some: {
|
some: {
|
||||||
paid: false
|
paid: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isActive: parentalControl,
|
isActive: parentalControl,
|
||||||
@ -158,7 +162,12 @@ export async function DevicesTable({
|
|||||||
// )}
|
// )}
|
||||||
// </TableCell>
|
// </TableCell>
|
||||||
// </TableRow>
|
// </TableRow>
|
||||||
<ClickableRow admin={isAdmin} key={device.id} device={device} parentalControl={parentalControl} />
|
<ClickableRow
|
||||||
|
admin={isAdmin}
|
||||||
|
key={device.id}
|
||||||
|
device={device}
|
||||||
|
parentalControl={parentalControl}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter>
|
<TableFooter>
|
||||||
@ -181,7 +190,11 @@ export async function DevicesTable({
|
|||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden my-4">
|
<div className="sm:hidden my-4">
|
||||||
{devices.map((device) => (
|
{devices.map((device) => (
|
||||||
<DeviceCard parentalControl={parentalControl} key={device.id} device={device} />
|
<DeviceCard
|
||||||
|
parentalControl={parentalControl}
|
||||||
|
key={device.id}
|
||||||
|
device={device}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import prisma from "@/lib/db";
|
import prisma from "@/lib/db";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/app/auth";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
import { Calendar } from "lucide-react";
|
import { Calendar } from "lucide-react";
|
||||||
@ -22,219 +22,258 @@ import { Button } from "./ui/button";
|
|||||||
import { Separator } from "./ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
|
|
||||||
type PaymentWithDevices = Prisma.PaymentGetPayload<{
|
type PaymentWithDevices = Prisma.PaymentGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
devices: true;
|
devices: true;
|
||||||
};
|
};
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
export async function PaymentsTable({
|
export async function PaymentsTable({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
query: string;
|
query: string;
|
||||||
page: number;
|
page: number;
|
||||||
sortBy: string;
|
sortBy: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: await headers()
|
headers: await headers(),
|
||||||
})
|
});
|
||||||
const query = (await searchParams)?.query || "";
|
const query = (await searchParams)?.query || "";
|
||||||
const page = (await searchParams)?.page;
|
const page = (await searchParams)?.page;
|
||||||
const totalPayments = await prisma.payment.count({
|
const totalPayments = await prisma.payment.count({
|
||||||
where: {
|
where: {
|
||||||
userId: session?.session.userId,
|
userId: session?.session.userId,
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
devices: {
|
devices: {
|
||||||
every: {
|
every: {
|
||||||
name: {
|
name: {
|
||||||
contains: query || "",
|
contains: query || "",
|
||||||
mode: "insensitive",
|
mode: "insensitive",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalPages = Math.ceil(totalPayments / 10);
|
const totalPages = Math.ceil(totalPayments / 10);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
const offset = (Number(page) - 1) * limit || 0;
|
const offset = (Number(page) - 1) * limit || 0;
|
||||||
|
|
||||||
const payments = await prisma.payment.findMany({
|
const payments = await prisma.payment.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: session?.session.userId,
|
userId: session?.session.userId,
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
devices: {
|
devices: {
|
||||||
every: {
|
every: {
|
||||||
name: {
|
name: {
|
||||||
contains: query || "",
|
contains: query || "",
|
||||||
mode: "insensitive",
|
mode: "insensitive",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
devices: true
|
devices: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
skip: offset,
|
skip: offset,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: "desc",
|
createdAt: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{payments.length === 0 ? (
|
{payments.length === 0 ? (
|
||||||
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
|
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
|
||||||
<h3>No Payments yet.</h3>
|
<h3>No Payments yet.</h3>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
<Table className="overflow-scroll">
|
<Table className="overflow-scroll">
|
||||||
<TableCaption>Table of all devices.</TableCaption>
|
<TableCaption>Table of all devices.</TableCaption>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Details</TableHead>
|
<TableHead>Details</TableHead>
|
||||||
<TableHead>Duration</TableHead>
|
<TableHead>Duration</TableHead>
|
||||||
|
|
||||||
<TableHead>Amount</TableHead>
|
<TableHead>Amount</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="overflow-scroll">
|
<TableBody className="overflow-scroll">
|
||||||
{payments.map((payment) => (
|
{payments.map((payment) => (
|
||||||
<TableRow key={payment.id}>
|
<TableRow key={payment.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
|
<div
|
||||||
<div className="flex items-center gap-2">
|
className={cn(
|
||||||
<Calendar size={16} opacity={0.5} />
|
"flex flex-col items-start border rounded p-2",
|
||||||
<span className="text-muted-foreground">
|
payment?.paid
|
||||||
{new Date(payment.createdAt).toLocaleDateString("en-US", {
|
? "bg-green-500/10 border-dashed border-green=500"
|
||||||
month: "short",
|
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
|
||||||
day: "2-digit",
|
)}
|
||||||
year: "numeric",
|
>
|
||||||
})}
|
<div className="flex items-center gap-2">
|
||||||
</span>
|
<Calendar size={16} opacity={0.5} />
|
||||||
</div>
|
<span className="text-muted-foreground">
|
||||||
|
{new Date(payment.createdAt).toLocaleDateString(
|
||||||
|
"en-US",
|
||||||
|
{
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<Link className="font-medium hover:underline" href={`/payments/${payment.id}`}>
|
<Link
|
||||||
<Button size={"sm"} variant="outline">
|
className="font-medium hover:underline"
|
||||||
View Details
|
href={`/payments/${payment.id}`}
|
||||||
</Button>
|
>
|
||||||
</Link>
|
<Button size={"sm"} variant="outline">
|
||||||
<Badge className={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} variant={payment.paid ? "outline" : "secondary"}>
|
View Details
|
||||||
{payment.paid ? "Paid" : "Unpaid"}
|
</Button>
|
||||||
</Badge>
|
</Link>
|
||||||
</div>
|
<Badge
|
||||||
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
|
className={cn(
|
||||||
<h3 className="text-sm font-medium">Devices</h3>
|
payment?.paid
|
||||||
<ol className="list-disc list-inside text-sm">
|
? "text-green-500 bg-green-500/20"
|
||||||
{payment.devices.map((device) => (
|
: "text-yellow-500 bg-yellow-500/20",
|
||||||
<li key={device.id} className="text-sm text-muted-foreground">
|
)}
|
||||||
{device.name}
|
variant={payment.paid ? "outline" : "secondary"}
|
||||||
</li>
|
>
|
||||||
))}
|
{payment.paid ? "Paid" : "Unpaid"}
|
||||||
</ol>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
|
||||||
</TableCell>
|
<h3 className="text-sm font-medium">Devices</h3>
|
||||||
<TableCell className="font-medium" >
|
<ol className="list-disc list-inside text-sm">
|
||||||
{payment.numberOfMonths} Months
|
{payment.devices.map((device) => (
|
||||||
</TableCell>
|
<li
|
||||||
<TableCell>
|
key={device.id}
|
||||||
<span className="font-semibold pr-2">
|
className="text-sm text-muted-foreground"
|
||||||
{payment.amount.toFixed(2)}
|
>
|
||||||
</span>
|
{device.name}
|
||||||
MVR
|
</li>
|
||||||
</TableCell>
|
))}
|
||||||
</TableRow>
|
</ol>
|
||||||
))}
|
</div>
|
||||||
</TableBody>
|
</div>
|
||||||
<TableFooter>
|
</TableCell>
|
||||||
<TableRow>
|
<TableCell className="font-medium">
|
||||||
<TableCell colSpan={2}>
|
{payment.numberOfMonths} Months
|
||||||
{query.length > 0 && (
|
</TableCell>
|
||||||
<p className="text-sm text-muted-foreground">
|
<TableCell>
|
||||||
Showing {payments.length} locations for "{query}
|
<span className="font-semibold pr-2">
|
||||||
"
|
{payment.amount.toFixed(2)}
|
||||||
</p>
|
</span>
|
||||||
)}
|
MVR
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-muted-foreground">
|
</TableRow>
|
||||||
{totalPayments} payments
|
))}
|
||||||
</TableCell>
|
</TableBody>
|
||||||
</TableRow>
|
<TableFooter>
|
||||||
</TableFooter>
|
<TableRow>
|
||||||
</Table>
|
<TableCell colSpan={2}>
|
||||||
<Pagination totalPages={totalPages} currentPage={page} />
|
{query.length > 0 && (
|
||||||
</div>
|
<p className="text-sm text-muted-foreground">
|
||||||
<div className="sm:hidden block">
|
Showing {payments.length} locations for "{query}
|
||||||
{payments.map((payment) => (
|
"
|
||||||
<MobilePaymentDetails key={payment.id} payment={payment} />
|
</p>
|
||||||
))}
|
)}
|
||||||
</div>
|
</TableCell>
|
||||||
</>
|
<TableCell className="text-muted-foreground">
|
||||||
)}
|
{totalPayments} payments
|
||||||
</div>
|
</TableCell>
|
||||||
);
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
<Pagination totalPages={totalPages} currentPage={page} />
|
||||||
|
</div>
|
||||||
|
<div className="sm:hidden block">
|
||||||
|
{payments.map((payment) => (
|
||||||
|
<MobilePaymentDetails key={payment.id} payment={payment} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) {
|
function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
|
<div
|
||||||
<div className="flex items-center gap-2">
|
className={cn(
|
||||||
<Calendar size={16} opacity={0.5} />
|
"flex flex-col items-start border rounded p-2",
|
||||||
<span className="text-muted-foreground text-sm">
|
payment?.paid
|
||||||
{new Date(payment.createdAt).toLocaleDateString("en-US", {
|
? "bg-green-500/10 border-dashed border-green=500"
|
||||||
month: "short",
|
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
|
||||||
day: "2-digit",
|
)}
|
||||||
year: "numeric",
|
>
|
||||||
})}
|
<div className="flex items-center gap-2">
|
||||||
</span>
|
<Calendar size={16} opacity={0.5} />
|
||||||
</div>
|
<span className="text-muted-foreground text-sm">
|
||||||
|
{new Date(payment.createdAt).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<Link className="font-medium hover:underline" href={`/payments/${payment.id}`}>
|
<Link
|
||||||
<Button size={"sm"} variant="outline">
|
className="font-medium hover:underline"
|
||||||
View Details
|
href={`/payments/${payment.id}`}
|
||||||
</Button>
|
>
|
||||||
</Link>
|
<Button size={"sm"} variant="outline">
|
||||||
<Badge className={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} variant={payment.paid ? "outline" : "secondary"}>
|
View Details
|
||||||
{payment.paid ? "Paid" : "Unpaid"}
|
</Button>
|
||||||
</Badge>
|
</Link>
|
||||||
</div>
|
<Badge
|
||||||
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
|
className={cn(
|
||||||
<h3 className="text-sm font-medium">Devices</h3>
|
payment?.paid
|
||||||
<ol className="list-disc list-inside text-sm">
|
? "text-green-500 bg-green-500/20"
|
||||||
{payment.devices.map((device) => (
|
: "text-yellow-500 bg-yellow-500/20",
|
||||||
<li key={device.id} className="text-sm text-muted-foreground">
|
)}
|
||||||
{device.name}
|
variant={payment.paid ? "outline" : "secondary"}
|
||||||
</li>
|
>
|
||||||
))}
|
{payment.paid ? "Paid" : "Unpaid"}
|
||||||
</ol>
|
</Badge>
|
||||||
<div className="block sm:hidden">
|
</div>
|
||||||
<Separator className="my-2" />
|
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
|
||||||
<h3 className="text-sm font-medium">Duration</h3>
|
<h3 className="text-sm font-medium">Devices</h3>
|
||||||
<span className="text-sm text-muted-foreground">
|
<ol className="list-disc list-inside text-sm">
|
||||||
{payment.numberOfMonths} Months
|
{payment.devices.map((device) => (
|
||||||
</span>
|
<li key={device.id} className="text-sm text-muted-foreground">
|
||||||
<Separator className="my-2" />
|
{device.name}
|
||||||
<h3 className="text-sm font-medium">Amount</h3>
|
</li>
|
||||||
<span className="text-sm text-muted-foreground">
|
))}
|
||||||
{payment.amount.toFixed(2)} MVR
|
</ol>
|
||||||
</span>
|
<div className="block sm:hidden">
|
||||||
</div>
|
<Separator className="my-2" />
|
||||||
</div>
|
<h3 className="text-sm font-medium">Duration</h3>
|
||||||
</div>
|
<span className="text-sm text-muted-foreground">
|
||||||
)
|
{payment.numberOfMonths} Months
|
||||||
|
</span>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<h3 className="text-sm font-medium">Amount</h3>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{payment.amount.toFixed(2)} MVR
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
@ -5,164 +5,160 @@ import flags from "react-phone-number-input/flags";
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type PhoneInputProps = Omit<
|
type PhoneInputProps = Omit<
|
||||||
React.ComponentProps<"input">,
|
React.ComponentProps<"input">,
|
||||||
"onChange" | "value" | "ref"
|
"onChange" | "value" | "ref"
|
||||||
> &
|
> &
|
||||||
Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
|
Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
|
||||||
onChange?: (value: RPNInput.Value) => void;
|
onChange?: (value: RPNInput.Value) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
|
const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
|
||||||
React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
|
React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
|
||||||
({ className, onChange, ...props }, ref) => {
|
({ className, onChange, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<RPNInput.default
|
<RPNInput.default
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("flex", className)}
|
className={cn("flex", className)}
|
||||||
flagComponent={FlagComponent}
|
flagComponent={FlagComponent}
|
||||||
countrySelectComponent={CountrySelect}
|
countrySelectComponent={CountrySelect}
|
||||||
inputComponent={InputComponent}
|
inputComponent={InputComponent}
|
||||||
smartCaret={false}
|
smartCaret={false}
|
||||||
/**
|
/**
|
||||||
* Handles the onChange event.
|
* Handles the onChange event.
|
||||||
*
|
*
|
||||||
* react-phone-number-input might trigger the onChange event as undefined
|
* react-phone-number-input might trigger the onChange event as undefined
|
||||||
* when a valid phone number is not entered. To prevent this,
|
* when a valid phone number is not entered. To prevent this,
|
||||||
* the value is coerced to an empty string.
|
* the value is coerced to an empty string.
|
||||||
*
|
*
|
||||||
* @param {E164Number | undefined} value - The entered value
|
* @param {E164Number | undefined} value - The entered value
|
||||||
*/
|
*/
|
||||||
onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
|
onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
PhoneInput.displayName = "PhoneInput";
|
PhoneInput.displayName = "PhoneInput";
|
||||||
|
|
||||||
const InputComponent = React.forwardRef<
|
const InputComponent = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
React.ComponentProps<"input">
|
React.ComponentProps<"input">
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<Input
|
<Input className={cn("mx-2", className)} {...props} ref={ref} />
|
||||||
className={cn("rounded-e-lg rounded-s-none", className)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
InputComponent.displayName = "InputComponent";
|
InputComponent.displayName = "InputComponent";
|
||||||
|
|
||||||
type CountryEntry = { label: string; value: RPNInput.Country | undefined };
|
type CountryEntry = { label: string; value: RPNInput.Country | undefined };
|
||||||
|
|
||||||
type CountrySelectProps = {
|
type CountrySelectProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
value: RPNInput.Country;
|
value: RPNInput.Country;
|
||||||
options: CountryEntry[];
|
options: CountryEntry[];
|
||||||
onChange: (country: RPNInput.Country) => void;
|
onChange: (country: RPNInput.Country) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CountrySelect = ({
|
const CountrySelect = ({
|
||||||
disabled,
|
disabled,
|
||||||
value: selectedCountry,
|
value: selectedCountry,
|
||||||
options: countryList,
|
options: countryList,
|
||||||
onChange,
|
onChange,
|
||||||
}: CountrySelectProps) => {
|
}: CountrySelectProps) => {
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
|
className="flex gap-1 px-3 focus:z-10"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
>
|
>
|
||||||
<FlagComponent
|
<FlagComponent
|
||||||
country={selectedCountry}
|
country={selectedCountry}
|
||||||
countryName={selectedCountry}
|
countryName={selectedCountry}
|
||||||
/>
|
/>
|
||||||
<ChevronsUpDown
|
<ChevronsUpDown
|
||||||
className={cn(
|
className={cn(
|
||||||
"-mr-2 size-4 opacity-50",
|
"-mr-2 size-4 opacity-50",
|
||||||
disabled ? "hidden" : "opacity-100",
|
disabled ? "hidden" : "opacity-100",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[300px] p-0">
|
<PopoverContent className="w-[300px] p-0">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder="Search country..." />
|
<CommandInput placeholder="Search country..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<ScrollArea className="h-72">
|
<ScrollArea className="h-72">
|
||||||
<CommandEmpty>No country found.</CommandEmpty>
|
<CommandEmpty>No country found.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{countryList.map(({ value, label }) =>
|
{countryList.map(({ value, label }) =>
|
||||||
value ? (
|
value ? (
|
||||||
<CountrySelectOption
|
<CountrySelectOption
|
||||||
key={value}
|
key={value}
|
||||||
country={value}
|
country={value}
|
||||||
countryName={label}
|
countryName={label}
|
||||||
selectedCountry={selectedCountry}
|
selectedCountry={selectedCountry}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
) : null,
|
) : null,
|
||||||
)}
|
)}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CountrySelectOptionProps extends RPNInput.FlagProps {
|
interface CountrySelectOptionProps extends RPNInput.FlagProps {
|
||||||
selectedCountry: RPNInput.Country;
|
selectedCountry: RPNInput.Country;
|
||||||
onChange: (country: RPNInput.Country) => void;
|
onChange: (country: RPNInput.Country) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CountrySelectOption = ({
|
const CountrySelectOption = ({
|
||||||
country,
|
country,
|
||||||
countryName,
|
countryName,
|
||||||
selectedCountry,
|
selectedCountry,
|
||||||
onChange,
|
onChange,
|
||||||
}: CountrySelectOptionProps) => {
|
}: CountrySelectOptionProps) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem className="gap-2" onSelect={() => onChange(country)}>
|
<CommandItem className="gap-2" onSelect={() => onChange(country)}>
|
||||||
<FlagComponent country={country} countryName={countryName} />
|
<FlagComponent country={country} countryName={countryName} />
|
||||||
<span className="flex-1 text-sm">{countryName}</span>
|
<span className="flex-1 text-sm">{countryName}</span>
|
||||||
<span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
|
<span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
|
className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
|
||||||
/>
|
/>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
|
const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
|
||||||
const Flag = flags[country];
|
const Flag = flags[country];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="flex scale-125 h-4 w-6 overflow-hidden rounded-sm cursor-not-allowed">
|
<span className="flex scale-125 h-4 w-6 overflow-hidden rounded-sm cursor-not-allowed">
|
||||||
{Flag && <Flag title={countryName} />}
|
{Flag && <Flag title={countryName} />}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { PhoneInput };
|
export { PhoneInput };
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { phoneNumberClient } from "better-auth/client/plugins";
|
|
||||||
import { createAuthClient } from "better-auth/react";
|
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
|
||||||
baseURL: process.env.BETTER_AUTH_URL,
|
|
||||||
plugins: [phoneNumberClient()],
|
|
||||||
});
|
|
@ -1,5 +1,5 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/app/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import { auth } from "./auth";
|
import { auth } from "../app/auth";
|
||||||
|
|
||||||
const getCurrentUserCache = cache(async () => {
|
const getCurrentUserCache = cache(async () => {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
|
44
lib/auth.ts
44
lib/auth.ts
@ -1,44 +0,0 @@
|
|||||||
import { sendOtp } from "@/actions/auth-actions";
|
|
||||||
import { betterAuth } from "better-auth";
|
|
||||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
||||||
import { phoneNumber } from "better-auth/plugins";
|
|
||||||
import prisma from "./db";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
session: {
|
|
||||||
cookieCache: {
|
|
||||||
enabled: true,
|
|
||||||
maxAge: 10 * 60, // Cache duration in seconds
|
|
||||||
},
|
|
||||||
},
|
|
||||||
trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(",") || [
|
|
||||||
"localhost:3000",
|
|
||||||
],
|
|
||||||
user: {
|
|
||||||
additionalFields: {
|
|
||||||
role: {
|
|
||||||
type: "string",
|
|
||||||
required: false,
|
|
||||||
defaultValue: "USER",
|
|
||||||
input: false, // don't allow user to set role
|
|
||||||
},
|
|
||||||
lang: {
|
|
||||||
type: "string",
|
|
||||||
required: false,
|
|
||||||
defaultValue: "en",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
database: prismaAdapter(prisma, {
|
|
||||||
provider: "postgresql", // or "mysql", "postgresql", ...etc
|
|
||||||
}),
|
|
||||||
plugins: [
|
|
||||||
phoneNumber({
|
|
||||||
sendOTP: async ({ phoneNumber, code }) => {
|
|
||||||
// Implement sending OTP code via SMS
|
|
||||||
console.log("Send OTP in auth.ts", phoneNumber, code);
|
|
||||||
await sendOtp(phoneNumber, code);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
17
lib/db.ts
17
lib/db.ts
@ -1,17 +0,0 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
const prismaClientSingleton = () => {
|
|
||||||
return new PrismaClient();
|
|
||||||
};
|
|
||||||
|
|
||||||
type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>;
|
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as {
|
|
||||||
prisma: PrismaClientSingleton | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
|
|
||||||
|
|
||||||
export default prisma;
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
|
38
lib/types/user.ts
Normal file
38
lib/types/user.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { ISODateString } from "next-auth";
|
||||||
|
|
||||||
|
export interface Permission {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAuthUser {
|
||||||
|
expiry?: string;
|
||||||
|
token?: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
user_permissions: Permission[];
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
is_superuser: boolean;
|
||||||
|
date_joined: string;
|
||||||
|
last_login: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
user?: {
|
||||||
|
token?: string;
|
||||||
|
name?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
image?: string | null;
|
||||||
|
user?: User & {
|
||||||
|
expiry?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
expires: ISODateString;
|
||||||
|
}
|
745
package-lock.json
generated
745
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^9.3.0",
|
"@faker-js/faker": "^9.3.0",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@prisma/client": "^6.1.0",
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.1",
|
"@radix-ui/react-collapsible": "^1.1.1",
|
||||||
@ -31,7 +30,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@tanstack/react-query": "^5.61.4",
|
"@tanstack/react-query": "^5.61.4",
|
||||||
"better-auth": "^1.1.13",
|
"axios": "^1.8.4",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
@ -41,12 +40,12 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"motion": "^11.15.0",
|
"motion": "^11.15.0",
|
||||||
"next": "15.1.2",
|
"next": "15.1.2",
|
||||||
|
"next-auth": "^4.24.11",
|
||||||
"next-themes": "^0.4.3",
|
"next-themes": "^0.4.3",
|
||||||
"nextjs-toploader": "^3.7.15",
|
"nextjs-toploader": "^3.7.15",
|
||||||
"prisma": "^6.1.0",
|
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-aria-components": "^1.5.0",
|
"react-aria-components": "^1.5.0",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^9.6.3",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-hook-form": "^7.53.2",
|
"react-hook-form": "^7.53.2",
|
||||||
"react-phone-number-input": "^3.4.9",
|
"react-phone-number-input": "^3.4.9",
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import type { Atoll, DataResponse } from "@/lib/backend-types";
|
|
||||||
|
|
||||||
export async function getAtollsWithIslands(): Promise<DataResponse<Atoll>> {
|
|
||||||
const response = await fetch(
|
|
||||||
`${process.env.SARLINK_API_BASE_URL}/api/auth/atolls`,
|
|
||||||
);
|
|
||||||
return response.json();
|
|
||||||
}
|
|
48
queries/authentication.ts
Normal file
48
queries/authentication.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"use server";
|
||||||
|
import type { TAuthUser } from "@/lib/types/user";
|
||||||
|
import axiosInstance from "@/utils/axiosInstance";
|
||||||
|
|
||||||
|
export async function login({
|
||||||
|
password,
|
||||||
|
username,
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<TAuthUser> {
|
||||||
|
const response = await axiosInstance
|
||||||
|
.post("/auth/login/", {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
return res.data; // Return the data from the response
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err.response);
|
||||||
|
throw err; // Throw the error to maintain the Promise rejection
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout({ token }: { token: string }) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/auth/logout/`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Token ${token}`, // Include the token for authentication
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 204) {
|
||||||
|
throw new Error("Failed to log out from the backend");
|
||||||
|
}
|
||||||
|
console.log("logout res in backend", response);
|
||||||
|
|
||||||
|
// Since the API endpoint returns 204 No Content on success, we don't need to parse JSON
|
||||||
|
return null; // Return null to indicate a successful logout with no content
|
||||||
|
}
|
13
utils/axiosInstance.ts
Normal file
13
utils/axiosInstance.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
axios.defaults.xsrfCookieName = "csrftoken";
|
||||||
|
axios.defaults.xsrfHeaderName = "X-CSRFToken";
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||||
|
validateStatus: (status) => {
|
||||||
|
return status < 500; // Resolve only if the status code is less than 500
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default axiosInstance;
|
Loading…
x
Reference in New Issue
Block a user