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/system": "^6.1.7",
|
||||
"@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-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
@@ -59,6 +61,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "^5.3.0",
|
||||
"sha2": "link:@oslojs/crypto/sha2",
|
||||
"sonner": "^1.7.2",
|
||||
"stripe": "^17.6.0",
|
||||
"superjson": "^2.2.2",
|
||||
|
||||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@@ -44,6 +44,12 @@ importers:
|
||||
'@mui/x-data-grid':
|
||||
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)
|
||||
'@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':
|
||||
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)
|
||||
@@ -158,6 +164,9 @@ importers:
|
||||
react-icons:
|
||||
specifier: ^5.3.0
|
||||
version: 5.4.0(react@18.2.0)
|
||||
sha2:
|
||||
specifier: link:@oslojs/crypto/sha2
|
||||
version: link:@oslojs/crypto/sha2
|
||||
sonner:
|
||||
specifier: ^1.7.2
|
||||
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)
|
||||
eslint: 8.57.1
|
||||
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-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-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@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-hooks: 5.1.0(eslint@8.57.1)
|
||||
@@ -6741,7 +6750,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.0
|
||||
@@ -6753,22 +6762,22 @@ snapshots:
|
||||
is-glob: 4.0.3
|
||||
stable-hash: 0.0.4
|
||||
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:
|
||||
- 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:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.21.0(eslint@8.57.1)(typescript@5.7.3)
|
||||
eslint: 8.57.1
|
||||
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:
|
||||
- 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:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.8
|
||||
@@ -6779,7 +6788,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
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
|
||||
is-core-module: 2.16.1
|
||||
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",
|
||||
link: "/lowers",
|
||||
source: "-",
|
||||
price: "-",
|
||||
ship_price: "-",
|
||||
},
|
||||
{
|
||||
name: "Lower Parts Kit",
|
||||
link: "/lowers",
|
||||
source: "-",
|
||||
price: "-",
|
||||
ship_price: "-",
|
||||
},
|
||||
{
|
||||
name: "Lower Parts Kit",
|
||||
link: "/lowers",
|
||||
source: "-",
|
||||
price: "-",
|
||||
ship_price: "-",
|
||||
},
|
||||
{
|
||||
name: "Lower Parts Kit",
|
||||
link: "/lowers",
|
||||
source: "-",
|
||||
price: "-",
|
||||
ship_price: "-",
|
||||
@@ -43,15 +47,11 @@ const partsData = [
|
||||
{
|
||||
group: "Upper Parts",
|
||||
parts: [
|
||||
{ name: "Upper Reciever", source: "-", price: "-", ship_price: "-" },
|
||||
{ name: "Barrel", source: "-", price: "-", ship_price: "-" },
|
||||
{ name: "BCG", source: "-", price: "-", ship_price: "-" },
|
||||
{ name: "Muzzle Device", source: "-", price: "-", ship_price: "-" },
|
||||
{
|
||||
name: "Charging Handle",
|
||||
source: "-",
|
||||
price: "-",
|
||||
ship_price: "-",
|
||||
{ name: "Upper Reciever", link: "/lowers", source: "-", price: "-", ship_price: "-" },
|
||||
{ name: "Barrel", link: "/lowers", source: "-", price: "-", ship_price: "-" },
|
||||
{ name: "BCG", link: "/lowers", source: "-", price: "-", ship_price: "-" },
|
||||
{ name: "Muzzle Device", link: "/lowers", source: "-", price: "-", ship_price: "-" },
|
||||
{ name: "Charging Handle", link: "/lowers", source: "-", price: "-", ship_price: "-",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -142,7 +142,7 @@ export default function BuilderPage() {
|
||||
aria-hidden="true"
|
||||
className="-ml-0.5 size-5"
|
||||
/>
|
||||
Purchase123
|
||||
Purchase
|
||||
</button>
|
||||
</td>
|
||||
</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 Link from "next/link";
|
||||
import ButtonOnClick from "./ButtonOnClick";
|
||||
|
||||
|
||||
export default async function UsersTable(props: any) {
|
||||
|
||||
const onClick = () => {
|
||||
alert("This feature is coming soon");
|
||||
}
|
||||
return (
|
||||
<div className="pb-12">
|
||||
|
||||
|
||||
<div className="mt-8 flow-root">
|
||||
<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">
|
||||
@@ -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 "
|
||||
>
|
||||
<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">
|
||||
<ChevronDownIcon
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<ChevronDownIcon
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<ChevronDownIcon
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<ChevronDownIcon
|
||||
aria-hidden="true"
|
||||
@@ -91,7 +96,7 @@ export default async function UsersTable(props: any) {
|
||||
{props.data.map((item: any) => (
|
||||
<tr key={item.uuid}>
|
||||
<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>
|
||||
</td>
|
||||
<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}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-xs text-gray-900">
|
||||
<button
|
||||
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>
|
||||
<ButtonOnClick />
|
||||
</td>
|
||||
<td style={{display:'none'}}>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link"
|
||||
import Link from "next/link";
|
||||
import constants from "@/lib/constants";
|
||||
|
||||
const navigation = {
|
||||
armory: [
|
||||
@@ -6,7 +7,7 @@ const navigation = {
|
||||
{ name: 'Lowers', href: '/Products/lowers' },
|
||||
{ name: 'Uppers', href: '/Products/uppers' },
|
||||
{ name: 'Optics', href: '/Products/optics' },
|
||||
{ name: 'Accessories', href: '/Products/accessories#' },
|
||||
{ name: 'Accessories', href: '/Products/accessories' },
|
||||
],
|
||||
admin: [
|
||||
{ name: 'Users', href: '/Admin/Users' },
|
||||
@@ -82,6 +83,8 @@ const navigation = {
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
let newDate = new Date();
|
||||
let year = newDate.getFullYear();
|
||||
return (
|
||||
<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">
|
||||
@@ -193,7 +196,7 @@ export default function Footer() {
|
||||
))}
|
||||
</div>
|
||||
<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>
|
||||
</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 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