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
|
||||
/coverage
|
||||
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
/prisma/db.sqlite-journal
|
||||
db.sqlite
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
|
||||
# production
|
||||
/build
|
||||
@@ -26,6 +32,8 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# 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
|
||||
|
||||
# vercel
|
||||
@@ -33,10 +41,6 @@ yarn-error.log*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.vscode
|
||||
|
||||
.env.development
|
||||
.env.production
|
||||
public/uploads
|
||||
.working/
|
||||
# idea files
|
||||
.idea
|
||||
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
|
||||
|
||||
Open source gif hosting for nerds.
|
||||
# Warning, may contain traces of webp
|
||||
|
||||
@@ -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 = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
experimental: { appDir: true },
|
||||
};
|
||||
/**
|
||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
|
||||
* for Docker builds.
|
||||
*/
|
||||
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",
|
||||
"description": "Open source hosting for your gifs",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node ./server.js",
|
||||
"build": "next build",
|
||||
"vercel-build": "prisma generate && prisma migrate deploy && next build",
|
||||
"docker-start": "prisma migrate deploy && next start",
|
||||
"start": "next start",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"dev": "next dev",
|
||||
"lint": "next lint",
|
||||
"start:prod": "node server.js",
|
||||
"start:prod:migrate": "prisma migrate deploy && yarn start:prod",
|
||||
"slugify": "ts-node /app/prisma/slugify.ts"
|
||||
"start": "next start"
|
||||
},
|
||||
"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": {
|
||||
<<<<<<< Updated upstream
|
||||
"@headlessui/react": "^1.7.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.5",
|
||||
"@prisma/client": "^4.5.0",
|
||||
@@ -59,9 +80,25 @@
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "4.8.4",
|
||||
"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": {
|
||||
"seed": "dotenv -e .env.development -- ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts",
|
||||
"asdseed": "ts-node /app/prisma/seed.ts"
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.37.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> => {
|
||||
const hashed = await bcrypt.hash(password, 0);
|
||||
@@ -7,7 +7,7 @@ export const hashPassword = async (password: string): Promise<string> => {
|
||||
|
||||
export const confirmPassword = async (
|
||||
password: string,
|
||||
hashed: string
|
||||
hashed: string,
|
||||
): Promise<boolean> => {
|
||||
const result = await bcrypt.compare(password, hashed);
|
||||
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"
|
||||