mirror of
https://gitea.gofwd.group/dstrawsb/ballistic-builder.git
synced 2025-12-06 02:36:44 -05:00
more stuff
This commit is contained in:
@@ -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
25
pnpm-lock.yaml
generated
@@ -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
26
src/app/(main)/util.ts
Normal 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";
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
23
src/app/Products/accessories/page.tsx
Normal file
23
src/app/Products/accessories/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/components/admin/UsersTable/ButtonOnClick.tsx
Normal file
23
src/components/admin/UsersTable/ButtonOnClick.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
© 2024 Your Company, Inc. All rights reserved.
|
© `{year} {constants.COMPANY_NAME}` All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
20
src/db/wdcStarter/index.ts
Normal file
20
src/db/wdcStarter/index.ts
Normal 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
354
src/db/wdcStarter/schema.ts
Normal 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;
|
||||||
@@ -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
106
src/lib/wdcStarter/auth.ts
Normal 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 };
|
||||||
58
src/lib/wdcStarter/session.ts
Normal file
58
src/lib/wdcStarter/session.ts
Normal 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
21
src/use-cases/types.ts
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user