Added some migrations and lots of good stuff

This commit is contained in:
Fergal Moran
2024-09-13 16:59:17 +01:00
parent f9e7f3e70d
commit 258fb1412c
23 changed files with 751 additions and 133 deletions

View File

@@ -1,8 +1,11 @@
DATABASE_URL="postgresql://postgres:hackme@localhost:5432/opengifame"
UPLOAD_PATH=/srv/dev/opengifame/working/uploads
NEXTAUTH_SECRET="tAOVgxpY1U0BsnPCr6Gf8WVkmRMkp06ztUfwMhBKMQ4="
NEXTAUTH_URL="https://opengifame.dev.fergl.ie:3000"
NEXT_PUBLIC_DEBUG_MODE=1
NEXT_PUBLIC_SITE_NAME=Open Gifame
NEXT_PUBLIC_SITE_DESCRIPTION=Robot powered giffage
NEXT_PUBLIC_SITE_URL=https://opengifame.dev.fergl.ie:3000

View File

@@ -1,20 +0,0 @@
# Since the ".env" file is gitignored, you can use the ".env.example" file to
# build a new ".env" file when you clone the repo. Keep this file up-to-date
# when you add new variables to `.env`.
# This file will be committed to version control, so make sure not to have any
# secrets in it. If you are cloning this repo, create a copy of this file named
# ".env" and populate it with your secrets.
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
# Drizzle
DATABASE_URL="postgresql://postgres:password@localhost:5432/opengifame"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"

View File

@@ -1,4 +1,4 @@
{
// "workbench.colorTheme": "Tinacious Design (High Contrast)"
"workbench.colorTheme": "Moonlight",
"workbench.colorTheme": "Tinacious Design (High Contrast)",
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS "image" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar,
"tags" text[],
"created_by" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updated_at" timestamp with time zone
);
--> statement-breakpoint
ALTER TABLE "post" ALTER COLUMN "id" SET DATA TYPE varchar(255);--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "image" ADD CONSTRAINT "image_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -0,0 +1,418 @@
{
"id": "01ae1334-57ff-4e93-8599-bdc2689aea71",
"prevId": "3cde7a7b-e714-4d18-a93a-9e07c9e48f5a",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"provider": {
"name": "provider",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"provider_account_id": {
"name": "provider_account_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"token_type": {
"name": "token_type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"session_state": {
"name": "session_state",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"account_user_id_idx": {
"name": "account_user_id_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"account_provider_provider_account_id_pk": {
"name": "account_provider_provider_account_id_pk",
"columns": [
"provider",
"provider_account_id"
]
}
},
"uniqueConstraints": {}
},
"public.image": {
"name": "image",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar",
"primaryKey": false,
"notNull": false
},
"tags": {
"name": "tags",
"type": "text[]",
"primaryKey": false,
"notNull": false
},
"created_by": {
"name": "created_by",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "CURRENT_TIMESTAMP"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"image_created_by_user_id_fk": {
"name": "image_created_by_user_id_fk",
"tableFrom": "image",
"tableTo": "user",
"columnsFrom": [
"created_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.post": {
"name": "post",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false
},
"created_by": {
"name": "created_by",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "CURRENT_TIMESTAMP"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"created_by_idx": {
"name": "created_by_idx",
"columns": [
{
"expression": "created_by",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"name_idx": {
"name": "name_idx",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"post_created_by_user_id_fk": {
"name": "post_created_by_user_id_fk",
"tableFrom": "post",
"tableTo": "user",
"columnsFrom": [
"created_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"session_token": {
"name": "session_token",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"session_user_id_idx": {
"name": "session_user_id_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false,
"default": "CURRENT_TIMESTAMP"
},
"image": {
"name": "image",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.verification_token": {
"name": "verification_token",
"schema": "",
"columns": {
"identifier": {
"name": "identifier",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"verification_token_identifier_token_pk": {
"name": "verification_token_identifier_token_pk",
"columns": [
"identifier",
"token"
]
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1725454391060,
"tag": "0000_dazzling_cerebro",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1726068700784,
"tag": "0001_wooden_ink",
"breakpoints": true
}
]
}

View File

@@ -16,7 +16,7 @@
},
"dependencies": {
"@auth/drizzle-adapter": "^1.4.2",
"@headlessui/react": "^2.1.6",
"@headlessui/react": "^2.1.8",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -47,7 +47,7 @@
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.55.4",
"@tanstack/react-query": "^5.56.2",
"@trpc/client": "^11.0.0-rc.446",
"@trpc/react-query": "^11.0.0-rc.446",
"@trpc/server": "^11.0.0-rc.446",
@@ -62,14 +62,14 @@
"cmdk": "1.0.0",
"date-fns": "^3.6.0",
"drizzle-orm": "^0.33.0",
"embla-carousel-react": "^8.2.1",
"embla-carousel-react": "^8.3.0",
"geist": "^1.3.1",
"input-otp": "^1.2.4",
"lodash": "^4.17.21",
"loglevel": "^1.9.2",
"loglevel-plugin-prefix": "^0.8.4",
"lucide-react": "^0.439.0",
"next": "^14.2.9",
"lucide-react": "^0.441.0",
"next": "^14.2.11",
"next-auth": "^4.24.7",
"next-themes": "^0.3.0",
"postgres": "^3.4.4",
@@ -99,12 +99,12 @@
"@typescript-eslint/parser": "^8.5.0",
"drizzle-kit": "^0.24.2",
"eslint": "^9.10.0",
"eslint-config-next": "^14.2.9",
"eslint-config-next": "^14.2.11",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.4.45",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.10",
"tailwindcss": "^3.4.11",
"typescript": "^5.6.2"
},
"ct3aMetadata": {

View File

@@ -1,5 +1,9 @@
import UploadPage from "@/components/pages/upload-page";
const Upload = () => <UploadPage />;
const Upload = () => (
<div className="mt-4">
<UploadPage />
</div>
);
export default Upload;

View File

@@ -0,0 +1,6 @@
import { getServerSession } from "next-auth";
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const session = await getServerSession();
}

View File

@@ -14,6 +14,7 @@ import { dashboardConfig } from "@/config/top-nav.config";
import { siteConfig } from "@/config/site.config";
import { getServerSession } from "next-auth";
import { SessionProvider } from "next-auth/react";
import { GlobalPasteListener } from "@/components/global-paste-listener";
export const viewport: Viewport = {
themeColor: [
@@ -36,6 +37,7 @@ export default async function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
const session = await getServerSession();
return (
<html lang="en" suppressHydrationWarning>
<head>
@@ -53,8 +55,10 @@ export default async function RootLayout({
>
<TRPCReactProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<GlobalPasteListener />
<Toaster />
<TailwindIndicator />
<TopNavbar items={dashboardConfig.mainNav} session={session} />
<main className="m-4">{children}</main>
</ThemeProvider>

View File

@@ -0,0 +1,3 @@
export function GlobalPasteListener() {
return <></>;
}

View File

@@ -27,6 +27,7 @@ import {
Terminal,
LogIn,
Upload,
Menu,
} from "lucide-react";
export type Icon = typeof LucideIcon;
@@ -111,6 +112,7 @@ export const Icons = {
></path>
</svg>
),
hamburger: Menu,
twitter: Twitter,
check: Check,
upload: Upload,

View File

@@ -0,0 +1,44 @@
import * as React from "react";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { Icons } from "@/components/icons";
import { NavItem } from "@/types";
import { siteConfig } from "@/config/site.config";
interface MobileNavProps {
items: NavItem[];
children?: React.ReactNode;
}
export function MobileNav({ items, children }: MobileNavProps) {
return (
<div
className={cn(
"fixed inset-0 top-16 z-50 grid h-[calc(100vh-4rem)] grid-flow-row auto-rows-max overflow-auto p-6 pb-32 shadow-md animate-in slide-in-from-bottom-80 md:hidden",
)}
>
<div className="relative z-20 grid gap-6 rounded-md bg-popover p-4 text-popover-foreground shadow-md">
<Link href="/" className="flex items-center space-x-2">
<Icons.logo />
<span className="font-bold">{siteConfig.name}</span>
</Link>
<nav className="grid grid-flow-row auto-rows-max text-sm">
{items.map((item, index) => (
<Link
key={index}
href={item.disabled ? "#" : item.href}
className={cn(
"flex w-full items-center rounded-md p-2 text-sm font-medium hover:underline",
item.disabled && "cursor-not-allowed opacity-60",
)}
>
{item.title}
</Link>
))}
</nav>
{children}
</div>
</div>
);
}

View File

@@ -11,6 +11,8 @@ import { cn } from "@/lib/utils";
import { useSelectedLayoutSegment } from "next/navigation";
import LoginButton from "@/components/widgets/login/login-button";
import { MobileNav } from "./mobile-navbar";
import { buttonVariants } from "@/components/ui/button";
type TopNavbarProps = {
items: NavItem[];
@@ -19,6 +21,7 @@ type TopNavbarProps = {
const TopNavbar: React.FC<TopNavbarProps> = ({ items, session }) => {
const segment = useSelectedLayoutSegment();
const [showMobileMenu, setShowMobileMenu] = React.useState<boolean>(false);
return (
<div className="mx-auto px-2 sm:px-4 lg:divide-y lg:divide-gray-200 lg:px-8">
<div className="relative flex h-16 justify-between">
@@ -30,7 +33,13 @@ const TopNavbar: React.FC<TopNavbarProps> = ({ items, session }) => {
{siteConfig.name}
</span>
</Link>
<Link
className={buttonVariants({ variant: "secondary" })}
href={"/upload"}
>
<Icons.upload className="mr-2 h-4 w-4" />
Upload
</Link>
{items?.length ? (
<nav className="hidden gap-6 md:flex">
{items?.map((item, index) => (
@@ -69,10 +78,20 @@ const TopNavbar: React.FC<TopNavbarProps> = ({ items, session }) => {
</div>
</div>
</div>
<div className="relative z-10 flex items-center lg:hidden">
Mobile menu
<div className="relative z-10 flex items-center md:hidden">
<button
className="flex items-center space-x-2 md:hidden"
onClick={() => setShowMobileMenu(!showMobileMenu)}
>
{showMobileMenu ? (
<Icons.close className="w-8" />
) : (
<Icons.logo className="w-8" />
)}
</button>
{showMobileMenu && items && <MobileNav items={items} />}
</div>
<div className="hidden lg:relative lg:z-10 lg:ml-4 lg:flex lg:items-center">
<div className="hidden md:relative md:z-10 md:ml-4 md:flex md:items-center">
<LoginButton session={session} />
</div>
</div>

View File

@@ -14,6 +14,11 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { env } from "@/env";
import { logger } from "@/lib/logger";
import { api } from "@/trpc/react";
import { STATUS_CODES } from "http";
type FormValues = {
title: string;
@@ -23,31 +28,56 @@ type FormValues = {
};
const UploadPage: React.FC = () => {
const utils = api.useUtils();
const router = useRouter();
const form = useForm<FormValues>({
defaultValues: {
title: "",
description: "",
title: env.NEXT_PUBLIC_DEBUG_MODE ? "This is my title" : "",
description: env.NEXT_PUBLIC_DEBUG_MODE ? "This is my description" : "",
tags: [],
image: undefined,
},
});
const createImage = api.image.create.useMutation({
onSuccess: async (e) => {
console.log("upload-page", "onSuccess", e);
const file = form.getValues().image;
if (e.id && file) {
const body = new FormData();
body.set("image", file);
body.set("id", e.id);
const response = await fetch("/api/upload/profile-image", {
method: "POST",
body,
});
await utils.image.invalidate();
router.replace("/");
} else {
//TODO: Probably need to delete the image from the database
logger.error("upload-page", "onSuccess", "Error uploading image");
}
},
});
const _submit: SubmitHandler<FormValues> = async (data) => {
console.log(data);
if (data.image) {
const body = new FormData();
body.append("title", data.title);
body.append("description", data.description);
body.append("tags", data.tags.join("|"));
body.append("file", data.image);
const response = await fetch("api/upload", {
method: "POST",
body,
});
if (response.status === 201) {
await router.replace("/");
}
try {
await createImage.mutateAsync(data);
} catch (error) {
logger.error("UploadPage", "error", error);
}
// if (data.image) {
// const body = new FormData();
// body.append("file", data.image);
// const response = await fetch("api/upload", {
// method: "POST",
// body,
// });
// if (response.status === 201) {
// router.replace("/");
// }
// }
};
return (
<div className="md:grid md:grid-cols-3 md:gap-6">
@@ -62,70 +92,68 @@ const UploadPage: React.FC = () => {
<div className="md:col-span-2">
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(_submit)}>
<div className="shadow sm:overflow-hidden sm:rounded-md">
<div className="space-y-4 px-4">
<FormField
control={form.control}
name="title"
defaultValue={"fergal.moran+opengifame@gmail.com"}
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
{form.formState.errors.title && (
<FormMessage>
{form.formState.errors.title.message}
</FormMessage>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
defaultValue={"fergal.moran+opengifame@gmail.com"}
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} rows={3} />
</FormControl>
{form.formState.errors.description && (
<FormMessage>
{form.formState.errors.description.message}
</FormMessage>
)}
</FormItem>
)}
/>
<Controller
control={form.control}
name="image"
render={({ field: { value, onChange } }) => (
<ImageUpload value={value} onChange={onChange} />
)}
/>
<div className="divider pt-4">optional stuff</div>
<Controller
control={form.control}
name="tags"
render={({ field: { value, onChange } }) => (
<TaggedInput
label="Tags"
value={value}
onChange={onChange}
/>
)}
/>
</div>
<div className="w-full px-4 py-3 text-right">
<button type="submit" className="btn btn-primary w-full">
Upload Gif
</button>
</div>
<div className="space-y-4 px-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Let's give your post a title"
type="text"
{...field}
/>
</FormControl>
{form.formState.errors.title && (
<FormMessage>
{form.formState.errors.title.message}
</FormMessage>
)}
</FormItem>
)}
/>
<Controller
control={form.control}
name="image"
render={({ field: { value, onChange } }) => (
<ImageUpload value={value} onChange={onChange} />
)}
/>
<FormField
control={form.control}
name="description"
defaultValue={"fergal.moran+opengifame@gmail.com"}
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
placeholder="Add a description (if you want?)."
{...field}
rows={3}
/>
</FormControl>
{form.formState.errors.description && (
<FormMessage>
{form.formState.errors.description.message}
</FormMessage>
)}
</FormItem>
)}
/>
<div className="divider pt-4">optional stuff</div>
<Controller
control={form.control}
name="tags"
render={({ field: { value, onChange } }) => (
<TaggedInput label="Tags" value={value} onChange={onChange} />
)}
/>
</div>
<div className="w-full px-4 py-3 text-right">
<Button type="submit" className="btn btn-primary w-full">
Upload Gif
</Button>
</div>
</form>
</FormProvider>

View File

@@ -24,13 +24,6 @@ export function TrendingImages() {
) : (
<p>No images yet.</p>
)}
<ImageUpload
value="Farts"
onChange={(e) => {
console.log("trending-images", "uploading", e);
}}
/>
</div>
);
}

View File

@@ -2,9 +2,9 @@ import { DashboardConfig } from "@/types";
export const dashboardConfig: DashboardConfig = {
mainNav: [
{
title: "Upload image",
href: "/upload",
},
// {
// title: "Upload image",
// href: "/upload",
// },
],
};

View File

@@ -8,6 +8,7 @@ export const env = createEnv({
*/
server: {
DATABASE_URL: z.string().url(),
UPLOAD_PATH: z.string().default("uploads"),
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
@@ -31,6 +32,8 @@ export const env = createEnv({
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
NEXT_PUBLIC_DEBUG_MODE: z.boolean(),
NEXT_PUBLIC_SITE_NAME: z.string().default("My Site"),
NEXT_PUBLIC_SITE_DESCRIPTION: z.string().default("My site description"),
NEXT_PUBLIC_SITE_URL: z.string().default("https://opengifame.com"),
@@ -47,11 +50,14 @@ export const env = createEnv({
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
UPLOAD_PATH: process.env.UPLOAD_PATH,
NODE_ENV: process.env.NODE_ENV,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
NEXT_PUBLIC_SITE_NAME: process.env.NEXT_PUBLIC_SITE_NAME,
NEXT_PUBLIC_DEBUG_MODE:
process.env.NEXT_PUBLIC_DEBUG_MODE === "1" ? true : false,
NEXT_PUBLIC_SITE_DESCRIPTION: process.env.NEXT_PUBLIC_SITE_NAME,
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_NAME,
NEXT_PUBLIC_SITE_OG_IMAGE: process.env.NEXT_PUBLIC_SITE_NAME,

View File

@@ -1,6 +1,7 @@
import { postRouter } from "@/server/api/routers/post";
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
import { authRouter } from "./routers/auth";
import { postRouter } from "@/server/api/routers/post";
import { imageRouter } from "@/server/api/routers/image";
/**
* This is the primary router for your server.
@@ -10,6 +11,7 @@ import { authRouter } from "./routers/auth";
export const appRouter = createTRPCRouter({
post: postRouter,
auth: authRouter,
image: imageRouter,
});
// export type definition of API

View File

@@ -0,0 +1,31 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { z } from "zod";
import { images } from "@/server/db/schema";
const imageCreateType = {
title: z.string().min(5),
description: z.string(),
tags: z.array(z.string()),
};
export const imageRouter = createTRPCRouter({
create: protectedProcedure
.input(z.object(imageCreateType))
.mutation(async ({ ctx, input }) => {
const post = await ctx.db
.insert(images)
.values({
title: input.title,
description: input.description,
tags: input.tags,
createdById: ctx.session.user.id,
})
.returning();
if (!post[0]) {
throw new Error("Failed to create image");
}
return {
id: post[0].id,
};
}),
});

View File

@@ -25,17 +25,47 @@ declare module "next-auth" {
} & DefaultSession["user"];
}
}
declare module "next-auth/jwt" {
interface JWT {
id: string;
}
}
export const authOptions: NextAuthOptions = {
callbacks: {
session: ({ session, user }) => {
const s = {
...session,
user: {
...session.user,
},
session: ({ token, session }) => {
if (token) {
session.user.id = token.id;
session.user.name = token.name;
session.user.email = token.email;
session.user.image = token.picture;
}
return session;
},
async jwt({ token, user }) {
if (!user || !user.email) {
return token;
}
const u = await db
.select()
.from(users)
.where(and(eq(users.email, user.email)))
.limit(1);
if (!u || !u[0]) {
if (user) {
token.id = user?.id;
}
return token;
}
const session = u[0];
return {
id: session.id,
name: session.name,
email: session.email,
picture: session.image,
};
return s;
},
},
session: {
@@ -70,7 +100,8 @@ export const authOptions: NextAuthOptions = {
if (!(await bcrypt.compare(credentials.password, user[0]!.password!))) {
return null;
}
return { id: user[0]!.id, email: user[0]!.email };
const session = { id: user[0]!.id, email: user[0]!.email };
return session;
},
}),
],

View File

@@ -13,10 +13,32 @@ import { type AdapterAccount } from "next-auth/adapters";
export const createTable = pgTableCreator((name) => `${name}`);
export const images = createTable("image", {
id: varchar("id", { length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
title: varchar("title", { length: 256 }),
description: varchar("description"),
tags: text("tags").array(),
createdById: varchar("created_by", { length: 255 })
.notNull()
.references(() => users.id),
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
() => new Date(),
),
});
export const posts = createTable(
"post",
{
id: serial("id").primaryKey(),
id: varchar("id", { length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: varchar("name", { length: 256 }),
createdById: varchar("created_by", { length: 255 })
.notNull()