more stuff

This commit is contained in:
2025-01-31 00:35:44 -05:00
parent 782384cad6
commit 88d7d6c7b1
14 changed files with 694 additions and 43 deletions

View File

@@ -21,6 +21,8 @@
"@mui/styles": "^6.1.7", "@mui/styles": "^6.1.7",
"@mui/system": "^6.1.7", "@mui/system": "^6.1.7",
"@mui/x-data-grid": "^7.22.2", "@mui/x-data-grid": "^7.22.2",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-alert-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
@@ -59,6 +61,7 @@
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-icons": "^5.3.0", "react-icons": "^5.3.0",
"sha2": "link:@oslojs/crypto/sha2",
"sonner": "^1.7.2", "sonner": "^1.7.2",
"stripe": "^17.6.0", "stripe": "^17.6.0",
"superjson": "^2.2.2", "superjson": "^2.2.2",

25
pnpm-lock.yaml generated
View File

@@ -44,6 +44,12 @@ importers:
'@mui/x-data-grid': '@mui/x-data-grid':
specifier: ^7.22.2 specifier: ^7.22.2
version: 7.24.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react@18.2.0))(@mui/material@6.4.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@6.4.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 7.24.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react@18.2.0))(@mui/material@6.4.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@6.4.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react@18.2.0))(@types/react@18.3.18)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@oslojs/crypto':
specifier: ^1.0.1
version: 1.0.1
'@oslojs/encoding':
specifier: ^1.1.0
version: 1.1.0
'@radix-ui/react-alert-dialog': '@radix-ui/react-alert-dialog':
specifier: ^1.1.5 specifier: ^1.1.5
version: 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -158,6 +164,9 @@ importers:
react-icons: react-icons:
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.4.0(react@18.2.0) version: 5.4.0(react@18.2.0)
sha2:
specifier: link:@oslojs/crypto/sha2
version: link:@oslojs/crypto/sha2
sonner: sonner:
specifier: ^1.7.2 specifier: ^1.7.2
version: 1.7.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 1.7.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -6721,8 +6730,8 @@ snapshots:
'@typescript-eslint/parser': 8.21.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/parser': 8.21.0(eslint@8.57.1)(typescript@5.7.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1) eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.4(eslint@8.57.1) eslint-plugin-react: 7.37.4(eslint@8.57.1)
eslint-plugin-react-hooks: 5.1.0(eslint@8.57.1) eslint-plugin-react-hooks: 5.1.0(eslint@8.57.1)
@@ -6741,7 +6750,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1): eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.0 debug: 4.4.0
@@ -6753,22 +6762,22 @@ snapshots:
is-glob: 4.0.3 is-glob: 4.0.3
stable-hash: 0.0.4 stable-hash: 0.0.4
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): eslint-module-utils@2.12.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.21.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/parser': 8.21.0(eslint@8.57.1)(typescript@5.7.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1) eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.8 array-includes: 3.1.8
@@ -6779,7 +6788,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3

26
src/app/(main)/util.ts Normal file
View File

@@ -0,0 +1,26 @@
export const AUTHENTICATION_ERROR_MESSAGE =
"You must be logged in to view this content";
export const PRIVATE_GROUP_ERROR_MESSAGE =
"You do not have permission to view this group";
export const AuthenticationError = class AuthenticationError extends Error {
constructor() {
super(AUTHENTICATION_ERROR_MESSAGE);
this.name = "AuthenticationError";
}
};
export const PrivateGroupAccessError = class PrivateGroupAccessError extends Error {
constructor() {
super(PRIVATE_GROUP_ERROR_MESSAGE);
this.name = "PrivateGroupAccessError";
}
};
export const NotFoundError = class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = "NotFoundError";
}
};

View File

@@ -16,24 +16,28 @@ const partsData = [
}, },
{ {
name: "Lower Parts Kit", name: "Lower Parts Kit",
link: "/lowers",
source: "-", source: "-",
price: "-", price: "-",
ship_price: "-", ship_price: "-",
}, },
{ {
name: "Lower Parts Kit", name: "Lower Parts Kit",
link: "/lowers",
source: "-", source: "-",
price: "-", price: "-",
ship_price: "-", ship_price: "-",
}, },
{ {
name: "Lower Parts Kit", name: "Lower Parts Kit",
link: "/lowers",
source: "-", source: "-",
price: "-", price: "-",
ship_price: "-", ship_price: "-",
}, },
{ {
name: "Lower Parts Kit", name: "Lower Parts Kit",
link: "/lowers",
source: "-", source: "-",
price: "-", price: "-",
ship_price: "-", ship_price: "-",
@@ -43,15 +47,11 @@ const partsData = [
{ {
group: "Upper Parts", group: "Upper Parts",
parts: [ parts: [
{ name: "Upper Reciever", source: "-", price: "-", ship_price: "-" }, { name: "Upper Reciever", link: "/lowers", source: "-", price: "-", ship_price: "-" },
{ name: "Barrel", source: "-", price: "-", ship_price: "-" }, { name: "Barrel", link: "/lowers", source: "-", price: "-", ship_price: "-" },
{ name: "BCG", source: "-", price: "-", ship_price: "-" }, { name: "BCG", link: "/lowers", source: "-", price: "-", ship_price: "-" },
{ name: "Muzzle Device", source: "-", price: "-", ship_price: "-" }, { name: "Muzzle Device", link: "/lowers", source: "-", price: "-", ship_price: "-" },
{ { name: "Charging Handle", link: "/lowers", source: "-", price: "-", ship_price: "-",
name: "Charging Handle",
source: "-",
price: "-",
ship_price: "-",
}, },
], ],
}, },
@@ -142,7 +142,7 @@ export default function BuilderPage() {
aria-hidden="true" aria-hidden="true"
className="-ml-0.5 size-5" className="-ml-0.5 size-5"
/> />
Purchase123 Purchase
</button> </button>
</td> </td>
</tr> </tr>

View File

@@ -0,0 +1,23 @@
import { getProductType } from "@queries/PSA";
import styles from '../styles.module.css';
import PageHero from "@components/PageHero";
import SortTable from "@components/SortTable";
import { Suspense } from "react";
import Loading from "@src/components/Loading/loading";
export default async function BarrelsPage() {
const data = await getProductType('Barrels');
return (
<div>
<PageHero title="Accessories" />
<div className="container mx-auto">
<Suspense fallback="Loading...">
<SortTable data={data}></SortTable>
</Suspense>
</div>
</div>
);
}

View File

@@ -0,0 +1,23 @@
"use client";
import PlusCircleIcon from "@heroicons/react/24/outline/PlusCircleIcon";
export default async function ButtonOnClick(props:any) {
const handleClick = async () => {
alert("This feature is coming soon");
}
return (
<button
type="button"
onClick={handleClick}
className="inline-flex items-center gap-x-1.5 rounded-xl bg-lime-800 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-lime-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lime-900"
>
Edit
<PlusCircleIcon
aria-hidden="true"
className="-mr-0.5 size-5"
/>
</button>
)
}

View File

@@ -3,12 +3,17 @@ import { PlusCircleIcon } from "@heroicons/react/20/solid";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import ButtonOnClick from "./ButtonOnClick";
export default async function UsersTable(props: any) { export default async function UsersTable(props: any) {
const onClick = () => {
alert("This feature is coming soon");
}
return ( return (
<div className="pb-12"> <div className="pb-12">
<div className="mt-8 flow-root"> <div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> <div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
@@ -20,7 +25,7 @@ export default async function UsersTable(props: any) {
className="py-3.5 pl-4 pr-3 text-left text-xs font-semibold text-gray-900 " className="py-3.5 pl-4 pr-3 text-left text-xs font-semibold text-gray-900 "
> >
<a href="#" className="group inline-flex"> <a href="#" className="group inline-flex">
{props.newColumnHeadings.getHeading()} {props.newColumnHeadings.getHeading()}
<span className="invisible ml-2 flex-none rounded text-gray-400 group-hover:visible group-focus:visible"> <span className="invisible ml-2 flex-none rounded text-gray-400 group-hover:visible group-focus:visible">
<ChevronDownIcon <ChevronDownIcon
aria-hidden="true" aria-hidden="true"
@@ -48,7 +53,7 @@ export default async function UsersTable(props: any) {
className="px-3 py-3.5 text-left text-xs font-semibold text-gray-900" className="px-3 py-3.5 text-left text-xs font-semibold text-gray-900"
> >
<a href="#" className="group inline-flex"> <a href="#" className="group inline-flex">
{props.newColumnHeadings.getHeading()} {props.newColumnHeadings.getHeading()}
<span className="ml-2 flex-none rounded bg-gray-100 text-gray-900 group-hover:bg-gray-200"> <span className="ml-2 flex-none rounded bg-gray-100 text-gray-900 group-hover:bg-gray-200">
<ChevronDownIcon <ChevronDownIcon
aria-hidden="true" aria-hidden="true"
@@ -62,7 +67,7 @@ export default async function UsersTable(props: any) {
className="px-3 py-3.5 text-left text-xs font-semibold text-gray-900" className="px-3 py-3.5 text-left text-xs font-semibold text-gray-900"
> >
<a href="#" className="group inline-flex"> <a href="#" className="group inline-flex">
{props.newColumnHeadings.getHeading()} {props.newColumnHeadings.getHeading()}
<span className="invisible ml-2 flex-none rounded text-gray-400 group-hover:visible group-focus:visible"> <span className="invisible ml-2 flex-none rounded text-gray-400 group-hover:visible group-focus:visible">
<ChevronDownIcon <ChevronDownIcon
aria-hidden="true" aria-hidden="true"
@@ -76,7 +81,7 @@ export default async function UsersTable(props: any) {
className="px-3 py-3.5 text-left text-xs font-semibold text-gray-900" className="px-3 py-3.5 text-left text-xs font-semibold text-gray-900"
> >
<a href="#" className="group inline-flex"> <a href="#" className="group inline-flex">
{props.newColumnHeadings.getHeading()} {props.newColumnHeadings.getHeading()}
<span className="invisible ml-2 flex-none rounded text-gray-400 group-hover:visible group-focus:visible"> <span className="invisible ml-2 flex-none rounded text-gray-400 group-hover:visible group-focus:visible">
<ChevronDownIcon <ChevronDownIcon
aria-hidden="true" aria-hidden="true"
@@ -91,7 +96,7 @@ export default async function UsersTable(props: any) {
{props.data.map((item: any) => ( {props.data.map((item: any) => (
<tr key={item.uuid}> <tr key={item.uuid}>
<td className="whitespace-wrap flex items-center py-4 pl-4 pr-3 text-xs font-medium text-gray-900 "> <td className="whitespace-wrap flex items-center py-4 pl-4 pr-3 text-xs font-medium text-gray-900 ">
<Link href={`/UserProfile/${item.uuid}`}><span className="pl-2"> {item.email}</span></Link> <Link href={`/UserProfile/${item.uuid}`}><span className="pl-2"> {item.email}</span></Link>
</td> </td>
<td className="whitespace-nowrap px-3 py-4 text-xs text-gray-900"> <td className="whitespace-nowrap px-3 py-4 text-xs text-gray-900">
@@ -104,25 +109,15 @@ export default async function UsersTable(props: any) {
{item.username} {item.username}
</td> </td>
<td className="whitespace-nowrap px-3 py-4 text-xs text-gray-900"> <td className="whitespace-nowrap px-3 py-4 text-xs text-gray-900">
<button <ButtonOnClick />
type="button"
className="inline-flex items-center gap-x-1.5 rounded-xl bg-lime-800 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-lime-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lime-900"
>
Edit
<PlusCircleIcon
aria-hidden="true"
className="-mr-0.5 size-5"
/>
</button>
</td> </td>
<td style={{display:'none'}}> </tr>
</td>
</tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import Link from "next/link" import Link from "next/link";
import constants from "@/lib/constants";
const navigation = { const navigation = {
armory: [ armory: [
@@ -6,7 +7,7 @@ const navigation = {
{ name: 'Lowers', href: '/Products/lowers' }, { name: 'Lowers', href: '/Products/lowers' },
{ name: 'Uppers', href: '/Products/uppers' }, { name: 'Uppers', href: '/Products/uppers' },
{ name: 'Optics', href: '/Products/optics' }, { name: 'Optics', href: '/Products/optics' },
{ name: 'Accessories', href: '/Products/accessories#' }, { name: 'Accessories', href: '/Products/accessories' },
], ],
admin: [ admin: [
{ name: 'Users', href: '/Admin/Users' }, { name: 'Users', href: '/Admin/Users' },
@@ -82,6 +83,8 @@ const navigation = {
} }
export default function Footer() { export default function Footer() {
let newDate = new Date();
let year = newDate.getFullYear();
return ( return (
<footer className="bg-zinc-900"> <footer className="bg-zinc-900">
<div className="mx-auto max-w-7xl px-6 pb-8 pt-20 sm:pt-24 lg:px-8 lg:pt-32"> <div className="mx-auto max-w-7xl px-6 pb-8 pt-20 sm:pt-24 lg:px-8 lg:pt-32">
@@ -193,7 +196,7 @@ export default function Footer() {
))} ))}
</div> </div>
<p className="mt-8 text-sm/6 text-gray-400 md:order-1 md:mt-0"> <p className="mt-8 text-sm/6 text-gray-400 md:order-1 md:mt-0">
&copy; 2024 Your Company, Inc. All rights reserved. &copy; `{year} {constants.COMPANY_NAME}` All rights reserved.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,20 @@
import { env } from "@/env";
import * as schema from "@schemas/schema";
import { PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
let database: PostgresJsDatabase<typeof schema>;
let pg: ReturnType<typeof postgres>;
if (env.NODE_ENV === "production") {
pg = postgres(env.DATABASE_URL);
database = drizzle(pg, { schema });
} else {
if (!(global as any).database!) {
pg = postgres(env.DATABASE_URL);
(global as any).database = drizzle(pg, { schema });
}
database = (global as any).database;
}
export { database, pg };

354
src/db/wdcStarter/schema.ts Normal file
View File

@@ -0,0 +1,354 @@
import { relations, sql } from "drizzle-orm";
import {
boolean,
index,
integer,
pgEnum,
pgTable,
serial,
text,
timestamp,
} from "drizzle-orm/pg-core";
export const roleEnum = pgEnum("role", ["member", "admin"]);
export const accountTypeEnum = pgEnum("type", ["email", "google", "github"]);
export const users = pgTable("gf_user", {
id: serial("id").primaryKey(),
email: text("email").unique(),
emailVerified: timestamp("emailVerified", { mode: "date" }),
});
export const accounts = pgTable(
"gf_accounts",
{
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
accountType: accountTypeEnum("accountType").notNull(),
githubId: text("githubId").unique(),
googleId: text("googleId").unique(),
password: text("password"),
salt: text("salt"),
},
(table) => ({
userIdAccountTypeIdx: index("user_id_account_type_idx").on(
table.userId,
table.accountType
),
})
);
export const magicLinks = pgTable(
"gf_magic_links",
{
id: serial("id").primaryKey(),
email: text("email").notNull().unique(),
token: text("token"),
tokenExpiresAt: timestamp("tokenExpiresAt", { mode: "date" }),
},
(table) => ({
tokenIdx: index("magic_links_token_idx").on(table.token),
})
);
export const resetTokens = pgTable(
"gf_reset_tokens",
{
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" })
.unique(),
token: text("token"),
tokenExpiresAt: timestamp("tokenExpiresAt", { mode: "date" }),
},
(table) => ({
tokenIdx: index("reset_tokens_token_idx").on(table.token),
})
);
export const verifyEmailTokens = pgTable(
"gf_verify_email_tokens",
{
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" })
.unique(),
token: text("token"),
tokenExpiresAt: timestamp("tokenExpiresAt", { mode: "date" }),
},
(table) => ({
tokenIdx: index("verify_email_tokens_token_idx").on(table.token),
})
);
export const profiles = pgTable("gf_profile", {
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" })
.unique(),
displayName: text("displayName"),
imageId: text("imageId"),
image: text("image"),
bio: text("bio").notNull().default(""),
});
export const sessions = pgTable(
"gf_session",
{
id: text("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expiresAt: timestamp("expires_at", {
withTimezone: true,
mode: "date",
}).notNull(),
},
(table) => ({
userIdIdx: index("sessions_user_id_idx").on(table.userId),
})
);
export const subscriptions = pgTable(
"gf_subscriptions",
{
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" })
.unique(),
stripeSubscriptionId: text("stripeSubscriptionId").notNull(),
stripeCustomerId: text("stripeCustomerId").notNull(),
stripePriceId: text("stripePriceId").notNull(),
stripeCurrentPeriodEnd: timestamp("expires", { mode: "date" }).notNull(),
},
(table) => ({
stripeSubscriptionIdIdx: index(
"subscriptions_stripe_subscription_id_idx"
).on(table.stripeSubscriptionId),
})
);
export const following = pgTable(
"gf_following",
{
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
foreignUserId: serial("foreignUserId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
},
(table) => ({
userIdForeignUserIdIdx: index("following_user_id_foreign_user_id_idx").on(
table.userId,
table.foreignUserId
),
})
);
/**
* newsletters - although the emails for the newsletter are tracked in Resend, it's beneficial to also track
* sign ups in your own database in case you decide to move to another email provider.
* The last thing you'd want is for your email list to get lost due to a
* third party provider shutting down or dropping your data.
*/
export const newsletters = pgTable("gf_newsletter", {
id: serial("id").primaryKey(),
email: text("email").notNull().unique(),
});
export const groups = pgTable(
"gf_group",
{
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
name: text("name").notNull(),
description: text("description").notNull(),
isPublic: boolean("isPublic").notNull().default(false),
bannerId: text("bannerId"),
info: text("info").default(""),
youtubeLink: text("youtubeLink").default(""),
discordLink: text("discordLink").default(""),
githubLink: text("githubLink").default(""),
xLink: text("xLink").default(""),
},
(table) => ({
userIdIsPublicIdx: index("groups_user_id_is_public_idx").on(
table.userId,
table.isPublic
),
})
);
export const memberships = pgTable(
"gf_membership",
{
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
groupId: serial("groupId")
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
role: roleEnum("role").default("member"),
},
(table) => ({
userIdGroupIdIdx: index("memberships_user_id_group_id_idx").on(
table.userId,
table.groupId
),
})
);
export const invites = pgTable("gf_invites", {
id: serial("id").primaryKey(),
token: text("token")
.notNull()
.default(sql`gen_random_uuid()`)
.unique(),
tokenExpiresAt: timestamp("tokenExpiresAt", { mode: "date" }),
groupId: serial("groupId")
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
tokenExpiresAt: timestamp("tokenExpiresAt", { mode: "date" }).notNull(),
});
export const events = pgTable("gf_events", {
id: serial("id").primaryKey(),
groupId: serial("groupId")
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
name: text("name").notNull(),
description: text("description").notNull(),
imageId: text("imageId"),
startsOn: timestamp("startsOn", { mode: "date" }).notNull(),
});
export const notifications = pgTable("gf_notifications", {
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
groupId: serial("groupId")
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
postId: integer("postId"),
isRead: boolean("isRead").notNull().default(false),
type: text("type").notNull(),
message: text("message").notNull(),
createdOn: timestamp("createdOn", { mode: "date" }).notNull(),
});
export const posts = pgTable("gf_posts", {
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
groupId: serial("groupId")
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
title: text("title").notNull(),
message: text("message").notNull(),
createdOn: timestamp("createdOn", { mode: "date" }).notNull(),
});
export const reply = pgTable(
"gf_replies",
{
id: serial("id").primaryKey(),
userId: serial("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
postId: serial("postId")
.notNull()
.references(() => posts.id, { onDelete: "cascade" }),
groupId: serial("groupId")
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
message: text("message").notNull(),
createdOn: timestamp("createdOn", { mode: "date" }).notNull(),
},
(table) => ({
postIdIdx: index("replies_post_id_idx").on(table.postId),
})
);
/**
* RELATIONSHIPS
*
* Here you can define drizzle relationships between table which helps improve the type safety
* in your code.
*/
export const groupRelations = relations(groups, ({ many }) => ({
memberships: many(memberships),
}));
export const membershipRelations = relations(memberships, ({ one }) => ({
user: one(users, { fields: [memberships.userId], references: [users.id] }),
profile: one(profiles, {
fields: [memberships.userId],
references: [profiles.userId],
}),
group: one(groups, {
fields: [memberships.groupId],
references: [groups.id],
}),
}));
export const postsRelationships = relations(posts, ({ one }) => ({
user: one(users, { fields: [posts.userId], references: [users.id] }),
group: one(groups, { fields: [posts.groupId], references: [groups.id] }),
}));
export const followingRelationship = relations(following, ({ one }) => ({
foreignProfile: one(profiles, {
fields: [following.foreignUserId],
references: [profiles.userId],
}),
userProfile: one(profiles, {
fields: [following.userId],
references: [profiles.userId],
}),
}));
/**
* TYPES
*
* You can create and export types from your schema to use in your application.
* This is useful when you need to know the shape of the data you are working with
* in a component or function.
*/
export type Subscription = typeof subscriptions.$inferSelect;
export type Group = typeof groups.$inferSelect;
export type NewGroup = typeof groups.$inferInsert;
export type Membership = typeof memberships.$inferSelect;
export type Event = typeof events.$inferSelect;
export type NewEvent = typeof events.$inferInsert;
export type User = typeof users.$inferSelect;
export type Profile = typeof profiles.$inferSelect;
export type Notification = typeof notifications.$inferSelect;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
export type Reply = typeof reply.$inferSelect;
export type NewReply = typeof reply.$inferInsert;
export type Following = typeof following.$inferSelect;
export type GroupId = Group["id"];
export type Session = typeof sessions.$inferSelect;

View File

@@ -532,4 +532,14 @@ export const sessions = pgTable(
})); }));
export type Post = typeof posts.$inferSelect; export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert; export type NewPost = typeof posts.$inferInsert;
export const vwUserSessions = pgView("vw_user_sessions", { id: varchar({ length: 255 }),
userId: varchar("user_id", { length: 21 }),
uId: varchar("u_id", { length: 21 }),
uEmail: varchar("u_email", { length: 255 }),
expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }),
createdAt: timestamp("created_at", { mode: 'string' }),
updatedAt: timestamp("updated_at", { mode: 'string' }),
}).existing();
//as(sql`SELECT s.id, s.user_id, u.id AS u_id, u.email AS u_email, s.expires_at, s.created_at, s.updated_at FROM sessions s, users u WHERE s.user_id::text = u.id::text`);

106
src/lib/wdcStarter/auth.ts Normal file
View File

@@ -0,0 +1,106 @@
import { GitHub, Google } from "arctic";
import { database } from "@/db/wdcStarter";
import {
encodeBase32LowerCaseNoPadding,
encodeHexLowerCase,
} from "@oslojs/encoding";
import { Session, sessions, User, users } from "@/db/wdcStarter/schema";
import { env } from "@/env";
import { eq } from "drizzle-orm/expressions";
import { sha256 } from "@oslojs/crypto/sha2";
import { UserId } from "@src/use-cases/types";
import { getSessionToken } from "@lib/wdcStarter/session";
const SESSION_REFRESH_INTERVAL_MS = 1000 * 60 * 60 * 24 * 15;
const SESSION_MAX_DURATION_MS = SESSION_REFRESH_INTERVAL_MS * 2;
export const github = new GitHub(
env.GITHUB_CLIENT_ID,
env.GITHUB_CLIENT_SECRET,
`${env.HOST_NAME}/api/login/github/callback`
);
export const googleAuth = new Google(
env.GOOGLE_CLIENT_ID,
env.GOOGLE_CLIENT_SECRET,
`${env.HOST_NAME}/api/login/google/callback`
);
export function generateSessionToken(): string {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
const token = encodeBase32LowerCaseNoPadding(bytes);
return token;
}
export async function createSession(
token: string,
userId: UserId
): Promise<Session> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: Session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + SESSION_MAX_DURATION_MS),
};
await database.insert(sessions).values(session);
return session;
}
export async function validateRequest(): Promise<SessionValidationResult> {
const sessionToken = await getSessionToken();
if (!sessionToken) {
return { session: null, user: null };
}
return validateSessionToken(sessionToken);
}
export async function validateSessionToken(
token: string
): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const sessionInDb = await database.query.sessions.findFirst({
where: eq(sessions.id, sessionId),
});
if (!sessionInDb) {
return { session: null, user: null };
}
if (Date.now() >= sessionInDb.expiresAt.getTime()) {
await database.delete(sessions).where(eq(sessions.id, sessionInDb.id));
return { session: null, user: null };
}
const user = await database.query.users.findFirst({
where: eq(users.id, sessionInDb.userId),
});
if (!user) {
await database.delete(sessions).where(eq(sessions.id, sessionInDb.id));
return { session: null, user: null };
}
if (
Date.now() >=
sessionInDb.expiresAt.getTime() - SESSION_REFRESH_INTERVAL_MS
) {
sessionInDb.expiresAt = new Date(Date.now() + SESSION_MAX_DURATION_MS);
await database
.update(sessions)
.set({
expiresAt: sessionInDb.expiresAt,
})
.where(eq(sessions.id, sessionInDb.id));
}
return { session: sessionInDb, user };
}
export async function invalidateSession(sessionId: string): Promise<void> {
await database.delete(sessions).where(eq(sessions.id, sessionId));
}
export async function invalidateUserSessions(userId: UserId): Promise<void> {
await database.delete(sessions).where(eq(users.id, userId));
}
export type SessionValidationResult =
| { session: Session; user: User }
| { session: null; user: null };

View File

@@ -0,0 +1,58 @@
import "server-only";
import { AuthenticationError } from "@/app/(main)/util";
import { createSession, generateSessionToken, validateRequest } from "@lib/wdcStarter/auth";
import { cache } from "react";
import { cookies } from "next/headers";
import { UserId } from "@/use-cases/types";
const SESSION_COOKIE_NAME = "session";
export async function setSessionTokenCookie(
token: string,
expiresAt: Date
): Promise<void> {
const allCookies = await cookies();
allCookies.set(SESSION_COOKIE_NAME, token, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
expires: expiresAt,
path: "/",
});
}
export async function deleteSessionTokenCookie(): Promise<void> {
const allCookies = await cookies();
allCookies.set(SESSION_COOKIE_NAME, "", {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
maxAge: 0,
path: "/",
});
}
export async function getSessionToken(): Promise<string | undefined> {
const allCookies = await cookies();
const sessionCookie = allCookies.get(SESSION_COOKIE_NAME)?.value;
return sessionCookie;
}
export const getCurrentUser = cache(async () => {
const { user } = await validateRequest();
return user ?? undefined;
});
export const assertAuthenticated = async () => {
const user = await getCurrentUser();
if (!user) {
throw new AuthenticationError();
}
return user;
};
export async function setSession(userId: UserId) {
const token = generateSessionToken();
const session = await createSession(token, userId);
await setSessionTokenCookie(token, session.expiresAt);
}

21
src/use-cases/types.ts Normal file
View File

@@ -0,0 +1,21 @@
export type Plan = "free" | "basic" | "premium";
export type Role = "owner" | "admin" | "member";
export type UserId = number;
export type UserProfile = {
id: UserId;
name: string | null;
image: string | null;
};
export type UserSession = {
id: UserId;
};
export type MemberInfo = {
name: string | null;
userId: UserId;
image: string | null;
role: Role;
};