diff --git a/package.json b/package.json
index ac4a4d8..f833667 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2c78f64..d57328f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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
diff --git a/src/app/(main)/util.ts b/src/app/(main)/util.ts
new file mode 100644
index 0000000..e9ba96c
--- /dev/null
+++ b/src/app/(main)/util.ts
@@ -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";
+ }
+};
\ No newline at end of file
diff --git a/src/app/Builder/page.tsx b/src/app/Builder/page.tsx
index 827d13b..085146b 100644
--- a/src/app/Builder/page.tsx
+++ b/src/app/Builder/page.tsx
@@ -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
diff --git a/src/app/Products/accessories/page.tsx b/src/app/Products/accessories/page.tsx
new file mode 100644
index 0000000..6e7ca9a
--- /dev/null
+++ b/src/app/Products/accessories/page.tsx
@@ -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 (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/admin/UsersTable/ButtonOnClick.tsx b/src/components/admin/UsersTable/ButtonOnClick.tsx
new file mode 100644
index 0000000..f3f7787
--- /dev/null
+++ b/src/components/admin/UsersTable/ButtonOnClick.tsx
@@ -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 (
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/UsersTable/index.tsx b/src/components/admin/UsersTable/index.tsx
index de7b977..04e5911 100644
--- a/src/components/admin/UsersTable/index.tsx
+++ b/src/components/admin/UsersTable/index.tsx
@@ -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 (
-
+
diff --git a/src/components/footer/index.tsx b/src/components/footer/index.tsx
index 996785b..cd95812 100644
--- a/src/components/footer/index.tsx
+++ b/src/components/footer/index.tsx
@@ -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 (
diff --git a/src/db/wdcStarter/index.ts b/src/db/wdcStarter/index.ts
new file mode 100644
index 0000000..2eed658
--- /dev/null
+++ b/src/db/wdcStarter/index.ts
@@ -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;
+let pg: ReturnType;
+
+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 };
\ No newline at end of file
diff --git a/src/db/wdcStarter/schema.ts b/src/db/wdcStarter/schema.ts
new file mode 100644
index 0000000..d7b1414
--- /dev/null
+++ b/src/db/wdcStarter/schema.ts
@@ -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;
\ No newline at end of file
diff --git a/src/drizzle/schema/schema.ts b/src/drizzle/schema/schema.ts
index 8f49e5b..432c5d1 100644
--- a/src/drizzle/schema/schema.ts
+++ b/src/drizzle/schema/schema.ts
@@ -532,4 +532,14 @@ export const sessions = pgTable(
}));
export type Post = typeof posts.$inferSelect;
- export type NewPost = typeof posts.$inferInsert;
\ No newline at end of file
+ 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`);
\ No newline at end of file
diff --git a/src/lib/wdcStarter/auth.ts b/src/lib/wdcStarter/auth.ts
new file mode 100644
index 0000000..6742ed1
--- /dev/null
+++ b/src/lib/wdcStarter/auth.ts
@@ -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 {
+ 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 {
+ const sessionToken = await getSessionToken();
+ if (!sessionToken) {
+ return { session: null, user: null };
+ }
+ return validateSessionToken(sessionToken);
+}
+
+export async function validateSessionToken(
+ token: string
+): Promise {
+ 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 {
+ await database.delete(sessions).where(eq(sessions.id, sessionId));
+}
+
+export async function invalidateUserSessions(userId: UserId): Promise {
+ await database.delete(sessions).where(eq(users.id, userId));
+}
+
+export type SessionValidationResult =
+ | { session: Session; user: User }
+ | { session: null; user: null };
\ No newline at end of file
diff --git a/src/lib/wdcStarter/session.ts b/src/lib/wdcStarter/session.ts
new file mode 100644
index 0000000..9c2805a
--- /dev/null
+++ b/src/lib/wdcStarter/session.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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);
+}
\ No newline at end of file
diff --git a/src/use-cases/types.ts b/src/use-cases/types.ts
new file mode 100644
index 0000000..5a86833
--- /dev/null
+++ b/src/use-cases/types.ts
@@ -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;
+};
\ No newline at end of file