Initial T3 move started
@@ -1,13 +0,0 @@
|
|||||||
# Editor configuration, see http://editorconfig.org
|
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
max_line_length = off
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
19
.env
@@ -0,0 +1,19 @@
|
|||||||
|
<<<<<<< Updated upstream
|
||||||
|
=======
|
||||||
|
# When adding additional environment variables, the schema in "/src/env.js"
|
||||||
|
# should be updated accordingly.
|
||||||
|
|
||||||
|
# Drizzle
|
||||||
|
DATABASE_URL="postgresql://postgres:hackme@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"
|
||||||
|
|
||||||
|
# Next Auth Discord Provider
|
||||||
|
DISCORD_CLIENT_ID=""
|
||||||
|
DISCORD_CLIENT_SECRET=""
|
||||||
|
>>>>>>> Stashed changes
|
||||||
|
|||||||
20
.env.example
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 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"
|
||||||
61
.eslintrc.cjs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
const config = {
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": true
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"drizzle"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"plugin:@typescript-eslint/recommended-type-checked",
|
||||||
|
"plugin:@typescript-eslint/stylistic-type-checked"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/array-type": "off",
|
||||||
|
"@typescript-eslint/consistent-type-definitions": "off",
|
||||||
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"prefer": "type-imports",
|
||||||
|
"fixStyle": "inline-type-imports"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/require-await": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"checksVoidReturn": {
|
||||||
|
"attributes": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"drizzle/enforce-delete-with-where": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"drizzleObjectName": [
|
||||||
|
"db",
|
||||||
|
"ctx.db"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"drizzle/enforce-update-with-where": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"drizzleObjectName": [
|
||||||
|
"db",
|
||||||
|
"ctx.db"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = config;
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "next/core-web-vitals",
|
|
||||||
"rules": {
|
|
||||||
"@next/next/no-img-element": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
.gitignore
vendored
@@ -8,9 +8,15 @@
|
|||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
|
# database
|
||||||
|
/prisma/db.sqlite
|
||||||
|
/prisma/db.sqlite-journal
|
||||||
|
db.sqlite
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
@@ -26,6 +32,8 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||||
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
@@ -33,10 +41,6 @@ yarn-error.log*
|
|||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
.env.development
|
# idea files
|
||||||
.env.production
|
.idea
|
||||||
public/uploads
|
|
||||||
.working/
|
|
||||||
8
.hintrc
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": [
|
|
||||||
"development"
|
|
||||||
],
|
|
||||||
"hints": {
|
|
||||||
"apple-touch-icons": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
.idea/.gitignore
generated
vendored
@@ -1,5 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
13
.idea/frasier-gifs.iml
generated
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.vscode" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/frasier-gifs.iml" filepath="$PROJECT_DIR$/.idea/frasier-gifs.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"singleAttributePerLine": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"semi": true
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
public/samples
|
|
||||||
public/uploads
|
|
||||||
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"workbench.colorTheme": "SynthWave '84"
|
||||||
|
}
|
||||||
21
LICENSE
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022 Fergal Moran
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
# Open Gifame
|
# Warning, may contain traces of webp
|
||||||
|
|
||||||
Open source gif hosting for nerds.
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const ErrorPage = () => {
|
|
||||||
return <div>ErrorPage</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ErrorPage;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const RequestPage = () => {
|
|
||||||
return (
|
|
||||||
<div className="relative px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="mx-auto text-lg max-w-prose">
|
|
||||||
<h1>
|
|
||||||
<span className="block text-base font-semibold tracking-wide text-center uppercase">
|
|
||||||
Work in progress
|
|
||||||
</span>
|
|
||||||
<span className="block mt-2 text-3xl font-extrabold leading-8 tracking-tight text-center sm:text-4xl">
|
|
||||||
Request a gif
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<p className="mt-8 text-xl leading-8">
|
|
||||||
Here you can allow your users to request a gif, if you have the TV
|
|
||||||
Show module enabled you can allow Season/Episode/Timestamp/Duration
|
|
||||||
type stuff
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RequestPage;
|
|
||||||
12
drizzle.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { type Config } from "drizzle-kit";
|
||||||
|
|
||||||
|
import { env } from "@/env";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
schema: "./src/server/db/schema.ts",
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
url: env.DATABASE_URL,
|
||||||
|
},
|
||||||
|
tablesFilter: ["opengifame_*"],
|
||||||
|
} satisfies Config;
|
||||||
66
drizzle/0000_sleepy_whizzer.sql
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "opengifame_account" (
|
||||||
|
"user_id" varchar(255) NOT NULL,
|
||||||
|
"type" varchar(255) NOT NULL,
|
||||||
|
"provider" varchar(255) NOT NULL,
|
||||||
|
"provider_account_id" varchar(255) NOT NULL,
|
||||||
|
"refresh_token" text,
|
||||||
|
"access_token" text,
|
||||||
|
"expires_at" integer,
|
||||||
|
"token_type" varchar(255),
|
||||||
|
"scope" varchar(255),
|
||||||
|
"id_token" text,
|
||||||
|
"session_state" varchar(255),
|
||||||
|
CONSTRAINT "opengifame_account_provider_provider_account_id_pk" PRIMARY KEY("provider","provider_account_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "opengifame_post" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" varchar(256),
|
||||||
|
"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
|
||||||
|
CREATE TABLE IF NOT EXISTS "opengifame_session" (
|
||||||
|
"session_token" varchar(255) PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" varchar(255) NOT NULL,
|
||||||
|
"expires" timestamp with time zone NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "opengifame_user" (
|
||||||
|
"id" varchar(255) PRIMARY KEY NOT NULL,
|
||||||
|
"name" varchar(255),
|
||||||
|
"email" varchar(255) NOT NULL,
|
||||||
|
"email_verified" timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"image" varchar(255)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "opengifame_verification_token" (
|
||||||
|
"identifier" varchar(255) NOT NULL,
|
||||||
|
"token" varchar(255) NOT NULL,
|
||||||
|
"expires" timestamp with time zone NOT NULL,
|
||||||
|
CONSTRAINT "opengifame_verification_token_identifier_token_pk" PRIMARY KEY("identifier","token")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "opengifame_account" ADD CONSTRAINT "opengifame_account_user_id_opengifame_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."opengifame_user"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "opengifame_post" ADD CONSTRAINT "opengifame_post_created_by_opengifame_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."opengifame_user"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "opengifame_session" ADD CONSTRAINT "opengifame_session_user_id_opengifame_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."opengifame_user"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "account_user_id_idx" ON "opengifame_account" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "created_by_idx" ON "opengifame_post" USING btree ("created_by");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "name_idx" ON "opengifame_post" USING btree ("name");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "session_user_id_idx" ON "opengifame_session" USING btree ("user_id");
|
||||||
351
drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
{
|
||||||
|
"id": "eba24bbe-db75-4d3a-a69e-fc8fc26a5173",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.opengifame_account": {
|
||||||
|
"name": "opengifame_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": {
|
||||||
|
"opengifame_account_user_id_opengifame_user_id_fk": {
|
||||||
|
"name": "opengifame_account_user_id_opengifame_user_id_fk",
|
||||||
|
"tableFrom": "opengifame_account",
|
||||||
|
"tableTo": "opengifame_user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"opengifame_account_provider_provider_account_id_pk": {
|
||||||
|
"name": "opengifame_account_provider_provider_account_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"provider",
|
||||||
|
"provider_account_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.opengifame_post": {
|
||||||
|
"name": "opengifame_post",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"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": {
|
||||||
|
"opengifame_post_created_by_opengifame_user_id_fk": {
|
||||||
|
"name": "opengifame_post_created_by_opengifame_user_id_fk",
|
||||||
|
"tableFrom": "opengifame_post",
|
||||||
|
"tableTo": "opengifame_user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"created_by"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.opengifame_session": {
|
||||||
|
"name": "opengifame_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": {
|
||||||
|
"opengifame_session_user_id_opengifame_user_id_fk": {
|
||||||
|
"name": "opengifame_session_user_id_opengifame_user_id_fk",
|
||||||
|
"tableFrom": "opengifame_session",
|
||||||
|
"tableTo": "opengifame_user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.opengifame_user": {
|
||||||
|
"name": "opengifame_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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.opengifame_verification_token": {
|
||||||
|
"name": "opengifame_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": {
|
||||||
|
"opengifame_verification_token_identifier_token_pk": {
|
||||||
|
"name": "opengifame_verification_token_identifier_token_pk",
|
||||||
|
"columns": [
|
||||||
|
"identifier",
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1725317790880,
|
||||||
|
"tag": "0000_sleepy_whizzer",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Install dependencies only when needed
|
|
||||||
FROM node:16-alpine AS deps
|
|
||||||
|
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
|
||||||
RUN \
|
|
||||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
|
||||||
elif [ -f package-lock.json ]; then npm ci; \
|
|
||||||
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
|
|
||||||
else echo "Lockfile not found." && exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rebuild the source code only when needed
|
|
||||||
FROM node:16-alpine AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
|
||||||
RUN npx prisma generate
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
FROM node:16-alpine AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
|
||||||
RUN adduser --system --uid 1001 nextjs
|
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
|
||||||
COPY --from=builder /app/prisma/seed.ts ./prisma/seed.ts
|
|
||||||
RUN npm install --global ts-node
|
|
||||||
|
|
||||||
# Automatically leverage output traces to reduce image size
|
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
|
||||||
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
ENV PORT 3000
|
|
||||||
|
|
||||||
CMD ["yarn", "start:prod:migrate"]
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
version: "3.3"
|
|
||||||
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
image: ghcr.io/fergalmoran/opengifame
|
|
||||||
volumes:
|
|
||||||
- type: volume
|
|
||||||
source: uploads
|
|
||||||
target: /app/public/uploads
|
|
||||||
volume:
|
|
||||||
nocopy: true
|
|
||||||
ports:
|
|
||||||
- "3043:3000"
|
|
||||||
depends_on:
|
|
||||||
- "db"
|
|
||||||
environment:
|
|
||||||
- DB_CONNECTION=${DB_CONNECTION:-secret}
|
|
||||||
- ADMIN_USER=${ADMIN_USER:-fergal.moran+live@gmail.com}
|
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-secret}
|
|
||||||
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:secret@db:5432/opengifame?schema=public}
|
|
||||||
- SECRET_KEY==${SECRET_KEY:-PLEASEDONTUSETHIS}
|
|
||||||
- NEXTAUTH_URL==${NEXTAUTH_URL:-https://dev.fergl.ie:3000}
|
|
||||||
db:
|
|
||||||
image: postgres:13
|
|
||||||
volumes:
|
|
||||||
- type: volume
|
|
||||||
source: data
|
|
||||||
target: /var/lib/postgresql/data
|
|
||||||
volume:
|
|
||||||
nocopy: true
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
environment:
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-secret}
|
|
||||||
- POSTGRES_DB=${POSTGRES_DB-opengifame}
|
|
||||||
volumes:
|
|
||||||
uploads:
|
|
||||||
data:
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import cookie from 'cookie';
|
|
||||||
import { ParsedUrlQuery } from 'querystring';
|
|
||||||
|
|
||||||
export const generateBrowserId = () => {
|
|
||||||
// always start with a letter (for DOM friendlyness)
|
|
||||||
var idstr = String.fromCharCode(Math.floor(Math.random() * 25 + 65));
|
|
||||||
do {
|
|
||||||
// between numbers and characters (48 is 0 and 90 is Z (42-48 = 90)
|
|
||||||
var ascicode = Math.floor(Math.random() * 42 + 48);
|
|
||||||
if (ascicode < 58 || ascicode > 64) {
|
|
||||||
// exclude all chars between : (58) and @ (64)
|
|
||||||
idstr += String.fromCharCode(ascicode);
|
|
||||||
}
|
|
||||||
} while (idstr.length < 256);
|
|
||||||
|
|
||||||
return idstr;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBrowserId = (c: string) => {
|
|
||||||
const parsed = cookie.parse(c);
|
|
||||||
return parsed.bid;
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import log from 'loglevel';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import prefix from 'loglevel-plugin-prefix';
|
|
||||||
|
|
||||||
const colours = {
|
|
||||||
TRACE: chalk.magenta,
|
|
||||||
DEBUG: chalk.cyan,
|
|
||||||
INFO: chalk.blue,
|
|
||||||
WARN: chalk.yellow,
|
|
||||||
ERROR: chalk.red,
|
|
||||||
};
|
|
||||||
type ObjectKey = keyof typeof colours;
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV == 'development') {
|
|
||||||
log.setLevel('debug');
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix.reg(log);
|
|
||||||
|
|
||||||
prefix.apply(log, {
|
|
||||||
format(level, name, timestamp) {
|
|
||||||
return `${chalk.gray(`[${timestamp}]`)} ${colours[
|
|
||||||
level.toUpperCase() as ObjectKey
|
|
||||||
](level)} ${chalk.green(`${name}:`)}`;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export { log as logger };
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import type { Gif as modelGif } from '@models';
|
|
||||||
import { Gif } from '@prisma/client';
|
|
||||||
|
|
||||||
export const mapGif = (
|
|
||||||
gif: Gif & { _count: { upVotes: number; downVotes: number } }
|
|
||||||
): modelGif => {
|
|
||||||
return {
|
|
||||||
id: gif.id,
|
|
||||||
slug: gif.slug,
|
|
||||||
title: gif.title,
|
|
||||||
description: gif.description,
|
|
||||||
fileName: `/uploads/${gif.id}.gif`,
|
|
||||||
searchTerms: gif.searchTerms,
|
|
||||||
dateCreated: gif.createdAt.toISOString(),
|
|
||||||
upVotes: gif._count.upVotes,
|
|
||||||
downVotes: gif._count.downVotes,
|
|
||||||
hasVoted: false,
|
|
||||||
fixedEmbedCode: `<iframe title="${gif.title}" width="800" height="600" frameBorder="0" src="${process.env.API_URL}/share/${gif.slug}">`,
|
|
||||||
responsiveEmbedCode: `<iframe title="${gif.title}" width="800" height="600" frameBorder="0" src="${process.env.API_URL}/share/${gif.slug}">`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var prisma: PrismaClient | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = globalThis.prisma || new PrismaClient();
|
|
||||||
if (process.env.NODE_ENV !== 'production') globalThis.prisma = client;
|
|
||||||
|
|
||||||
export default client;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import Season from './season';
|
|
||||||
|
|
||||||
export default interface Episode {
|
|
||||||
id: string;
|
|
||||||
number: number;
|
|
||||||
name: string;
|
|
||||||
airDate: Date;
|
|
||||||
season: Season;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export default interface Gif {
|
|
||||||
id: string;
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
description: string | null;
|
|
||||||
fileName: string;
|
|
||||||
searchTerms: string[];
|
|
||||||
dateCreated: string;
|
|
||||||
upVotes: number;
|
|
||||||
downVotes: number;
|
|
||||||
hasVoted: Boolean;
|
|
||||||
fixedEmbedCode: string;
|
|
||||||
responsiveEmbedCode: string;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import Episode from './episode';
|
|
||||||
import type Gif from './gif';
|
|
||||||
import Season from './season';
|
|
||||||
import type VoteModel from './vote';
|
|
||||||
|
|
||||||
export type { Gif, VoteModel, Season, Episode };
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import Episode from './episode';
|
|
||||||
|
|
||||||
export default interface Season {
|
|
||||||
id: string;
|
|
||||||
number: number;
|
|
||||||
episodes: Episode[];
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default interface VoteModel {
|
|
||||||
id: String;
|
|
||||||
isUp: boolean;
|
|
||||||
browserId: String;
|
|
||||||
createdAt: Date;
|
|
||||||
gifId: String;
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/**
|
||||||
const nextConfig = {
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
|
||||||
reactStrictMode: true,
|
* for Docker builds.
|
||||||
swcMinify: true,
|
*/
|
||||||
experimental: { appDir: true },
|
await import("./src/env.js");
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = nextConfig;
|
/** @type {import("next").NextConfig} */
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|||||||
5534
package-lock.json
generated
Normal file
61
package.json
@@ -1,20 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "opengifame",
|
"name": "opengifame",
|
||||||
"description": "Open source hosting for your gifs",
|
"version": "0.1.0",
|
||||||
"version": "0.1.4",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node ./server.js",
|
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"vercel-build": "prisma generate && prisma migrate deploy && next build",
|
"db:generate": "drizzle-kit generate",
|
||||||
"docker-start": "prisma migrate deploy && next start",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"start": "next start",
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"dev": "next dev",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start:prod": "node server.js",
|
"start": "next start"
|
||||||
"start:prod:migrate": "prisma migrate deploy && yarn start:prod",
|
},
|
||||||
"slugify": "ts-node /app/prisma/slugify.ts"
|
"dependencies": {
|
||||||
|
"@auth/drizzle-adapter": "^1.1.0",
|
||||||
|
"@t3-oss/env-nextjs": "^0.10.1",
|
||||||
|
"@tanstack/react-query": "^5.50.0",
|
||||||
|
"@trpc/client": "^11.0.0-rc.446",
|
||||||
|
"@trpc/react-query": "^11.0.0-rc.446",
|
||||||
|
"@trpc/server": "^11.0.0-rc.446",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"drizzle-orm": "^0.33.0",
|
||||||
|
"geist": "^1.3.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"next": "^14.2.4",
|
||||||
|
"next-auth": "^4.24.7",
|
||||||
|
"postgres": "^3.4.4",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"server-only": "^0.0.1",
|
||||||
|
"superjson": "^2.2.1",
|
||||||
|
"zod": "^3.23.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
<<<<<<< Updated upstream
|
||||||
"@headlessui/react": "^1.7.4",
|
"@headlessui/react": "^1.7.4",
|
||||||
"@next-auth/prisma-adapter": "^1.0.5",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@prisma/client": "^4.5.0",
|
"@prisma/client": "^4.5.0",
|
||||||
@@ -59,9 +80,25 @@
|
|||||||
"tailwindcss": "^3.1.8",
|
"tailwindcss": "^3.1.8",
|
||||||
"typescript": "4.8.4",
|
"typescript": "4.8.4",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
|
=======
|
||||||
|
"@types/eslint": "^8.56.10",
|
||||||
|
"@types/node": "^20.14.10",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.1.0",
|
||||||
|
"@typescript-eslint/parser": "^8.1.0",
|
||||||
|
"drizzle-kit": "^0.24.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-next": "^14.2.4",
|
||||||
|
"eslint-plugin-drizzle": "^0.2.3",
|
||||||
|
"postcss": "^8.4.39",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"typescript": "^5.5.3"
|
||||||
|
>>>>>>> Stashed changes
|
||||||
},
|
},
|
||||||
"prisma": {
|
"ct3aMetadata": {
|
||||||
"seed": "dotenv -e .env.development -- ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts",
|
"initVersion": "7.37.0"
|
||||||
"asdseed": "ts-node /app/prisma/seed.ts"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import NextAuth, { NextAuthOptions } from 'next-auth';
|
|
||||||
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
|
||||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
|
||||||
import GitHubProvider from 'next-auth/providers/github';
|
|
||||||
import GoogleProvider from 'next-auth/providers/google';
|
|
||||||
import TwitterProvider from 'next-auth/providers/twitter';
|
|
||||||
import FacebookProvider from 'next-auth/providers/facebook';
|
|
||||||
import prisma from '@lib/prismadb';
|
|
||||||
import { confirmPassword } from '@lib/crypt';
|
|
||||||
import { omit } from 'lodash';
|
|
||||||
type Credentials = {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
export const authOptions: NextAuthOptions = {
|
|
||||||
adapter: PrismaAdapter(prisma),
|
|
||||||
session: {
|
|
||||||
strategy: 'jwt',
|
|
||||||
},
|
|
||||||
callbacks: {
|
|
||||||
session: async ({ session, token }) => {
|
|
||||||
if (session?.user && token.sub) {
|
|
||||||
session.user.id = token.sub;
|
|
||||||
}
|
|
||||||
return session;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
secret: process.env.SECRET_KEY,
|
|
||||||
providers: [
|
|
||||||
GitHubProvider({
|
|
||||||
clientId: process.env.GITHUB_ID as string,
|
|
||||||
clientSecret: process.env.GITHUB_SECRET as string,
|
|
||||||
}),
|
|
||||||
FacebookProvider({
|
|
||||||
clientId: process.env.FACEBOOK_CLIENT_ID as string,
|
|
||||||
clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string,
|
|
||||||
}),
|
|
||||||
GoogleProvider({
|
|
||||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
|
||||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
|
||||||
}),
|
|
||||||
TwitterProvider({
|
|
||||||
clientId: process.env.TWITTER_CLIENT_ID as string,
|
|
||||||
clientSecret: process.env.TWITTER_CLIENT_SECRET as string,
|
|
||||||
}),
|
|
||||||
CredentialsProvider({
|
|
||||||
type: 'credentials',
|
|
||||||
credentials: {
|
|
||||||
email: { label: 'Email', type: 'email' },
|
|
||||||
password: { label: 'Password', type: 'password' },
|
|
||||||
},
|
|
||||||
authorize: async (credentials, request) => {
|
|
||||||
if (!credentials) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { email: credentials.email },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
password: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (user && user.password) {
|
|
||||||
const hashed = await confirmPassword(
|
|
||||||
credentials.password,
|
|
||||||
user.password
|
|
||||||
);
|
|
||||||
if (hashed) {
|
|
||||||
return omit(user, 'password');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
pages: {
|
|
||||||
signIn: '/auth/signin',
|
|
||||||
signOut: '/auth/signout',
|
|
||||||
error: '/auth/error',
|
|
||||||
verifyRequest: '/auth/verify-request',
|
|
||||||
newUser: '/auth/new-user',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NextAuth(authOptions);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
ping: 'pong',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import prisma from '@lib/prismadb';
|
|
||||||
|
|
||||||
export default async function handle(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
query: { q },
|
|
||||||
} = req;
|
|
||||||
const results = await prisma.tags.findMany({
|
|
||||||
where: {
|
|
||||||
name: {
|
|
||||||
contains: q as string,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return res.status(200).json(results);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import prisma from '@lib/prismadb';
|
|
||||||
import { confirmPassword, hashPassword } from '@lib/crypt';
|
|
||||||
import { logger } from '@lib/logger';
|
|
||||||
import { omit } from 'lodash';
|
|
||||||
|
|
||||||
export default async function handle(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { email: req.body.email },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
password: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
user?.password &&
|
|
||||||
(await confirmPassword(user.password, req.body.password))
|
|
||||||
) {
|
|
||||||
logger.debug('password correct');
|
|
||||||
res.json(omit(user, 'password'));
|
|
||||||
} else {
|
|
||||||
logger.debug('incorrect credentials');
|
|
||||||
res.status(400).end('Invalid credentials');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`The HTTP ${req.method} method is not supported at this route.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import prisma from '@lib/prismadb';
|
|
||||||
import { logger } from '@lib/logger';
|
|
||||||
import { hashPassword } from '@lib/crypt';
|
|
||||||
import { omit } from 'lodash';
|
|
||||||
|
|
||||||
export default async function handle(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
logger.debug('creating user', {
|
|
||||||
...req.body,
|
|
||||||
password: hashPassword(req.body.password),
|
|
||||||
});
|
|
||||||
const user = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
email: req.body.email,
|
|
||||||
password: await hashPassword(req.body.password),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
res.json(omit(user, 'password'));
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`The HTTP ${req.method} method is not supported at this route.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
postcss.config.cjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
6
prettier.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
|
||||||
|
const config = {
|
||||||
|
plugins: ["prettier-plugin-tailwindcss"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Please do not edit this file manually
|
|
||||||
# It should be added in your version-control system (i.e. Git)
|
|
||||||
provider = "postgresql"
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export async function main() {
|
|
||||||
console.log('slugify');
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 583 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.1 KiB |
15
public/icon.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<!-- Background -->
|
||||||
|
<rect x="0" y="0" width="100" height="100" rx="20" ry="20" fill="#FF6B6B"/>
|
||||||
|
|
||||||
|
<!-- Abstract prism shape - significantly enlarged -->
|
||||||
|
<path d="M50,10 L90,80 L10,80 Z" fill="white"/>
|
||||||
|
|
||||||
|
<!-- Color refraction lines - adjusted for new size -->
|
||||||
|
<path d="M50,10 L62,80" stroke="#FFD166" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M50,10 L50,80" stroke="#06D6A0" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M50,10 L38,80" stroke="#118AB2" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Circular highlight - adjusted position and size -->
|
||||||
|
<circle cx="50" cy="35" r="9" fill="#FF6B6B" stroke="white" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 755 B |
|
Before Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,55 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<path style="fill:#FBB346;" d="M273.357,385.122c0-202.505,225.648,0,225.648,0S273.357,587.626,273.357,385.122z"/>
|
|
||||||
<path style="fill:#FBB346;" d="M12.994,385.122c0,0,225.648-202.505,225.648,0S12.994,385.122,12.994,385.122z"/>
|
|
||||||
</g>
|
|
||||||
<path style="fill:#FFD248;" d="M255.999,41.188c0,0,202.505,225.648,0,225.648S255.999,41.188,255.999,41.188z"/>
|
|
||||||
<path d="M255.999,275.514c-49.985,0-81.409-13.622-93.397-40.488c-13.002-29.14-2.003-71.908,32.689-127.115
|
|
||||||
c25.016-39.808,53.067-71.203,54.249-72.52l6.459-7.198l6.459,7.198c1.181,1.317,29.234,32.712,54.249,72.52
|
|
||||||
c34.692,55.207,45.691,97.975,32.689,127.115C337.409,261.892,305.986,275.514,255.999,275.514z M256.002,54.391
|
|
||||||
c-33.418,39.315-97.514,128.835-77.547,173.566c8.945,20.039,35.034,30.199,77.544,30.199c42.512,0,68.603-10.162,77.547-30.204
|
|
||||||
C353.522,183.185,289.424,93.693,256.002,54.391z"/>
|
|
||||||
<path d="M331.875,483.807c-9.892,0-18.801-1.76-26.707-5.289c-26.866-11.988-40.488-43.411-40.488-93.396
|
|
||||||
s13.622-81.409,40.488-93.396c29.141-13.001,71.908-2.003,127.115,32.689c39.809,25.016,71.203,53.068,72.521,54.249l7.198,6.459
|
|
||||||
l-7.198,6.459c-1.318,1.181-32.712,29.234-72.521,54.249C392.056,471.109,358.432,483.807,331.875,483.807z M331.802,303.8
|
|
||||||
c-7.146,0-13.734,1.175-19.567,3.778c-20.039,8.945-30.2,35.034-30.2,77.543c0,42.512,10.162,68.603,30.204,77.545
|
|
||||||
c5.838,2.605,12.438,3.78,19.586,3.78c47.668,0.002,119.797-52.26,153.975-81.325C451.612,356.064,379.454,303.8,331.802,303.8z"/>
|
|
||||||
<path d="M180.125,483.807c-26.558,0-60.177-12.698-100.408-37.977c-39.809-25.016-71.203-53.068-72.521-54.249L0,385.122
|
|
||||||
l7.198-6.459c1.318-1.181,32.712-29.234,72.521-54.249c55.207-34.694,97.978-45.692,127.115-32.689
|
|
||||||
c26.866,11.988,40.488,43.411,40.488,93.396s-13.622,81.409-40.488,93.396C198.927,482.046,190.013,483.807,180.125,483.807z
|
|
||||||
M26.199,385.12c34.189,29.06,106.346,81.32,153.998,81.322c7.146,0,13.734-1.175,19.567-3.778
|
|
||||||
c20.039-8.945,30.2-35.034,30.2-77.543c0-42.512-10.162-68.603-30.204-77.545C154.989,287.601,65.5,351.697,26.199,385.12z"/>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 795 KiB |
14
public/manifest.webmanifest
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 461 KiB |
|
Before Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 490 KiB |
|
Before Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 4.9 MiB |
@@ -1 +0,0 @@
|
|||||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
|
||||||
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 490 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
docker --context default \
|
|
||||||
build -t ghcr.io/fergalmoran/opengifame \
|
|
||||||
-f ./hosting/Dockerfile \
|
|
||||||
--push \
|
|
||||||
.
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
patchlevel="${1:-patch}"
|
|
||||||
|
|
||||||
if ! [[ "$patchlevel" =~ ^(major|minor|patch)$ ]]; then
|
|
||||||
echo "Patchlevel must be one of major|minor|patch"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ -z "$(git status --porcelain)" ]; then
|
|
||||||
echo Repository is clean
|
|
||||||
else
|
|
||||||
echo Repository is dirty, please commit before releasing
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
current_branch=$((git symbolic-ref HEAD 2>/dev/null || echo "(unnamed branch)")|cut -d/ -f3-)
|
|
||||||
if [ "$current_branch" != "develop" ]; then
|
|
||||||
echo "You are not on develop branch, please switch to develop before releasing"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
npm --no-git-tag-version --tag-version-prefix="" version $patchlevel
|
|
||||||
version=$(cat package.json |
|
|
||||||
grep version |
|
|
||||||
head -1 |
|
|
||||||
awk -F: '{ print $2 }' |
|
|
||||||
sed 's/[",]//g' |
|
|
||||||
tr -d '[[:space:]]')
|
|
||||||
echo $version
|
|
||||||
|
|
||||||
echo "Bump version to ${version}"
|
|
||||||
|
|
||||||
git commit -am "Bump version to ${version}"
|
|
||||||
git tag -a "v${version}" -m "Release ${version}"
|
|
||||||
|
|
||||||
git checkout trunk
|
|
||||||
git merge develop
|
|
||||||
git push --tags origin trunk develop
|
|
||||||
|
|
||||||
git checkout develop
|
|
||||||
22
server.js
@@ -1,22 +0,0 @@
|
|||||||
var https = require('https');
|
|
||||||
var fs = require('fs');
|
|
||||||
|
|
||||||
const next = require('next');
|
|
||||||
const port = 3000;
|
|
||||||
const dev = process.env.NODE_ENV !== 'production';
|
|
||||||
const app = next({ dev, dir: __dirname });
|
|
||||||
const handle = app.getRequestHandler();
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
key: fs.readFileSync('/etc/letsencrypt/live/fergl.ie/privkey.pem'),
|
|
||||||
cert: fs.readFileSync('/etc/letsencrypt/live/fergl.ie/fullchain.pem'),
|
|
||||||
};
|
|
||||||
|
|
||||||
app.prepare().then(() => {
|
|
||||||
https
|
|
||||||
.createServer(options, (req, res) => handle(req, res))
|
|
||||||
.listen(port, (err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
console.log(`> Ready on localhost:${port}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
5
src/app/(site)/auth/signin/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const SignInPage = () => {
|
||||||
|
return <div>Sign me in bitch!</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignInPage;
|
||||||
50
src/app/_components/trending-images.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { api } from "@/trpc/react";
|
||||||
|
|
||||||
|
export function TrendingImages() {
|
||||||
|
const [latestPost] = api.post.getLatest.useSuspenseQuery();
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const createPost = api.post.create.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
await utils.post.invalidate();
|
||||||
|
setName("");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-xs">
|
||||||
|
{latestPost ? (
|
||||||
|
<p className="truncate">Your most recent post: {latestPost.name}</p>
|
||||||
|
) : (
|
||||||
|
<p>You have no posts yet.</p>
|
||||||
|
)}
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
createPost.mutate({ name });
|
||||||
|
}}
|
||||||
|
className="flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Title"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full rounded-full px-4 py-2 text-black"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
|
||||||
|
disabled={createPost.isPending}
|
||||||
|
>
|
||||||
|
{createPost.isPending ? "Submitting..." : "Submit"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import NextAuth from "next-auth";
|
||||||
|
|
||||||
|
import { authOptions } from "@/server/auth";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const handler = NextAuth(authOptions);
|
||||||
|
export { handler as GET, handler as POST };
|
||||||
34
src/app/api/trpc/[trpc]/route.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||||
|
import { type NextRequest } from "next/server";
|
||||||
|
|
||||||
|
import { env } from "@/env";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { createTRPCContext } from "@/server/api/trpc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
|
||||||
|
* handling a HTTP request (e.g. when you make requests from Client Components).
|
||||||
|
*/
|
||||||
|
const createContext = async (req: NextRequest) => {
|
||||||
|
return createTRPCContext({
|
||||||
|
headers: req.headers,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = (req: NextRequest) =>
|
||||||
|
fetchRequestHandler({
|
||||||
|
endpoint: "/api/trpc",
|
||||||
|
req,
|
||||||
|
router: appRouter,
|
||||||
|
createContext: () => createContext(req),
|
||||||
|
onError:
|
||||||
|
env.NODE_ENV === "development"
|
||||||
|
? ({ path, error }) => {
|
||||||
|
console.error(
|
||||||
|
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST };
|
||||||
31
src/app/layout.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import "@/styles/globals.css";
|
||||||
|
|
||||||
|
import { GeistSans } from "geist/font/sans";
|
||||||
|
import { type Metadata } from "next";
|
||||||
|
|
||||||
|
import { TRPCReactProvider } from "@/trpc/react";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Open Gifame",
|
||||||
|
description: "Contains traces of gifs",
|
||||||
|
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className={`${GeistSans.variable}`}>
|
||||||
|
<head>
|
||||||
|
<title>Open Gifame</title>
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/app/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { TrendingImages } from "@/app/_components/trending-images";
|
||||||
|
import { getServerAuthSession } from "@/server/auth";
|
||||||
|
import { api, HydrateClient } from "@/trpc/server";
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
const session = await getServerAuthSession();
|
||||||
|
void api.post.getLatest.prefetch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HydrateClient>
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
|
||||||
|
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
|
||||||
|
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
|
||||||
|
Warning <span className="text-[hsl(280,100%,70%)]">contains</span>{" "}
|
||||||
|
Gifs
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/auth/signin">Sign In</a>
|
||||||
|
</div>
|
||||||
|
{session?.user && <TrendingImages />}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</HydrateClient>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
src/env.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { createEnv } from "@t3-oss/env-nextjs";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const env = createEnv({
|
||||||
|
/**
|
||||||
|
* Specify your server-side environment variables schema here. This way you can ensure the app
|
||||||
|
* isn't built with invalid env vars.
|
||||||
|
*/
|
||||||
|
server: {
|
||||||
|
DATABASE_URL: z.string().url(),
|
||||||
|
NODE_ENV: z
|
||||||
|
.enum(["development", "test", "production"])
|
||||||
|
.default("development"),
|
||||||
|
NEXTAUTH_SECRET:
|
||||||
|
process.env.NODE_ENV === "production"
|
||||||
|
? z.string()
|
||||||
|
: z.string().optional(),
|
||||||
|
NEXTAUTH_URL: z.preprocess(
|
||||||
|
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
|
||||||
|
// Since NextAuth.js automatically uses the VERCEL_URL if present.
|
||||||
|
(str) => process.env.VERCEL_URL ?? str,
|
||||||
|
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
|
||||||
|
process.env.VERCEL ? z.string() : z.string().url()
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify your client-side environment variables schema here. This way you can ensure the app
|
||||||
|
* isn't built with invalid env vars. To expose them to the client, prefix them with
|
||||||
|
* `NEXT_PUBLIC_`.
|
||||||
|
*/
|
||||||
|
client: {
|
||||||
|
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
|
||||||
|
* middlewares) or client-side so we need to destruct manually.
|
||||||
|
*/
|
||||||
|
runtimeEnv: {
|
||||||
|
DATABASE_URL: process.env.DATABASE_URL,
|
||||||
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||||
|
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
* useful for Docker builds.
|
||||||
|
*/
|
||||||
|
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||||
|
/**
|
||||||
|
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
|
||||||
|
* `SOME_VAR=''` will throw an error.
|
||||||
|
*/
|
||||||
|
emptyStringAsUndefined: true,
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import bcrypt from 'bcrypt';
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
export const hashPassword = async (password: string): Promise<string> => {
|
export const hashPassword = async (password: string): Promise<string> => {
|
||||||
const hashed = await bcrypt.hash(password, 0);
|
const hashed = await bcrypt.hash(password, 0);
|
||||||
@@ -7,7 +7,7 @@ export const hashPassword = async (password: string): Promise<string> => {
|
|||||||
|
|
||||||
export const confirmPassword = async (
|
export const confirmPassword = async (
|
||||||
password: string,
|
password: string,
|
||||||
hashed: string
|
hashed: string,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const result = await bcrypt.compare(password, hashed);
|
const result = await bcrypt.compare(password, hashed);
|
||||||
return result;
|
return result;
|
||||||
23
src/server/api/root.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { postRouter } from "@/server/api/routers/post";
|
||||||
|
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the primary router for your server.
|
||||||
|
*
|
||||||
|
* All routers added in /api/routers should be manually added here.
|
||||||
|
*/
|
||||||
|
export const appRouter = createTRPCRouter({
|
||||||
|
post: postRouter,
|
||||||
|
});
|
||||||
|
|
||||||
|
// export type definition of API
|
||||||
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a server-side caller for the tRPC API.
|
||||||
|
* @example
|
||||||
|
* const trpc = createCaller(createContext);
|
||||||
|
* const res = await trpc.post.all();
|
||||||
|
* ^? Post[]
|
||||||
|
*/
|
||||||
|
export const createCaller = createCallerFactory(appRouter);
|
||||||
15
src/server/api/routers/auth.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { users } from "@/server/db/schema";
|
||||||
|
|
||||||
|
export const authRouter = createTRPCRouter({
|
||||||
|
create: publicProcedure
|
||||||
|
.input(z.object({ email: z.string().email(), password: z.string().min(8) }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const user = await ctx.db.insert(users).values({
|
||||||
|
email: input.email,
|
||||||
|
password: input.password,
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}),
|
||||||
|
});
|
||||||
39
src/server/api/routers/post.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
publicProcedure,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
|
import { posts } from "@/server/db/schema";
|
||||||
|
|
||||||
|
export const postRouter = createTRPCRouter({
|
||||||
|
hello: publicProcedure
|
||||||
|
.input(z.object({ text: z.string() }))
|
||||||
|
.query(({ input }) => {
|
||||||
|
return {
|
||||||
|
greeting: `Hello ${input.text}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
create: protectedProcedure
|
||||||
|
.input(z.object({ name: z.string().min(1) }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
await ctx.db.insert(posts).values({
|
||||||
|
name: input.name,
|
||||||
|
createdById: ctx.session.user.id,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
getLatest: publicProcedure.query(async ({ ctx }) => {
|
||||||
|
const post = await ctx.db.query.posts.findFirst({
|
||||||
|
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return post ?? null;
|
||||||
|
}),
|
||||||
|
|
||||||
|
getSecretMessage: protectedProcedure.query(() => {
|
||||||
|
return "you can now see this secret message!";
|
||||||
|
}),
|
||||||
|
});
|
||||||
133
src/server/api/trpc.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
|
||||||
|
* 1. You want to modify request context (see Part 1).
|
||||||
|
* 2. You want to create a new middleware or type of procedure (see Part 3).
|
||||||
|
*
|
||||||
|
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
||||||
|
* need to use are documented accordingly near the end.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { initTRPC, TRPCError } from "@trpc/server";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { getServerAuthSession } from "@/server/auth";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. CONTEXT
|
||||||
|
*
|
||||||
|
* This section defines the "contexts" that are available in the backend API.
|
||||||
|
*
|
||||||
|
* These allow you to access things when processing a request, like the database, the session, etc.
|
||||||
|
*
|
||||||
|
* This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
|
||||||
|
* wrap this and provides the required context.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/server/context
|
||||||
|
*/
|
||||||
|
export const createTRPCContext = async (opts: { headers: Headers }) => {
|
||||||
|
const session = await getServerAuthSession();
|
||||||
|
|
||||||
|
return {
|
||||||
|
db,
|
||||||
|
session,
|
||||||
|
...opts,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2. INITIALIZATION
|
||||||
|
*
|
||||||
|
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
|
||||||
|
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
|
||||||
|
* errors on the backend.
|
||||||
|
*/
|
||||||
|
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||||
|
transformer: superjson,
|
||||||
|
errorFormatter({ shape, error }) {
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
data: {
|
||||||
|
...shape.data,
|
||||||
|
zodError:
|
||||||
|
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a server-side caller.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/server/server-side-calls
|
||||||
|
*/
|
||||||
|
export const createCallerFactory = t.createCallerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||||
|
*
|
||||||
|
* These are the pieces you use to build your tRPC API. You should import these a lot in the
|
||||||
|
* "/src/server/api/routers" directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is how you create new routers and sub-routers in your tRPC API.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/router
|
||||||
|
*/
|
||||||
|
export const createTRPCRouter = t.router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware for timing procedure execution and adding an artificial delay in development.
|
||||||
|
*
|
||||||
|
* You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
|
||||||
|
* network latency that would occur in production but not in local development.
|
||||||
|
*/
|
||||||
|
const timingMiddleware = t.middleware(async ({ next, path }) => {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
if (t._config.isDev) {
|
||||||
|
// artificial delay in dev
|
||||||
|
const waitMs = Math.floor(Math.random() * 400) + 100;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await next();
|
||||||
|
|
||||||
|
const end = Date.now();
|
||||||
|
console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public (unauthenticated) procedure
|
||||||
|
*
|
||||||
|
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
|
||||||
|
* guarantee that a user querying is authorized, but you can still access user session data if they
|
||||||
|
* are logged in.
|
||||||
|
*/
|
||||||
|
export const publicProcedure = t.procedure.use(timingMiddleware);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protected (authenticated) procedure
|
||||||
|
*
|
||||||
|
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
|
||||||
|
* the session is valid and guarantees `ctx.session.user` is not null.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/procedures
|
||||||
|
*/
|
||||||
|
export const protectedProcedure = t.procedure
|
||||||
|
.use(timingMiddleware)
|
||||||
|
.use(({ ctx, next }) => {
|
||||||
|
if (!ctx.session || !ctx.session.user) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
// infers the `session` as non-nullable
|
||||||
|
session: { ...ctx.session, user: ctx.session.user },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
109
src/server/auth.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||||
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
|
import {
|
||||||
|
getServerSession,
|
||||||
|
type DefaultSession,
|
||||||
|
type NextAuthOptions,
|
||||||
|
} from "next-auth";
|
||||||
|
import { type Adapter } from "next-auth/adapters";
|
||||||
|
import { omit } from "lodash";
|
||||||
|
|
||||||
|
import { env } from "@/env";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import {
|
||||||
|
accounts,
|
||||||
|
sessions,
|
||||||
|
users,
|
||||||
|
verificationTokens,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
import { confirmPassword } from "@/lib/crypt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
||||||
|
* object and keep type safety.
|
||||||
|
*
|
||||||
|
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
|
||||||
|
*/
|
||||||
|
declare module "next-auth" {
|
||||||
|
interface Session extends DefaultSession {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
// ...other properties
|
||||||
|
// role: UserRole;
|
||||||
|
} & DefaultSession["user"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface User {
|
||||||
|
// // ...other properties
|
||||||
|
// // role: UserRole;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
|
||||||
|
*
|
||||||
|
* @see https://next-auth.js.org/configuration/options
|
||||||
|
*/
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
callbacks: {
|
||||||
|
session: ({ session, user }) => ({
|
||||||
|
...session,
|
||||||
|
user: {
|
||||||
|
...session.user,
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
adapter: DrizzleAdapter(db, {
|
||||||
|
usersTable: users,
|
||||||
|
accountsTable: accounts,
|
||||||
|
sessionsTable: sessions,
|
||||||
|
verificationTokensTable: verificationTokens,
|
||||||
|
}) as Adapter,
|
||||||
|
providers: [
|
||||||
|
CredentialsProvider({
|
||||||
|
type: "credentials",
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "email" },
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
authorize: async (credentials, request) => {
|
||||||
|
if (!credentials) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// const user = await prisma.users.findUnique({
|
||||||
|
// where: { email: credentials.email },
|
||||||
|
// select: {
|
||||||
|
// id: true,
|
||||||
|
// email: true,
|
||||||
|
// password: true,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// if (user && user.password) {
|
||||||
|
// const hashed = await confirmPassword(
|
||||||
|
// credentials.password,
|
||||||
|
// user.password,
|
||||||
|
// );
|
||||||
|
// if (hashed) {
|
||||||
|
// return omit(user, "password");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
pages: {
|
||||||
|
signIn: "/auth/signin",
|
||||||
|
signOut: "/auth/signout",
|
||||||
|
error: "/auth/error",
|
||||||
|
verifyRequest: "/auth/verify-request",
|
||||||
|
newUser: "/auth/new-user",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
|
||||||
|
*
|
||||||
|
* @see https://next-auth.js.org/configuration/nextjs
|
||||||
|
*/
|
||||||
|
export const getServerAuthSession = () => getServerSession(authOptions);
|
||||||
18
src/server/db/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import postgres from "postgres";
|
||||||
|
|
||||||
|
import { env } from "@/env";
|
||||||
|
import * as schema from "./schema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache the database connection in development. This avoids creating a new connection on every HMR
|
||||||
|
* update.
|
||||||
|
*/
|
||||||
|
const globalForDb = globalThis as unknown as {
|
||||||
|
conn: postgres.Sql | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connection = globalForDb.conn ?? postgres(env.DATABASE_URL);
|
||||||
|
if (env.NODE_ENV !== "production") globalForDb.conn = connection;
|
||||||
|
|
||||||
|
export const db = drizzle(connection, { schema });
|
||||||
9
src/server/db/migrate.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import "dotenv/config";
|
||||||
|
import { connection, db } from ".";
|
||||||
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||||
|
|
||||||
|
// This will run migrations on the database, skipping the ones already applied
|
||||||
|
await migrate(db, { migrationsFolder: "./drizzle" });
|
||||||
|
|
||||||
|
// Don't forget to close the connection, otherwise the script will hang
|
||||||
|
await connection.end();
|
||||||
131
src/server/db/schema.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { relations, sql } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
pgTableCreator,
|
||||||
|
primaryKey,
|
||||||
|
serial,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
varchar,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { type AdapterAccount } from "next-auth/adapters";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||||
|
* database instance for multiple projects.
|
||||||
|
*
|
||||||
|
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
||||||
|
*/
|
||||||
|
export const createTable = pgTableCreator((name) => `opengifame_${name}`);
|
||||||
|
|
||||||
|
export const posts = createTable(
|
||||||
|
"post",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
name: varchar("name", { length: 256 }),
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
(example) => ({
|
||||||
|
createdByIdIdx: index("created_by_idx").on(example.createdById),
|
||||||
|
nameIndex: index("name_idx").on(example.name),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const users = createTable("user", {
|
||||||
|
id: varchar("id", { length: 255 })
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => crypto.randomUUID()),
|
||||||
|
name: varchar("name", { length: 255 }),
|
||||||
|
email: varchar("email", { length: 255 }).notNull(),
|
||||||
|
emailVerified: timestamp("email_verified", {
|
||||||
|
mode: "date",
|
||||||
|
withTimezone: true,
|
||||||
|
}).default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
image: varchar("image", { length: 255 }),
|
||||||
|
password: text("password"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usersRelations = relations(users, ({ many }) => ({
|
||||||
|
accounts: many(accounts),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const accounts = createTable(
|
||||||
|
"account",
|
||||||
|
{
|
||||||
|
userId: varchar("user_id", { length: 255 })
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
type: varchar("type", { length: 255 })
|
||||||
|
.$type<AdapterAccount["type"]>()
|
||||||
|
.notNull(),
|
||||||
|
provider: varchar("provider", { length: 255 }).notNull(),
|
||||||
|
providerAccountId: varchar("provider_account_id", {
|
||||||
|
length: 255,
|
||||||
|
}).notNull(),
|
||||||
|
refresh_token: text("refresh_token"),
|
||||||
|
access_token: text("access_token"),
|
||||||
|
expires_at: integer("expires_at"),
|
||||||
|
token_type: varchar("token_type", { length: 255 }),
|
||||||
|
scope: varchar("scope", { length: 255 }),
|
||||||
|
id_token: text("id_token"),
|
||||||
|
session_state: varchar("session_state", { length: 255 }),
|
||||||
|
},
|
||||||
|
(account) => ({
|
||||||
|
compoundKey: primaryKey({
|
||||||
|
columns: [account.provider, account.providerAccountId],
|
||||||
|
}),
|
||||||
|
userIdIdx: index("account_user_id_idx").on(account.userId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const accountsRelations = relations(accounts, ({ one }) => ({
|
||||||
|
user: one(users, { fields: [accounts.userId], references: [users.id] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const sessions = createTable(
|
||||||
|
"session",
|
||||||
|
{
|
||||||
|
sessionToken: varchar("session_token", { length: 255 })
|
||||||
|
.notNull()
|
||||||
|
.primaryKey(),
|
||||||
|
userId: varchar("user_id", { length: 255 })
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
expires: timestamp("expires", {
|
||||||
|
mode: "date",
|
||||||
|
withTimezone: true,
|
||||||
|
}).notNull(),
|
||||||
|
},
|
||||||
|
(session) => ({
|
||||||
|
userIdIdx: index("session_user_id_idx").on(session.userId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const sessionsRelations = relations(sessions, ({ one }) => ({
|
||||||
|
user: one(users, { fields: [sessions.userId], references: [users.id] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const verificationTokens = createTable(
|
||||||
|
"verification_token",
|
||||||
|
{
|
||||||
|
identifier: varchar("identifier", { length: 255 }).notNull(),
|
||||||
|
token: varchar("token", { length: 255 }).notNull(),
|
||||||
|
expires: timestamp("expires", {
|
||||||
|
mode: "date",
|
||||||
|
withTimezone: true,
|
||||||
|
}).notNull(),
|
||||||
|
},
|
||||||
|
(vt) => ({
|
||||||
|
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
25
src/trpc/query-client.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
defaultShouldDehydrateQuery,
|
||||||
|
QueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
|
export const createQueryClient = () =>
|
||||||
|
new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
// With SSR, we usually want to set some default staleTime
|
||||||
|
// above 0 to avoid refetching immediately on the client
|
||||||
|
staleTime: 30 * 1000,
|
||||||
|
},
|
||||||
|
dehydrate: {
|
||||||
|
serializeData: SuperJSON.serialize,
|
||||||
|
shouldDehydrateQuery: (query) =>
|
||||||
|
defaultShouldDehydrateQuery(query) ||
|
||||||
|
query.state.status === "pending",
|
||||||
|
},
|
||||||
|
hydrate: {
|
||||||
|
deserializeData: SuperJSON.deserialize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
76
src/trpc/react.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { QueryClientProvider, type QueryClient } from "@tanstack/react-query";
|
||||||
|
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
|
||||||
|
import { createTRPCReact } from "@trpc/react-query";
|
||||||
|
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
||||||
|
import { useState } from "react";
|
||||||
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
|
import { type AppRouter } from "@/server/api/root";
|
||||||
|
import { createQueryClient } from "./query-client";
|
||||||
|
|
||||||
|
let clientQueryClientSingleton: QueryClient | undefined = undefined;
|
||||||
|
const getQueryClient = () => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
// Server: always make a new query client
|
||||||
|
return createQueryClient();
|
||||||
|
}
|
||||||
|
// Browser: use singleton pattern to keep the same query client
|
||||||
|
return (clientQueryClientSingleton ??= createQueryClient());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const api = createTRPCReact<AppRouter>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inference helper for inputs.
|
||||||
|
*
|
||||||
|
* @example type HelloInput = RouterInputs['example']['hello']
|
||||||
|
*/
|
||||||
|
export type RouterInputs = inferRouterInputs<AppRouter>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inference helper for outputs.
|
||||||
|
*
|
||||||
|
* @example type HelloOutput = RouterOutputs['example']['hello']
|
||||||
|
*/
|
||||||
|
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||||
|
|
||||||
|
export function TRPCReactProvider(props: { children: React.ReactNode }) {
|
||||||
|
const queryClient = getQueryClient();
|
||||||
|
|
||||||
|
const [trpcClient] = useState(() =>
|
||||||
|
api.createClient({
|
||||||
|
links: [
|
||||||
|
loggerLink({
|
||||||
|
enabled: (op) =>
|
||||||
|
process.env.NODE_ENV === "development" ||
|
||||||
|
(op.direction === "down" && op.result instanceof Error),
|
||||||
|
}),
|
||||||
|
unstable_httpBatchStreamLink({
|
||||||
|
transformer: SuperJSON,
|
||||||
|
url: getBaseUrl() + "/api/trpc",
|
||||||
|
headers: () => {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set("x-trpc-source", "nextjs-react");
|
||||||
|
return headers;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<api.Provider client={trpcClient} queryClient={queryClient}>
|
||||||
|
{props.children}
|
||||||
|
</api.Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseUrl() {
|
||||||
|
if (typeof window !== "undefined") return window.location.origin;
|
||||||
|
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
||||||
|
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||||
|
}
|
||||||
30
src/trpc/server.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { createHydrationHelpers } from "@trpc/react-query/rsc";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { cache } from "react";
|
||||||
|
|
||||||
|
import { createCaller, type AppRouter } from "@/server/api/root";
|
||||||
|
import { createTRPCContext } from "@/server/api/trpc";
|
||||||
|
import { createQueryClient } from "./query-client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
|
||||||
|
* handling a tRPC call from a React Server Component.
|
||||||
|
*/
|
||||||
|
const createContext = cache(() => {
|
||||||
|
const heads = new Headers(headers());
|
||||||
|
heads.set("x-trpc-source", "rsc");
|
||||||
|
|
||||||
|
return createTRPCContext({
|
||||||
|
headers: heads,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const getQueryClient = cache(createQueryClient);
|
||||||
|
const caller = createCaller(createContext);
|
||||||
|
|
||||||
|
export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(
|
||||||
|
caller,
|
||||||
|
getQueryClient
|
||||||
|
);
|
||||||
55
start-database.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Use this script to start a docker container for a local development database
|
||||||
|
|
||||||
|
# TO RUN ON WINDOWS:
|
||||||
|
# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install
|
||||||
|
# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/
|
||||||
|
# 3. Open WSL - `wsl`
|
||||||
|
# 4. Run this script - `./start-database.sh`
|
||||||
|
|
||||||
|
# On Linux and macOS you can run this script directly - `./start-database.sh`
|
||||||
|
|
||||||
|
DB_CONTAINER_NAME="opengifame-postgres"
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then
|
||||||
|
echo "Database container '$DB_CONTAINER_NAME' already running"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then
|
||||||
|
docker start "$DB_CONTAINER_NAME"
|
||||||
|
echo "Existing database container '$DB_CONTAINER_NAME' started"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# import env variables from .env
|
||||||
|
set -a
|
||||||
|
source .env
|
||||||
|
|
||||||
|
DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')
|
||||||
|
DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}')
|
||||||
|
|
||||||
|
if [ "$DB_PASSWORD" = "password" ]; then
|
||||||
|
echo "You are using the default database password"
|
||||||
|
read -p "Should we generate a random password for you? [y/N]: " -r REPLY
|
||||||
|
if ! [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Please change the default password in the .env file and try again"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Generate a random URL-safe password
|
||||||
|
DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')
|
||||||
|
sed -i -e "s#:password@#:$DB_PASSWORD@#" .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name $DB_CONTAINER_NAME \
|
||||||
|
-e POSTGRES_USER="postgres" \
|
||||||
|
-e POSTGRES_PASSWORD="$DB_PASSWORD" \
|
||||||
|
-e POSTGRES_DB=opengifame \
|
||||||
|
-p "$DB_PORT":5432 \
|
||||||
|
docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created"
|
||||||