Its been a while
This commit is contained in:
@@ -46,7 +46,7 @@ export default async function LandingPageLayout({ children }: { children: React.
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="w-full">
|
||||
<div className="w-full text-platinum">
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"user client"
|
||||
|
||||
import { sideNavItems } from '@/lib/dashboard';
|
||||
import { headers } from "next/headers";
|
||||
import Script from 'next/script';
|
||||
|
||||
import AccountDropdown from '../../components/AccountDropdown';
|
||||
import SideNav from "@/components/SideNav";
|
||||
import Image from 'next/image';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
||||
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const headersList = await headers();
|
||||
const pathname = headersList.get("x-current-path");
|
||||
const dashboardType = pathname?.split('/').pop() || "";
|
||||
const pageNavItems = sideNavItems[dashboardType];
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<div className="absolute bottom-0 right-0 flex flex-col justify-end items-end w-full h-full">
|
||||
<div className="w-full -mb-[2px] flex justify-start">
|
||||
<div className="w-[87vw] sm:w-[91vw] md:w-[93vw] lg:w-[94.5vw] xl:w-[95.5vw] h-20 border-b-2 border-light-bronze z-2" />
|
||||
</div>
|
||||
<div className="w-[60px] h-full flex justify-start">
|
||||
<div className="w-[32.5px] flex flex-col">
|
||||
<div className={classnames(
|
||||
"relative h-full border-r-6 rounded-out-tl-4xl rounded-tr-4xl bg-light-bronze border-atmosphere"
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full flex items-start">
|
||||
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
|
||||
<div className="w-[90.15vw] h-20 flex justify-between items-center">
|
||||
<div className={classnames('flex items-center')}>
|
||||
<div className="-ml-8.5 z-2">
|
||||
<Image
|
||||
src="/images/bbstpa-forsite.png"
|
||||
alt="Britton Logo"
|
||||
width={205}
|
||||
height={60}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full flex justify-center ml-[60px]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// /app/login/page.tsx
|
||||
'use test' // Marks this component as a client component
|
||||
'use client'
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useActionState } from 'react'
|
||||
import { signInAndFetchUser } from '@/app/actions/signin'
|
||||
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
import Link from 'next/link';
|
||||
import Button from '@/components/Button';
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
|
||||
export default function SignInPage() {
|
||||
// useActionState manages the server state (errors, loading) and form binding
|
||||
const [state, formAction, isPending] = useActionState(signInAndFetchUser, null)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full flex flex-col items-center justify-center py-6 mt-10 px-4 sm:px-6 lg:px-8"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center text-7xl font-bold text-atmosphere">
|
||||
Britton Benefits
|
||||
{/* <p>Britton</p>
|
||||
<p>Benefits</p> */}
|
||||
</div>
|
||||
<div className={classnames("w-md space-y-4", font.className)}>
|
||||
<div className="mt-10 text-center text-3xl font-extrabold text-light-bronze border-b-2 border-light-bronze">
|
||||
Sign In
|
||||
</div>
|
||||
<form action={formAction}>
|
||||
{/* Error Message Display */}
|
||||
{state?.error && (
|
||||
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">
|
||||
{state.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-1">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="relative w-full flex justify-center py-2 px-4 mt-6 border-4 border-cobalt font-bold rounded-lg text-lg text-platinum hover:text-light-bronze bg-cobalt hover:bg-deepcove hover:cursor-pointer"
|
||||
>
|
||||
{isPending ? 'Signing In...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
{/* <div className="text-bluemana">
|
||||
Not signed up?
|
||||
<Link
|
||||
href="/account/signin"
|
||||
>
|
||||
<span className="hover:text-light-bronze">Click here.</span>
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="text-bluemana mt-4 z-20">
|
||||
Not signed up?{' '}
|
||||
<Link
|
||||
href="/account/signup"
|
||||
className="underline text-atmosphere hover:text-light-bronze z-10"
|
||||
>
|
||||
Click here.
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// /app/login/page.tsx
|
||||
'use test' // Marks this component as a client component
|
||||
'use client'
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useActionState, useState } from 'react'
|
||||
import { registerUser } from '@/app/actions/signup'
|
||||
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
|
||||
export default function LoginPage() {
|
||||
// useActionState manages the server state (errors, loading) and form binding
|
||||
const [state, formAction, isPending] = useActionState(registerUser, null)
|
||||
const [role, setRole] = useState("");
|
||||
const [date, setDate] = useState("");
|
||||
|
||||
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let input = e.target.value.replace(/\D/g, "");
|
||||
|
||||
if (input.length > 2 && input.length <= 4) {
|
||||
input = `${input.slice(0, 2)}/${input.slice(2)}`;
|
||||
} else if (input.length > 4) {
|
||||
input = `${input.slice(0, 2)}/${input.slice(2, 4)}/${input.slice(4, 8)}`;
|
||||
}
|
||||
|
||||
setDate(input);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full flex flex-col items-center justify-center py-6 px-4 sm:px-6 lg:px-8"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center text-7xl font-bold text-atmosphere">
|
||||
Britton Benefits
|
||||
{/* <p>Britton</p>
|
||||
<p>Benefits</p> */}
|
||||
</div>
|
||||
<div className={classnames("w-md space-y-4", font.className)}>
|
||||
<div className="mt-10 text-center text-3xl font-extrabold text-light-bronze border-b-2 border-light-bronze">
|
||||
Sign Up
|
||||
</div>
|
||||
<div className="mb-4 text-center text-md font-extrabold text-bluemana">
|
||||
(Employer and Broker users must be invited)
|
||||
</div>
|
||||
|
||||
<form action={formAction}>
|
||||
{/* Error Message Display */}
|
||||
{state?.error && (
|
||||
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">
|
||||
{state.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="two-item-dropdown" className="block text-md font-bold text-platinum">
|
||||
I Am A...
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="role"
|
||||
name="role"
|
||||
value={role}
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
required
|
||||
className="rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
>
|
||||
{/* Placeholder option */}
|
||||
<option value="" disabled>Select User Role</option>
|
||||
|
||||
{/* The two dropdown items */}
|
||||
<option value="member">Member</option>
|
||||
<option value="provider">Provider</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{role === 'member' && (
|
||||
<>
|
||||
{/* <div className="mt-2">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Member ID</label>
|
||||
<input
|
||||
type="roleId"
|
||||
name="roleId"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="######"
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Last 4 Digits of SSN</label>
|
||||
<input
|
||||
type="lastFourSsn"
|
||||
name="lastFourSsn"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="####"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Date of Birth</label>
|
||||
<input
|
||||
type="dateOfBirth"
|
||||
name="dateOfBirth"
|
||||
value={date}
|
||||
onChange={handleDateChange}
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="MM/DD/YYYY"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{role === 'provider' && (
|
||||
<div className="mt-2">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Tax ID</label>
|
||||
<input
|
||||
type="roleId"
|
||||
name="roleId"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="######"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-1">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-1">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Confirm Password</label>
|
||||
<input
|
||||
type="confirmPassword"
|
||||
name="confirmPassword"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="relative w-full flex justify-center py-2 px-4 mt-6 border-4 border-cobalt font-bold rounded-lg text-lg text-platinum hover:text-light-bronze bg-cobalt hover:bg-deepcove hover:cursor-pointer"
|
||||
>
|
||||
{isPending ? 'Signing In...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// /app/actions/auth.ts
|
||||
'use server'
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { SignJWT } from 'jose/jwt/sign';
|
||||
|
||||
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET);
|
||||
|
||||
function roleMapping(role_number: number) {
|
||||
const map = [
|
||||
"member",
|
||||
"employer",
|
||||
"broker",
|
||||
"provider",
|
||||
"carrier",
|
||||
"admin"
|
||||
]
|
||||
|
||||
return map[role_number]
|
||||
}
|
||||
|
||||
export async function loginUser(prevState: any, formData: FormData) {
|
||||
const email = formData.get('email') as string
|
||||
const password = formData.get('password') as string
|
||||
|
||||
// Simple validation check
|
||||
if (!email || !password) {
|
||||
throw new Error('Please fill in all fields.');
|
||||
}
|
||||
|
||||
const response = await fetch(`${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const role = roleMapping(data.keychain.role)
|
||||
|
||||
const sessionPayload = {
|
||||
accessToken: data.token,
|
||||
user: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
keychain: data.keychain,
|
||||
role: role,
|
||||
role_id: data.keychain.role_id
|
||||
}
|
||||
};
|
||||
|
||||
const encryptedSession = await new SignJWT(sessionPayload)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('2h')
|
||||
.sign(SECRET_KEY);
|
||||
|
||||
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set('session', encryptedSession, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
});
|
||||
|
||||
const initUserResponse = await fetch('https://yourdomain.com', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
const userData = await initUserResponse.json();
|
||||
|
||||
// return { success: true };
|
||||
|
||||
if (role == "member") {
|
||||
redirect('/dashboard/member')
|
||||
} else if (role == "employer") {
|
||||
redirect('/dashboard/employer')
|
||||
} else if (role == "broker") {
|
||||
redirect('/dashboard/broker')
|
||||
} else if (role == "provider") {
|
||||
redirect('/dashboard/provider')
|
||||
} else if (role == "carrier") {
|
||||
redirect('/dashboard/carrier')
|
||||
} else if (role == "admin") {
|
||||
redirect('/dashboard/admin')
|
||||
} else {
|
||||
return { error: 'Invalid email or password.' }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
'use server'
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { SignJWT } from 'jose';
|
||||
|
||||
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||
|
||||
function roleMapping(role_number: number) {
|
||||
const map = [
|
||||
"member",
|
||||
"employer",
|
||||
"broker",
|
||||
"provider",
|
||||
"carrier",
|
||||
"admin"
|
||||
]
|
||||
|
||||
return map[role_number]
|
||||
}
|
||||
|
||||
export async function signInAndFetchUser(prevState: any, formData: FormData) {
|
||||
const email = formData.get('email') as string
|
||||
const password = formData.get('password') as string
|
||||
|
||||
if (!email || !password) {
|
||||
throw new Error('Please fill in all fields.');
|
||||
}
|
||||
|
||||
const authResponse = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/signin`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ user: {email, password} }),
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (!authResponse.ok) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
const { user } = await authResponse.json();
|
||||
const token = authResponse.headers.get('authorization')?.split(' ')[1];
|
||||
console.log("token: ", token)
|
||||
// const token = user.jti
|
||||
const role = user.role
|
||||
|
||||
const sessionPayload = {
|
||||
token: token,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: role,
|
||||
roleId: user.role_id,
|
||||
keychain: user.keychain
|
||||
}
|
||||
};
|
||||
|
||||
console.log("sessionPayload: ", sessionPayload)
|
||||
|
||||
const encryptedSession = await new SignJWT(sessionPayload)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('2h')
|
||||
.sign(SECRET_KEY);
|
||||
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set('session', encryptedSession, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
});
|
||||
|
||||
if (role == "member") {
|
||||
redirect('/dashboard/member')
|
||||
} else if (role == "employer") {
|
||||
redirect('/dashboard/employer')
|
||||
} else if (role == "broker") {
|
||||
redirect('/dashboard/broker')
|
||||
} else if (role == "provider") {
|
||||
redirect('/dashboard/provider')
|
||||
} else if (role == "carrier") {
|
||||
redirect('/dashboard/carrier')
|
||||
} else if (role == "admin") {
|
||||
redirect('/dashboard/admin')
|
||||
} else {
|
||||
return { error: 'Invalid email or password.' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// app/login/actions.ts
|
||||
'use server'
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { SignJWT } from 'jose';
|
||||
|
||||
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||
|
||||
function roleMapping(role_number: number) {
|
||||
const map = [
|
||||
"member",
|
||||
"employer",
|
||||
"broker",
|
||||
"provider",
|
||||
"carrier",
|
||||
"admin"
|
||||
]
|
||||
|
||||
return map[role_number]
|
||||
}
|
||||
|
||||
export async function registerUser(prevState: any, formData: FormData) {
|
||||
const email = formData.get('email') as string
|
||||
const password = formData.get('password') as string
|
||||
const confirm_password = formData.get('confirmPassword') as string
|
||||
const role = formData.get('role') as string
|
||||
const role_id = formData.get('roleId') as string
|
||||
const last_four_ssn = formData.get('lastFourSsn') as string
|
||||
const date_of_birth = formData.get('dateOfBirth') as string
|
||||
|
||||
if (!email || !password || !confirm_password || !role) {
|
||||
throw new Error('Please fill in all fields.');
|
||||
}
|
||||
if (role === "provider" && !role_id) {
|
||||
throw new Error('Please fill in all fields.');
|
||||
}
|
||||
if (role === "member" && (!date_of_birth || !last_four_ssn)) {
|
||||
throw new Error('Please fill in all fields.');
|
||||
}
|
||||
|
||||
const authResponse = await fetch(`${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/login`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ user: {email, password, confirm_password, role, role_id, last_four_ssn, date_of_birth} }),
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (!authResponse.ok) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
const data = await authResponse.json();
|
||||
const token = data.jti
|
||||
|
||||
const sessionPayload = {
|
||||
token: token,
|
||||
user: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
role: data.role,
|
||||
roleId: data.role_id,
|
||||
keychain: data.keychain
|
||||
}
|
||||
};
|
||||
|
||||
const encryptedSession = await new SignJWT(sessionPayload)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('2h')
|
||||
.sign(SECRET_KEY);
|
||||
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set('session', encryptedSession, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
});
|
||||
|
||||
if (role == "member") {
|
||||
redirect('/dashboard/member')
|
||||
} else if (role == "employer") {
|
||||
redirect('/dashboard/employer')
|
||||
} else if (role == "broker") {
|
||||
redirect('/dashboard/broker')
|
||||
} else if (role == "provider") {
|
||||
redirect('/dashboard/provider')
|
||||
} else if (role == "carrier") {
|
||||
redirect('/dashboard/carrier')
|
||||
} else if (role == "admin") {
|
||||
redirect('/dashboard/admin')
|
||||
} else {
|
||||
return { error: 'Invalid email or password.' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { isIPv4 } from 'net';
|
||||
|
||||
export async function GET() {
|
||||
// Read the runtime env variable safely on the server
|
||||
let apiUrl = "";
|
||||
const backendApiHost = process.env.BE_API_SERVER_HOST || "localhost";
|
||||
if (isIPv4(backendApiHost)) {
|
||||
apiUrl = apiUrl.concat(backendApiHost)
|
||||
} else {
|
||||
apiUrl = apiUrl.concat("127.0.0.1")
|
||||
}
|
||||
|
||||
if (process.env.BE_API_SERVER_PORT) {
|
||||
apiUrl = apiUrl.concat(":").concat(process.env.BE_API_SERVER_PORT)
|
||||
}
|
||||
|
||||
return Response.json({ apiUrl });
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import classnames from 'classnames';
|
||||
import { cookies } from "next/headers";
|
||||
import { JWTPayload, jwtVerify } from "jose";
|
||||
|
||||
import DashboardHeader from "@/components/DashboardHeader";
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
import { redirect } from 'next/navigation';
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||
|
||||
interface ClaimType {
|
||||
claim_number: number;
|
||||
visit_date: string;
|
||||
claim_status: string;
|
||||
patient_name: string;
|
||||
provider_name: string;
|
||||
}
|
||||
|
||||
interface UserJwtPayload extends JWTPayload {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
roleId: string;
|
||||
keychain: UserKeychain;
|
||||
}
|
||||
|
||||
interface UserKeychain {
|
||||
family_id: string;
|
||||
mb_member_key: number;
|
||||
pb_entity_key: number;
|
||||
pl_plan_key: number;
|
||||
};
|
||||
|
||||
export default async function EmployerIdCards() {
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get("session");
|
||||
|
||||
if (!sessionCookie) {
|
||||
redirect('/account/signin')
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Extract the raw string token from the cookie object
|
||||
const token = sessionCookie.value;
|
||||
|
||||
// 3. Verify and decode the JWT using jose
|
||||
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||
|
||||
// 3. Cast the payload to your custom interface
|
||||
const data = payload as UserJwtPayload;
|
||||
|
||||
// const userDataTest = {
|
||||
// pb_entity_key: 377527,
|
||||
// pl_plan_key: '61',
|
||||
// name: "John McMember"
|
||||
// }
|
||||
|
||||
const userData = {
|
||||
pb_entity_key: data.user.keychain.pb_entity_key,
|
||||
pl_plan_key: data.user.keychain.pl_plan_key,
|
||||
name: data.user.name
|
||||
}
|
||||
|
||||
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/claims`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json' ,
|
||||
'Authorization': `Bearer ${data.token}`
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Unauthenticated");
|
||||
|
||||
|
||||
const claimsData: ClaimType[] = await response.json()
|
||||
|
||||
return (
|
||||
<>
|
||||
{claimsData &&
|
||||
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||
<DashboardHeader
|
||||
userDisplayName={userData.name}
|
||||
/>
|
||||
<div className="flex w-full pr-23">
|
||||
<div className={classnames("w-full flex flex-col pt-4 z-2 space-x-4 space-y-4 xl:space-y-0 text-bluemana")}>
|
||||
<div className="w-full flex text-3xl ml-2 mb-4 font-semibold">
|
||||
<p>Claims</p>
|
||||
</div>
|
||||
<div className={classnames("w-full flex flex-col px-2 text-lg", font.className)}>
|
||||
<div className='flex w-full mb-4 border-b border-bluemana'>
|
||||
<div className="flex w-2/20">
|
||||
Claim #
|
||||
</div>
|
||||
<div className="flex w-4/20 pl-3">
|
||||
Patient Name
|
||||
</div>
|
||||
<div className="flex w-6/20 pl-3">
|
||||
Provider
|
||||
</div>
|
||||
<div className="flex w-3/20 pl-3">
|
||||
Visit Date
|
||||
</div>
|
||||
<div className="flex w-4/20 pl-8.5">
|
||||
Claim Status
|
||||
</div>
|
||||
</div>
|
||||
{(claimsData).map((mc) => (
|
||||
<div key={mc.claim_number} className="flex w-full text-platinum">
|
||||
<div className="flex w-2/20">
|
||||
{mc.claim_number}
|
||||
</div>
|
||||
<div className="flex w-4/20 pl-3">
|
||||
{mc.patient_name}
|
||||
</div>
|
||||
<div className="flex w-6/20 pl-3">
|
||||
{mc.provider_name}
|
||||
</div>
|
||||
<div className="flex w-3/20 pl-3">
|
||||
{mc.visit_date}
|
||||
</div>
|
||||
<div className="flex w-4/20 pl-8.5">
|
||||
{mc.claim_status}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error.message === 'NEXT_REDIRECT') throw error;
|
||||
|
||||
if (error.message === 'Unauthenticated') {
|
||||
redirect('/account/signin');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import classnames from 'classnames';
|
||||
|
||||
import DashboardHeader from "@/components/DashboardHeader";
|
||||
import FindProviderWidget from '@/components/FindProvider';
|
||||
import InsuranceCardWidget from '@/components/InsuranceCard';
|
||||
import FindRxInformationWidget from '@/components/FindRxInformation';
|
||||
import RecentClaimsWidget from '@/components/RecentClaimsWidget';
|
||||
import BenefitsAAGWidget from '@/components/BenefitsAAGWidget';
|
||||
import { cookies } from "next/headers";
|
||||
import { JWTPayload, jwtVerify } from "jose";
|
||||
import { redirect } from 'next/navigation';
|
||||
import IdCardViewer from '@/components/IdCardViewer';
|
||||
|
||||
// Ensure your secret key matches exactly what you used to sign the token
|
||||
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||
|
||||
interface PlanBenefitType {
|
||||
id: number;
|
||||
title: string;
|
||||
sequence: number;
|
||||
benefit: string;
|
||||
benefit_desc: string;
|
||||
}
|
||||
|
||||
interface RecentClaimType {
|
||||
claim_number: number;
|
||||
visit_date: string;
|
||||
claim_status: string;
|
||||
patient_name: string;
|
||||
provider_name: string;
|
||||
}
|
||||
|
||||
interface DashboardDataType {
|
||||
network_provider: string;
|
||||
plan_benefits: PlanBenefitType[];
|
||||
recent_claims: RecentClaimType[];
|
||||
}
|
||||
|
||||
interface UserJwtPayload extends JWTPayload {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
roleId: string;
|
||||
keychain: UserKeychain;
|
||||
}
|
||||
|
||||
interface UserKeychain {
|
||||
family_id: string;
|
||||
mb_member_key: number;
|
||||
pb_entity_key: number;
|
||||
pl_plan_key: number;
|
||||
};
|
||||
|
||||
export default async function Member() {
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
|
||||
|
||||
if (!sessionCookie) {
|
||||
redirect('/account/signin')
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Extract the raw string token from the cookie object
|
||||
const token = sessionCookie.value;
|
||||
|
||||
// 3. Verify and decode the JWT using jose
|
||||
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||
|
||||
// 3. Cast the payload to your custom interface
|
||||
const data = payload as UserJwtPayload;
|
||||
|
||||
// const userDataTest = {
|
||||
// pb_entity_key: 377527,
|
||||
// pl_plan_key: '61',
|
||||
// name: "John McMember"
|
||||
// }
|
||||
|
||||
const userData = {
|
||||
pb_entity_key: data.user.keychain.pb_entity_key,
|
||||
pl_plan_key: data.user.keychain.pl_plan_key,
|
||||
name: data.user.name
|
||||
}
|
||||
|
||||
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/initialize_dashboard`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json' ,
|
||||
'Authorization': `Bearer ${data.token}`
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Unauthenticated");
|
||||
|
||||
const dashboardData: DashboardDataType = await response.json()
|
||||
|
||||
return (
|
||||
<>
|
||||
{dashboardData &&
|
||||
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||
<DashboardHeader
|
||||
userDisplayName={userData.name}
|
||||
/>
|
||||
<div className="flex w-full pr-23">
|
||||
<div className={classnames("w-full flex flex-col xl:flex-row pt-4 z-2 space-x-4 space-y-4 xl:space-y-0")}>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col w-full h-full xl:h-auto lg:flex-row gap-y-4 lg:gap-y-0 lg:gap-x-4">
|
||||
{/* <DeductableProgressWidget /> */}
|
||||
<IdCardViewer
|
||||
pbEntityKey={userData.pb_entity_key}
|
||||
cardLaout="FullPageCard"
|
||||
/>
|
||||
<FindProviderWidget
|
||||
// pl_plan_key='57'
|
||||
networkProvider={dashboardData.network_provider}
|
||||
/>
|
||||
</div>
|
||||
<FindRxInformationWidget />
|
||||
<RecentClaimsWidget
|
||||
recentClaims={dashboardData.recent_claims}
|
||||
/>
|
||||
</div>
|
||||
<BenefitsAAGWidget
|
||||
planBenefits={dashboardData.plan_benefits}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error.message === 'NEXT_REDIRECT') throw error;
|
||||
|
||||
if (error.message === 'Unauthenticated') {
|
||||
redirect('/account/signin');
|
||||
}
|
||||
}
|
||||
// } catch (error) {
|
||||
// // Fails if token is expired, tampered with, or invalid
|
||||
// console.error("JWT verification failed:", error);
|
||||
// return <div>Session invalid or expired.</div>;
|
||||
// }
|
||||
}
|
||||
@@ -1,63 +1,100 @@
|
||||
"user client"
|
||||
|
||||
import { sideNavItems } from '@/lib/dashboard';
|
||||
import { headers } from "next/headers";
|
||||
import Script from 'next/script';
|
||||
import { cookies } from "next/headers";
|
||||
import { JWTPayload, jwtVerify } from "jose";
|
||||
|
||||
import AccountDropdown from '../../components/AccountDropdown';
|
||||
import SideNav from "@/components/SideNav";
|
||||
import Image from 'next/image';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||
|
||||
interface UserJwtPayload extends JWTPayload {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
roleId: string;
|
||||
}
|
||||
|
||||
|
||||
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const headersList = await headers();
|
||||
const pathname = headersList.get("x-current-path");
|
||||
const dashboardType = pathname?.split('/').pop() || "";
|
||||
const pageNavItems = sideNavItems[dashboardType];
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<Script src={`https://maps.googleapis.com/maps/api/js?key=${process.env.GOOGLE_PLACES_API_KEY}&libraries=places`} strategy="beforeInteractive" />
|
||||
<div className="absolute bottom-0 right-0 flex flex-col justify-end items-end w-full h-full">
|
||||
<div className="w-full -mb-[2px] flex justify-start">
|
||||
<div className="w-[87vw] sm:w-[91vw] md:w-[93vw] lg:w-[94.5vw] xl:w-[95.5vw] h-20 border-b-2 border-copper z-2" />
|
||||
</div>
|
||||
<div className="w-[60px] h-full flex justify-start">
|
||||
<div className="w-[32.5px] flex flex-col">
|
||||
<div className={classnames(
|
||||
"relative h-full border-r-6 rounded-out-tl-4xl rounded-tr-4xl bg-copper border-atmosphere"
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full flex items-start">
|
||||
<SideNav
|
||||
maxWidth="w-[16vw]"
|
||||
bgColor="bg-atmosphere"
|
||||
navItems={pageNavItems}
|
||||
/>
|
||||
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
|
||||
<div className="w-[90.15vw] h-20 flex justify-between items-center">
|
||||
<div className={classnames('flex items-center')}>
|
||||
<div className="-ml-8.5 z-2">
|
||||
<Image
|
||||
src="/images/bbstpa-forsite.png"
|
||||
alt="Britton Logo"
|
||||
width={205}
|
||||
height={60}
|
||||
/>
|
||||
if (!sessionCookie) {
|
||||
return <div className='flex w-full justify-center'>No active session. Please log in.</div>;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Extract the raw string token from the cookie object
|
||||
const token = sessionCookie.value;
|
||||
|
||||
// 3. Verify and decode the JWT using jose
|
||||
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||
|
||||
// 3. Cast the payload to your custom interface
|
||||
const data = payload as UserJwtPayload;
|
||||
|
||||
const userRole = data.user.role
|
||||
|
||||
// const headersList = await headers();
|
||||
// const pathname = headersList.get("x-current-path");
|
||||
// const dashboardType = pathname?.split('/').pop() || "";
|
||||
const pageNavItems = sideNavItems[userRole];
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<div className="absolute bottom-0 right-0 flex flex-col justify-end items-end w-full h-full">
|
||||
<div className="w-full -mb-[2px] flex justify-start">
|
||||
<div className="w-[87vw] sm:w-[91vw] md:w-[93vw] lg:w-[94.5vw] xl:w-[95.5vw] h-20 border-b-2 border-copper z-2" />
|
||||
</div>
|
||||
<div className="w-[60px] h-full flex justify-start">
|
||||
<div className="w-[32.5px] flex flex-col">
|
||||
<div className={classnames(
|
||||
"relative h-full border-r-6 rounded-out-tl-4xl rounded-tr-4xl bg-copper border-atmosphere"
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pr-20 z-2">
|
||||
<AccountDropdown userRole="member" />
|
||||
</div>
|
||||
<div className="w-full h-full flex items-start">
|
||||
<SideNav
|
||||
maxWidth="w-[16vw]"
|
||||
bgColor="bg-atmosphere"
|
||||
navItems={pageNavItems}
|
||||
/>
|
||||
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
|
||||
<div className="w-[90.15vw] h-20 flex justify-between items-center">
|
||||
<div className={classnames('flex items-center')}>
|
||||
<div className="-ml-4.5 z-2">
|
||||
<Image
|
||||
src="/images/bbstpa-forsite.png"
|
||||
alt="Britton Logo"
|
||||
width={205}
|
||||
height={60}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pr-20 z-2">
|
||||
<AccountDropdown userRole="member" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full flex justify-start">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full flex justify-start">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
// Fails if token is expired, tampered with, or invalid
|
||||
console.error("JWT verification failed:", error);
|
||||
return <div className='flex w-full justify-center'>Session invalid or expired.</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// /app/login/page.tsx
|
||||
'use test' // Marks this component as a client component
|
||||
'use client'
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useActionState } from 'react'
|
||||
import { loginAndFetchUser } from '@/app/actions/signin'
|
||||
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
|
||||
export default function LoginPage() {
|
||||
// useActionState manages the server state (errors, loading) and form binding
|
||||
const [state, formAction, isPending] = useActionState(loginAndFetchUser, null)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-full flex flex-col items-center justify-center py-6 px-4 sm:px-6 lg:px-8"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center text-7xl font-bold text-atmosphere">
|
||||
Britton Benefits
|
||||
{/* <p>Britton</p>
|
||||
<p>Benefits</p> */}
|
||||
</div>
|
||||
<div className={classnames("w-sm space-y-4", font.className)}>
|
||||
<div className="mt-10 text-center text-3xl font-extrabold text-light-bronze border-b-1 border-light-bronze">
|
||||
Sign In
|
||||
</div>
|
||||
|
||||
<form action={formAction}>
|
||||
{/* Error Message Display */}
|
||||
{state?.error && (
|
||||
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">
|
||||
{state.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-1">
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<label className="block text-md font-bold text-platinum">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="relative w-full flex justify-center py-2 px-4 mt-6 border-4 border-cobalt font-bold rounded-lg text-lg text-platinum hover:text-light-bronze bg-cobalt hover:bg-deepcove hover:cursor-pointer"
|
||||
>
|
||||
{isPending ? 'Signing In...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import classnames from 'classnames';
|
||||
import { cookies } from "next/headers";
|
||||
import { JWTPayload, jwtVerify } from "jose";
|
||||
|
||||
import DashboardHeader from "@/components/DashboardHeader";
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
import { redirect } from 'next/navigation';
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||
|
||||
interface ClaimType {
|
||||
claim_number: number;
|
||||
visit_date: string;
|
||||
claim_status: string;
|
||||
patient_name: string;
|
||||
provider_name: string;
|
||||
}
|
||||
|
||||
interface UserJwtPayload extends JWTPayload {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
roleId: string;
|
||||
keychain: UserKeychain;
|
||||
}
|
||||
|
||||
interface UserKeychain {
|
||||
family_id: string;
|
||||
mb_member_key: number;
|
||||
pb_entity_key: number;
|
||||
pl_plan_key: number;
|
||||
};
|
||||
|
||||
export default async function MemberClaims() {
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get("session");
|
||||
|
||||
if (!sessionCookie) {
|
||||
redirect('/account/signin')
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Extract the raw string token from the cookie object
|
||||
const token = sessionCookie.value;
|
||||
|
||||
// 3. Verify and decode the JWT using jose
|
||||
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||
|
||||
// 3. Cast the payload to your custom interface
|
||||
const data = payload as UserJwtPayload;
|
||||
|
||||
// const userDataTest = {
|
||||
// pb_entity_key: 377527,
|
||||
// pl_plan_key: '61',
|
||||
// name: "John McMember"
|
||||
// }
|
||||
|
||||
const userData = {
|
||||
pb_entity_key: data.user.keychain.pb_entity_key,
|
||||
pl_plan_key: data.user.keychain.pl_plan_key,
|
||||
name: data.user.name
|
||||
}
|
||||
|
||||
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/claims`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json' ,
|
||||
'Authorization': `Bearer ${data.token}`
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Unauthenticated");
|
||||
|
||||
|
||||
const claimsData: ClaimType[] = await response.json()
|
||||
|
||||
return (
|
||||
<>
|
||||
{claimsData &&
|
||||
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||
<DashboardHeader
|
||||
userDisplayName={userData.name}
|
||||
/>
|
||||
<div className="flex w-full pr-23">
|
||||
<div className={classnames("w-full flex flex-col pt-4 z-2 space-x-4 space-y-4 xl:space-y-0 text-bluemana")}>
|
||||
<div className="w-full flex text-3xl ml-2 mb-4 font-semibold">
|
||||
<p>Claims</p>
|
||||
</div>
|
||||
<div className={classnames("w-full flex flex-col px-2 text-lg", font.className)}>
|
||||
<div className='flex w-full mb-4 border-b border-bluemana'>
|
||||
<div className="flex w-2/20">
|
||||
Claim #
|
||||
</div>
|
||||
<div className="flex w-4/20 pl-3">
|
||||
Patient Name
|
||||
</div>
|
||||
<div className="flex w-6/20 pl-3">
|
||||
Provider
|
||||
</div>
|
||||
<div className="flex w-3/20 pl-3">
|
||||
Visit Date
|
||||
</div>
|
||||
<div className="flex w-4/20 pl-8.5">
|
||||
Claim Status
|
||||
</div>
|
||||
</div>
|
||||
{(claimsData).map((mc) => (
|
||||
<div key={mc.claim_number} className="flex w-full text-platinum">
|
||||
<div className="flex w-2/20">
|
||||
{mc.claim_number}
|
||||
</div>
|
||||
<div className="flex w-4/20 pl-3">
|
||||
{mc.patient_name}
|
||||
</div>
|
||||
<div className="flex w-6/20 pl-3">
|
||||
{mc.provider_name}
|
||||
</div>
|
||||
<div className="flex w-3/20 pl-3">
|
||||
{mc.visit_date}
|
||||
</div>
|
||||
<div className="flex w-4/20 pl-8.5">
|
||||
{mc.claim_status}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error.message === 'NEXT_REDIRECT') throw error;
|
||||
|
||||
if (error.message === 'Unauthenticated') {
|
||||
redirect('/account/signin');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,149 @@
|
||||
import classnames from 'classnames';
|
||||
|
||||
import DashboardHeader from "@/components/DashboardHeader";
|
||||
import FindProvider from '@/components/FindProvider';
|
||||
import FindProviderWidget from '@/components/FindProvider';
|
||||
import InsuranceCardWidget from '@/components/InsuranceCard';
|
||||
import DeductableProgressWidget from '@/components/DeductableProgressWidget';
|
||||
import FindRxInformationWidget from '@/components/FindRxInformation';
|
||||
import RecentClaimsWidget from '@/components/RecentClaimsWidget';
|
||||
import BenefitsAAGWidget from '@/components/BenefitsAAGWidget';
|
||||
import { cookies } from "next/headers";
|
||||
import { JWTPayload, jwtVerify } from "jose";
|
||||
import { redirect } from 'next/navigation';
|
||||
import IdCardViewer from '@/components/IdCardViewer';
|
||||
|
||||
export default function Member() {
|
||||
return (
|
||||
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||
<DashboardHeader
|
||||
userDisplayName="Robert La'Blah"
|
||||
/>
|
||||
<div className="flex w-full pr-23">
|
||||
<div className={classnames("w-full flex flex-col xl:flex-row pt-4 z-2 space-x-4 space-y-4 xl:space-y-0")}>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col w-full h-full xl:h-auto lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4">
|
||||
<DeductableProgressWidget />
|
||||
<InsuranceCardWidget />
|
||||
// Ensure your secret key matches exactly what you used to sign the token
|
||||
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||
|
||||
interface PlanBenefitType {
|
||||
id: number;
|
||||
title: string;
|
||||
sequence: number;
|
||||
benefit: string;
|
||||
benefit_desc: string;
|
||||
}
|
||||
|
||||
interface RecentClaimType {
|
||||
claim_number: number;
|
||||
visit_date: string;
|
||||
claim_status: string;
|
||||
patient_name: string;
|
||||
provider_name: string;
|
||||
}
|
||||
|
||||
interface DashboardDataType {
|
||||
network_provider: string;
|
||||
plan_benefits: PlanBenefitType[];
|
||||
recent_claims: RecentClaimType[];
|
||||
}
|
||||
|
||||
interface UserJwtPayload extends JWTPayload {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
roleId: string;
|
||||
keychain: UserKeychain;
|
||||
}
|
||||
|
||||
interface UserKeychain {
|
||||
family_id: string;
|
||||
mb_member_key: number;
|
||||
pb_entity_key: number;
|
||||
pl_plan_key: number;
|
||||
};
|
||||
|
||||
export default async function Member() {
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
|
||||
|
||||
if (!sessionCookie) {
|
||||
redirect('/account/signin')
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Extract the raw string token from the cookie object
|
||||
const token = sessionCookie.value;
|
||||
|
||||
// 3. Verify and decode the JWT using jose
|
||||
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||
|
||||
// 3. Cast the payload to your custom interface
|
||||
const data = payload as UserJwtPayload;
|
||||
|
||||
// const userDataTest = {
|
||||
// pb_entity_key: 377527,
|
||||
// pl_plan_key: '61',
|
||||
// name: "John McMember"
|
||||
// }
|
||||
|
||||
const userData = {
|
||||
pb_entity_key: data.user.keychain.pb_entity_key,
|
||||
pl_plan_key: data.user.keychain.pl_plan_key,
|
||||
name: data.user.name
|
||||
}
|
||||
|
||||
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/initialize_dashboard`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json' ,
|
||||
'Authorization': `Bearer ${data.token}`
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Unauthenticated");
|
||||
|
||||
const dashboardData: DashboardDataType = await response.json()
|
||||
|
||||
return (
|
||||
<>
|
||||
{dashboardData &&
|
||||
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||
<DashboardHeader
|
||||
userDisplayName={userData.name}
|
||||
/>
|
||||
<div className="flex w-full pr-23">
|
||||
<div className={classnames("w-full flex flex-col xl:flex-row pt-4 z-2 space-x-4 space-y-4 xl:space-y-0")}>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col w-full h-full xl:h-auto lg:flex-row gap-y-4 lg:gap-y-0 lg:gap-x-4">
|
||||
{/* <DeductableProgressWidget /> */}
|
||||
<IdCardViewer
|
||||
pbEntityKey={userData.pb_entity_key}
|
||||
cardLaout="FullPageCard"
|
||||
/>
|
||||
<FindProviderWidget
|
||||
// pl_plan_key='57'
|
||||
networkProvider={dashboardData.network_provider}
|
||||
/>
|
||||
</div>
|
||||
<FindRxInformationWidget />
|
||||
<RecentClaimsWidget
|
||||
recentClaims={dashboardData.recent_claims}
|
||||
/>
|
||||
</div>
|
||||
<FindProvider />
|
||||
<RecentClaimsWidget />
|
||||
<BenefitsAAGWidget
|
||||
planBenefits={dashboardData.plan_benefits}
|
||||
/>
|
||||
</div>
|
||||
<BenefitsAAGWidget />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error.message === 'NEXT_REDIRECT') throw error;
|
||||
|
||||
if (error.message === 'Unauthenticated') {
|
||||
redirect('/account/signin');
|
||||
}
|
||||
}
|
||||
// } catch (error) {
|
||||
// // Fails if token is expired, tampered with, or invalid
|
||||
// console.error("JWT verification failed:", error);
|
||||
// return <div>Session invalid or expired.</div>;
|
||||
// }
|
||||
}
|
||||
+1
-1
@@ -19,7 +19,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<main className={classNames("bg-deepcove min-h-screen flex justify-center", font.className)}>
|
||||
<main className={classNames("bg-deepcove h-full min-h-screen flex justify-center", font.className)}>
|
||||
<div className="w-full max-w-[1920px]">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
+26
-46
@@ -4,33 +4,11 @@ import InlineLink from "../components/InlineLink";
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
import { getClient } from "@/lib/graphqlClient";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
const HOME_GET_DATA_QUERY = gql`
|
||||
query {
|
||||
homePage {
|
||||
headline
|
||||
section1Header
|
||||
section1Body
|
||||
section1Cta
|
||||
section2Header
|
||||
section2Body
|
||||
section2Cta
|
||||
section3Header
|
||||
section3Body
|
||||
section3Cta
|
||||
section4Header
|
||||
section4Body
|
||||
section4Cta
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default async function Home() {
|
||||
const { data } = await getClient().query({
|
||||
query: HOME_GET_DATA_QUERY,
|
||||
});
|
||||
// const { data } = await getClient().query({
|
||||
// query: HOME_GET_DATA_QUERY,
|
||||
// });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -41,40 +19,42 @@ export default async function Home() {
|
||||
textTop="Healthcare that's"
|
||||
textBottom="truly affordable"
|
||||
/>
|
||||
<div className="pt-4">
|
||||
<div className="pt-10">
|
||||
<InfoCard
|
||||
title={data.homePage.section1Header}
|
||||
learnMoreUrl={data.homePage.section1Cta}
|
||||
imageUrl="/images/members-stock.jpg"
|
||||
title="Members"
|
||||
learnMoreUrl="/members"
|
||||
imageUrl="/images/member-image.jpg"
|
||||
>
|
||||
{data.homePage.section1Body}
|
||||
What we do for members
|
||||
</InfoCard>
|
||||
</div>
|
||||
<div className='bg-atmosphere rounded-3xl'>
|
||||
<InfoCard
|
||||
title={data.homePage.section2Header}
|
||||
learnMoreUrl={data.homePage.section2Cta}
|
||||
imageUrl='/images/employers-stock.jpg'
|
||||
title="Employers"
|
||||
learnMoreUrl="/employers"
|
||||
imageUrl='/images/employer-image.jpg'
|
||||
invert
|
||||
>
|
||||
{data.homePage.section2Body}
|
||||
What we do for employers
|
||||
</InfoCard>
|
||||
</div>
|
||||
<InfoCard
|
||||
title={data.homePage.section3Header}
|
||||
learnMoreUrl={data.homePage.section3Cta}
|
||||
imageUrl="/images/providers-stock.jpg"
|
||||
>
|
||||
{data.homePage.section3Body}
|
||||
</InfoCard>
|
||||
<div className='bg-platinum rounded-3xl'>
|
||||
<div className="w-full">
|
||||
<InfoCard
|
||||
title={data.homePage.section4Header}
|
||||
learnMoreUrl={data.homePage.section4Cta}
|
||||
imageUrl="/images/brokers-stock.jpg"
|
||||
title="Providers"
|
||||
learnMoreUrl="/providers"
|
||||
imageUrl="/images/provider-image.png"
|
||||
>
|
||||
What we do for providers
|
||||
</InfoCard>
|
||||
</div>
|
||||
<div className='bg-light-bronze rounded-3xl'>
|
||||
<InfoCard
|
||||
title="Brokers"
|
||||
learnMoreUrl="/brokers"
|
||||
imageUrl="/images/broker-image.jpg"
|
||||
invert
|
||||
>
|
||||
{data.homePage.section4Body}
|
||||
What we do for brokers
|
||||
</InfoCard>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
Reference in New Issue
Block a user