From 7b5716e7f0805736de37a038a263c3bd1f7117e3 Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Thu, 6 Jun 2024 14:34:45 +0200
Subject: [PATCH 01/13] feat: google oauth
---
Taskfile.yml | 3 +
app/.env.dist | 8 +
app/package-lock.json | 142 ++++++++++++++++++
app/package.json | 2 +
.../migrations/20240606120109/migration.sql | 62 ++++++++
app/prisma/schema.prisma | 53 ++++++-
app/src/actions/auth.ts | 11 ++
app/src/{mutations => actions}/message.ts | 0
app/src/app/api/auth/[...nextauth]/route.ts | 2 +
app/src/app/layout.tsx | 2 +
app/src/app/messages/page.tsx | 2 +-
app/src/app/page.tsx | 8 +
app/src/auth.ts | 37 +++++
app/src/components/AuthButton.tsx | 14 ++
app/src/components/header.tsx | 24 +++
docker-compose.yaml | 7 +
16 files changed, 375 insertions(+), 2 deletions(-)
create mode 100644 app/prisma/migrations/20240606120109/migration.sql
create mode 100644 app/src/actions/auth.ts
rename app/src/{mutations => actions}/message.ts (100%)
create mode 100644 app/src/app/api/auth/[...nextauth]/route.ts
create mode 100644 app/src/app/page.tsx
create mode 100644 app/src/auth.ts
create mode 100644 app/src/components/AuthButton.tsx
create mode 100644 app/src/components/header.tsx
diff --git a/Taskfile.yml b/Taskfile.yml
index e9e1cd6..798eb2e 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -13,6 +13,7 @@ tasks:
cmds:
- cmd: docker compose up --detach
- task: prisma:deploy
+ - task: prisma:generate
init-env:
desc: Copy default environment variables
@@ -20,6 +21,8 @@ tasks:
cmd: cp .env.dist .env.local
ignore_error: true
dir: app
+ status:
+ - test -f .env.local
docker-build:
desc: Build the Docker image
diff --git a/app/.env.dist b/app/.env.dist
index 108b00b..af37ec9 100644
--- a/app/.env.dist
+++ b/app/.env.dist
@@ -1 +1,9 @@
BASE_URL=http://localhost:3000
+
+# Generate a new one with `docker compose run --rm --no-deps app npx auth secret`
+AUTH_SECRET=
+
+# Google IDs https://console.cloud.google.com/apis/credentials?hl=fr&project=knp-hot-tools-1717666401021
+AUTH_GOOGLE_ID=
+AUTH_GOOGLE_SECRET=
+AUTH_GOOGLE_RESTRICT_DOMAIN=
diff --git a/app/package-lock.json b/app/package-lock.json
index a542810..d31a37a 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -8,8 +8,10 @@
"name": "nextjs-fullstack",
"version": "0.1.0",
"dependencies": {
+ "@auth/prisma-adapter": "^2.2.0",
"@prisma/client": "^5.15.0",
"next": "14.2.3",
+ "next-auth": "^5.0.0-beta.19",
"prisma-zod-generator": "^0.8.13",
"react": "^18",
"react-dom": "^18"
@@ -53,6 +55,49 @@
"nun": "bin/nun.mjs"
}
},
+ "node_modules/@auth/core": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.32.0.tgz",
+ "integrity": "sha512-3+ssTScBd+1fd0/fscAyQN1tSygXzuhysuVVzB942ggU4mdfiTbv36P0ccVnExKWYJKvu3E2r3/zxXCCAmTOrg==",
+ "license": "ISC",
+ "dependencies": {
+ "@panva/hkdf": "^1.1.1",
+ "@types/cookie": "0.6.0",
+ "cookie": "0.6.0",
+ "jose": "^5.1.3",
+ "oauth4webapi": "^2.9.0",
+ "preact": "10.11.3",
+ "preact-render-to-string": "5.2.3"
+ },
+ "peerDependencies": {
+ "@simplewebauthn/browser": "^9.0.1",
+ "@simplewebauthn/server": "^9.0.2",
+ "nodemailer": "^6.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@simplewebauthn/browser": {
+ "optional": true
+ },
+ "@simplewebauthn/server": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@auth/prisma-adapter": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.2.0.tgz",
+ "integrity": "sha512-DiXgyJfqBb/6TPhNJ5IcRw3klxcFD+qaa7qvOYopV6rYinSUvsQkJ30m+so2SRlEsuDA6PqMu+Mf5Sd+zr3g+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "@auth/core": "0.32.0"
+ },
+ "peerDependencies": {
+ "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz",
@@ -544,6 +589,15 @@
"node": ">=8.0.0"
}
},
+ "node_modules/@panva/hkdf": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz",
+ "integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -820,6 +874,12 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT"
+ },
"node_modules/@types/cross-spawn": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",
@@ -1854,6 +1914,15 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
+ "node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -4001,6 +4070,15 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/jose": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-5.4.0.tgz",
+ "integrity": "sha512-6rpxTHPAQyWMb9A35BroFl1Sp0ST3DpPcm5EVIxZxdH+e0Hv9fwhyB3XLKFUcHNpdSDnETmBfuPPTTlYz5+USw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4456,6 +4534,33 @@
}
}
},
+ "node_modules/next-auth": {
+ "version": "5.0.0-beta.19",
+ "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.19.tgz",
+ "integrity": "sha512-YHu1igcAxZPh8ZB7GIM93dqgY6gcAzq66FOhQFheAdOx1raxNcApt05nNyNCSB6NegSiyJ4XOPsaNow4pfDmsg==",
+ "license": "ISC",
+ "dependencies": {
+ "@auth/core": "0.32.0"
+ },
+ "peerDependencies": {
+ "@simplewebauthn/browser": "^9.0.1",
+ "@simplewebauthn/server": "^9.0.2",
+ "next": "^14 || ^15.0.0-0",
+ "nodemailer": "^6.6.5",
+ "react": "^18.2.0 || ^19.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@simplewebauthn/browser": {
+ "optional": true
+ },
+ "@simplewebauthn/server": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -4624,6 +4729,15 @@
"node": ">=8"
}
},
+ "node_modules/oauth4webapi": {
+ "version": "2.10.4",
+ "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.10.4.tgz",
+ "integrity": "sha512-DSoj8QoChzOCQlJkRmYxAJCIpnXFW32R0Uq7avyghIeB6iJq0XAblOD7pcq3mx4WEBDwMuKr0Y1qveCBleG2Xw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5239,6 +5353,28 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
+ "node_modules/preact": {
+ "version": "10.11.3",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
+ "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/preact-render-to-string": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz",
+ "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==",
+ "license": "MIT",
+ "dependencies": {
+ "pretty-format": "^3.8.0"
+ },
+ "peerDependencies": {
+ "preact": ">=10"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5263,6 +5399,12 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/pretty-format": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
+ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
+ "license": "MIT"
+ },
"node_modules/prisma": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.15.0.tgz",
diff --git a/app/package.json b/app/package.json
index 702eec8..6c2527b 100644
--- a/app/package.json
+++ b/app/package.json
@@ -11,8 +11,10 @@
"type-check": "tsc"
},
"dependencies": {
+ "@auth/prisma-adapter": "^2.2.0",
"@prisma/client": "^5.15.0",
"next": "14.2.3",
+ "next-auth": "^5.0.0-beta.19",
"prisma-zod-generator": "^0.8.13",
"react": "^18",
"react-dom": "^18"
diff --git a/app/prisma/migrations/20240606120109/migration.sql b/app/prisma/migrations/20240606120109/migration.sql
new file mode 100644
index 0000000..96ba996
--- /dev/null
+++ b/app/prisma/migrations/20240606120109/migration.sql
@@ -0,0 +1,62 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" TEXT NOT NULL,
+ "name" TEXT,
+ "email" TEXT NOT NULL,
+ "emailVerified" TIMESTAMP(3),
+ "image" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Account" (
+ "userId" TEXT NOT NULL,
+ "type" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "providerAccountId" TEXT NOT NULL,
+ "refresh_token" TEXT,
+ "access_token" TEXT,
+ "expires_at" INTEGER,
+ "token_type" TEXT,
+ "scope" TEXT,
+ "id_token" TEXT,
+ "session_state" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId")
+);
+
+-- CreateTable
+CREATE TABLE "Session" (
+ "sessionToken" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "expires" TIMESTAMP(3) NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "VerificationToken" (
+ "identifier" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "expires" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
+
+-- AddForeignKey
+ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma
index 811a342..c7d3d54 100644
--- a/app/prisma/schema.prisma
+++ b/app/prisma/schema.prisma
@@ -1,4 +1,3 @@
-// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
@@ -17,3 +16,55 @@ model Message {
createdAt DateTime @default(now())
content String
}
+
+model User {
+ id String @id @default(cuid())
+ name String?
+ email String @unique
+ emailVerified DateTime?
+ image String?
+ accounts Account[]
+ sessions Session[]
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+model Account {
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String?
+ access_token String?
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String?
+ session_state String?
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@id([provider, providerAccountId])
+}
+
+model Session {
+ sessionToken String @unique
+ userId String
+ expires DateTime
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+model VerificationToken {
+ identifier String
+ token String
+ expires DateTime
+
+ @@id([identifier, token])
+}
diff --git a/app/src/actions/auth.ts b/app/src/actions/auth.ts
new file mode 100644
index 0000000..e80dfab
--- /dev/null
+++ b/app/src/actions/auth.ts
@@ -0,0 +1,11 @@
+"use server";
+
+import { signIn, signOut } from "@/auth";
+
+export async function loginWithGoogle() {
+ await signIn("google");
+};
+
+export async function logout() {
+ await signOut();
+}
diff --git a/app/src/mutations/message.ts b/app/src/actions/message.ts
similarity index 100%
rename from app/src/mutations/message.ts
rename to app/src/actions/message.ts
diff --git a/app/src/app/api/auth/[...nextauth]/route.ts b/app/src/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..7c62e2d
--- /dev/null
+++ b/app/src/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,2 @@
+import { handlers } from "@/auth";
+export const { GET, POST } = handlers;
diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx
index ed9772c..08e4427 100644
--- a/app/src/app/layout.tsx
+++ b/app/src/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import Luciole from '@/fonts/luciole';
+import Header from '@/components/header';
export const metadata: Metadata = {
title: "Create Next App",
@@ -15,6 +16,7 @@ export default function RootLayout({
return (
+
{children}
diff --git a/app/src/app/messages/page.tsx b/app/src/app/messages/page.tsx
index c811fbe..1f34cc8 100644
--- a/app/src/app/messages/page.tsx
+++ b/app/src/app/messages/page.tsx
@@ -1,6 +1,6 @@
"use server";
-import { createMessage } from '@/mutations/message';
+import { createMessage } from '@/actions/message';
import getAllMessages from '@/resolvers/message/getAllMessages';
import Link from 'next/link';
diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx
new file mode 100644
index 0000000..219c3ec
--- /dev/null
+++ b/app/src/app/page.tsx
@@ -0,0 +1,8 @@
+export default function Home() {
+ return (
+
+
Home
+
Welcome to the home page.
+
+ );
+}
diff --git a/app/src/auth.ts b/app/src/auth.ts
new file mode 100644
index 0000000..9076803
--- /dev/null
+++ b/app/src/auth.ts
@@ -0,0 +1,37 @@
+import NextAuth from "next-auth";
+import google from 'next-auth/providers/google';
+import { PrismaAdapter } from "@auth/prisma-adapter";
+import client from '@/prisma-client';
+
+const DOMAIN_RESCTRICT = process.env.AUTH_GOOGLE_RESTRICT_DOMAIN ?? '';
+
+export const { handlers, signIn, signOut, auth } = NextAuth({
+ adapter: PrismaAdapter(client),
+ providers: [
+ google({
+ authorization: {
+ params: {
+ prompt: "consent",
+ access_type: "offline",
+ response_type: "code"
+ }
+ }
+ })
+ ],
+ callbacks: {
+ async signIn(user): Promise {
+ if (DOMAIN_RESCTRICT === '') {
+ return true;
+ }
+
+ if (user.account?.provider === "google") {
+ return user.profile != null
+ && user.profile.email_verified === true
+ && typeof user.profile.email === 'string'
+ && user.profile.email.endsWith(DOMAIN_RESCTRICT);
+ }
+
+ return false;
+ }
+ }
+});
diff --git a/app/src/components/AuthButton.tsx b/app/src/components/AuthButton.tsx
new file mode 100644
index 0000000..a9d5f0f
--- /dev/null
+++ b/app/src/components/AuthButton.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+export default function AuthButton({ action, children }: {
+ action: () => Promise;
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
diff --git a/app/src/components/header.tsx b/app/src/components/header.tsx
new file mode 100644
index 0000000..d5cb996
--- /dev/null
+++ b/app/src/components/header.tsx
@@ -0,0 +1,24 @@
+"use server";
+
+import { loginWithGoogle, logout } from '@/actions/auth';
+import { auth } from '@/auth';
+import AuthButton from '@/components/AuthButton';
+
+export default async function Header() {
+ const session = await auth();
+
+ return (
+
+ {
+ session?.user == null
+ ? Sign In
+ : (
+ <>
+ Welcome, {session.user.name}
+ Sign Out
+ >
+ )
+ }
+
+ );
+}
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 61f14e8..84d241f 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -30,6 +30,13 @@ services:
retries: 5
start_period: 10s
+ adminer:
+ image: adminer
+ restart: unless-stopped
+ environment:
+ - ADMINER_DEFAULT_SERVER=postgres
+ ports:
+ - '8080:8080'
# Generate Zod schemas
prisma-zod-generator:
From 2fa7a67801d1e277e16cf0ddaeeaa1e9aaf4433d Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Thu, 6 Jun 2024 15:25:15 +0200
Subject: [PATCH 02/13] feat: use google picture and icon in header
---
app/next.config.mjs | 13 ++++++++++++-
app/package-lock.json | 10 ++++++++++
app/package.json | 2 +-
app/src/app/layout.tsx | 8 +++++---
app/src/components/Icon.tsx | 15 +++++++++++++++
app/src/components/header.tsx | 32 +++++++++++++++++++++++++++-----
6 files changed, 70 insertions(+), 10 deletions(-)
create mode 100644 app/src/components/Icon.tsx
diff --git a/app/next.config.mjs b/app/next.config.mjs
index 68dea63..4ebe1dd 100644
--- a/app/next.config.mjs
+++ b/app/next.config.mjs
@@ -1,6 +1,17 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
- output: "standalone"
+ output: "standalone",
+ transpilePackages: [
+ "lucide-react",
+ ],
+ images: {
+ remotePatterns: [
+ {
+ hostname: '*.googleusercontent.com',
+ protocol: 'https',
+ }
+ ]
+ }
};
export default nextConfig;
diff --git a/app/package-lock.json b/app/package-lock.json
index d31a37a..29d7bb7 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@auth/prisma-adapter": "^2.2.0",
"@prisma/client": "^5.15.0",
+ "lucide-react": "^0.387.0",
"next": "14.2.3",
"next-auth": "^5.0.0-beta.19",
"prisma-zod-generator": "^0.8.13",
@@ -4340,6 +4341,15 @@
"node": "14 || >=16.14"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.387.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.387.0.tgz",
+ "integrity": "sha512-NyB4oJZ0pzLHT/QgMpgCPbez6yqvz8QPBocMJBXQCInPpXcQVCUpcU1CDlRG8mT2j0KqodLQYp+F5zn8U86sXg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
diff --git a/app/package.json b/app/package.json
index 6c2527b..5e75cf2 100644
--- a/app/package.json
+++ b/app/package.json
@@ -2,7 +2,6 @@
"name": "nextjs-fullstack",
"version": "0.1.0",
"private": true,
- "type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
@@ -13,6 +12,7 @@
"dependencies": {
"@auth/prisma-adapter": "^2.2.0",
"@prisma/client": "^5.15.0",
+ "lucide-react": "^0.387.0",
"next": "14.2.3",
"next-auth": "^5.0.0-beta.19",
"prisma-zod-generator": "^0.8.13",
diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx
index 08e4427..303c438 100644
--- a/app/src/app/layout.tsx
+++ b/app/src/app/layout.tsx
@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import Luciole from '@/fonts/luciole';
-import Header from '@/components/header';
+import Header from '@/components/Header';
export const metadata: Metadata = {
title: "Create Next App",
@@ -15,9 +15,11 @@ export default function RootLayout({
}>) {
return (
-
+
- {children}
+
);
diff --git a/app/src/components/Icon.tsx b/app/src/components/Icon.tsx
new file mode 100644
index 0000000..df641b5
--- /dev/null
+++ b/app/src/components/Icon.tsx
@@ -0,0 +1,15 @@
+import dynamic from 'next/dynamic';
+import { LucideProps } from 'lucide-react';
+import dynamicIconImports from 'lucide-react/dynamicIconImports';
+
+interface IconProps extends LucideProps {
+ name: keyof typeof dynamicIconImports;
+}
+
+const Icon = ({ name, ...props }: IconProps) => {
+ const LucideIcon = dynamic(dynamicIconImports[name]);
+
+ return ;
+};
+
+export default Icon;
diff --git a/app/src/components/header.tsx b/app/src/components/header.tsx
index d5cb996..fc38b3f 100644
--- a/app/src/components/header.tsx
+++ b/app/src/components/header.tsx
@@ -3,19 +3,41 @@
import { loginWithGoogle, logout } from '@/actions/auth';
import { auth } from '@/auth';
import AuthButton from '@/components/AuthButton';
+import Icon from '@/components/Icon';
+import Image from 'next/image';
export default async function Header() {
const session = await auth();
return (
-
+
{
session?.user == null
- ? Sign In
- : (
+ ? (
+
+
+ Log in
+
+ ) : (
<>
- Welcome, {session.user.name}
- Sign Out
+ {
+ session.user.image != null
+ ? (
+ ) : (
+
+ {session.user.name?.charAt(0)}
+ )
+ }
+
+
+ Log out
+
>
)
}
From cae145cd6a6be6c728b1d6609b7b928e136b7a2b Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Thu, 6 Jun 2024 15:45:42 +0200
Subject: [PATCH 03/13] doc: updates readme
---
.task/checksum/npm-install | 1 +
README.md | 62 ++++++++++++++++++++++++++++++++++++++
Taskfile.yml | 12 +++++++-
app/.env.dist | 5 ++-
app/README.md | 36 ----------------------
app/package-lock.json | 1 +
app/package.json | 3 +-
docker-compose.yaml | 10 +-----
8 files changed, 80 insertions(+), 50 deletions(-)
create mode 100644 .task/checksum/npm-install
delete mode 100644 app/README.md
diff --git a/.task/checksum/npm-install b/.task/checksum/npm-install
new file mode 100644
index 0000000..02a48d0
--- /dev/null
+++ b/.task/checksum/npm-install
@@ -0,0 +1 @@
+3b45ef577fa625b06731f3d3d182e939
diff --git a/README.md b/README.md
index b915745..ddec71b 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,64 @@
[](https://github.com/clementvtrd/nextjs-fullstack/actions/workflows/test.yaml)
+
# NextJS Fullstack
+
+## Installation
+
+> This project was built around Docker version 25.0 and Docker Compose plugin version 2.27.
+
+### Taskfile
+
+A `Taskfile.yml`is present at the root of the repository which containers the common commands to bootstrap the project. Subfiles stands in the directory `tasks` and are imported at the beginning of the main file.
+
+You can install the `task` CLI through their website at [taskfile.dev/installation](https://taskfile.dev/installation/).
+
+### Boostrap
+
+To bootstrap the project, first run the `init` command:
+
+```sh
+task init
+```
+
+This will copy the default .env, build images and install NodeJS dependencies.
+
+You needs to update the [.env.local](./app/.env.local) with the following information:
+
+- `AUTH_SECRET` can be generate with this command:
+
+ ```sh
+ docker compose run --rm --no-deps app npx auth secret
+ ```
+
+- `AUTH_GOOGLE_ID` and `AUTH_GOOGLE_SECRET` are available in [Google Cloud Platform](https://console.cloud.google.com) under APIs & Services > Credentials. As of writing those lines, only two possibility remains :
+
+ 1. use the `KNP Hot Tools (localhost)` for development purpose only ;
+ 2. use the `KNP Hot Tools (prod)` for production server under knpnet.net domain ;
+
+- `AUTH_GOOGLE_RESTRICT_DOMAIN` might be blank during development, so you can loggin with any Google account or with `@knplabs.com` to restrict Google account from KNP Labs.
+
+### Starting containers
+
+Simply run:
+
+```sh
+task start
+```
+
+## Adminer
+
+You can start Adminer with the following command:
+
+```sh
+docker compose --profile adminer up -d adminer
+```
+
+## Help
+
+You can si all available shortcuts defined with Taskfile with this command[^1]:
+
+```sh
+task --list
+```
+
+[^1]: Only shortcuts with a description (`desc`) are displayed
diff --git a/Taskfile.yml b/Taskfile.yml
index 798eb2e..775d902 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -10,10 +10,13 @@ tasks:
- init-env
- docker-build
- npm-install
+
+ start:
+ desc: Start the project
+ deps: [init]
cmds:
- cmd: docker compose up --detach
- task: prisma:deploy
- - task: prisma:generate
init-env:
desc: Copy default environment variables
@@ -33,3 +36,10 @@ tasks:
desc: Install the dependencies
deps: [docker-build]
cmd: docker compose run --no-deps --rm app npm install --ci
+ sources:
+ - app/package.json
+ - app/package-lock.json
+ - app/prisma/schema.prisma
+ generates:
+ - app/node_modules/**/*
+ - app/prisma/generated/**/*
diff --git a/app/.env.dist b/app/.env.dist
index af37ec9..988617d 100644
--- a/app/.env.dist
+++ b/app/.env.dist
@@ -1,9 +1,8 @@
BASE_URL=http://localhost:3000
-# Generate a new one with `docker compose run --rm --no-deps app npx auth secret`
-AUTH_SECRET=
+## Auth JS
-# Google IDs https://console.cloud.google.com/apis/credentials?hl=fr&project=knp-hot-tools-1717666401021
+AUTH_SECRET=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_GOOGLE_RESTRICT_DOMAIN=
diff --git a/app/README.md b/app/README.md
deleted file mode 100644
index c403366..0000000
--- a/app/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/app/package-lock.json b/app/package-lock.json
index 29d7bb7..b1c159d 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -7,6 +7,7 @@
"": {
"name": "nextjs-fullstack",
"version": "0.1.0",
+ "hasInstallScript": true,
"dependencies": {
"@auth/prisma-adapter": "^2.2.0",
"@prisma/client": "^5.15.0",
diff --git a/app/package.json b/app/package.json
index 5e75cf2..251a438 100644
--- a/app/package.json
+++ b/app/package.json
@@ -7,7 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
- "type-check": "tsc"
+ "type-check": "tsc",
+ "postinstall": "prisma generate"
},
"dependencies": {
"@auth/prisma-adapter": "^2.2.0",
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 84d241f..486538b 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -33,16 +33,8 @@ services:
adminer:
image: adminer
restart: unless-stopped
+ profiles: [adminer]
environment:
- ADMINER_DEFAULT_SERVER=postgres
ports:
- '8080:8080'
-
- # Generate Zod schemas
- prisma-zod-generator:
- build:
- context: app
- target: development
- volumes:
- - ./app:/opt/app
- command: npx prisma generate
From aa08bfba87dc27dd489f23593d43b4d8b1578039 Mon Sep 17 00:00:00 2001
From: AntoineGonzalez <45924026+AntoineGonzalez@users.noreply.github.com>
Date: Thu, 6 Jun 2024 16:15:53 +0200
Subject: [PATCH 04/13] fix: workaround to edge-compatibility issue on authjs
prisma adapter
---
.task/checksum/npm-install | 2 +-
.../migration.sql | 24 +++++--------
app/prisma/schema.prisma | 11 ------
app/src/auth.config.ts | 34 +++++++++++++++++++
app/src/auth.ts | 33 ++----------------
app/src/middleware.ts | 14 ++++++++
6 files changed, 61 insertions(+), 57 deletions(-)
rename app/prisma/migrations/{20240606120109 => 20240606161446}/migration.sql (76%)
create mode 100644 app/src/auth.config.ts
create mode 100644 app/src/middleware.ts
diff --git a/.task/checksum/npm-install b/.task/checksum/npm-install
index 02a48d0..9572991 100644
--- a/.task/checksum/npm-install
+++ b/.task/checksum/npm-install
@@ -1 +1 @@
-3b45ef577fa625b06731f3d3d182e939
+a37bf263bb7a27ec2c9ccb37408b9bdb
diff --git a/app/prisma/migrations/20240606120109/migration.sql b/app/prisma/migrations/20240606161446/migration.sql
similarity index 76%
rename from app/prisma/migrations/20240606120109/migration.sql
rename to app/prisma/migrations/20240606161446/migration.sql
index 96ba996..55c6b07 100644
--- a/app/prisma/migrations/20240606120109/migration.sql
+++ b/app/prisma/migrations/20240606161446/migration.sql
@@ -1,3 +1,12 @@
+-- CreateTable
+CREATE TABLE "Message" (
+ "id" SERIAL NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "content" TEXT NOT NULL,
+
+ CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
+);
+
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
@@ -30,15 +39,6 @@ CREATE TABLE "Account" (
CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId")
);
--- CreateTable
-CREATE TABLE "Session" (
- "sessionToken" TEXT NOT NULL,
- "userId" TEXT NOT NULL,
- "expires" TIMESTAMP(3) NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL
-);
-
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
@@ -51,12 +51,6 @@ CREATE TABLE "VerificationToken" (
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
--- CreateIndex
-CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
--- AddForeignKey
-ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma
index c7d3d54..c389e11 100644
--- a/app/prisma/schema.prisma
+++ b/app/prisma/schema.prisma
@@ -24,7 +24,6 @@ model User {
emailVerified DateTime?
image String?
accounts Account[]
- sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -51,16 +50,6 @@ model Account {
@@id([provider, providerAccountId])
}
-model Session {
- sessionToken String @unique
- userId String
- expires DateTime
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-}
-
model VerificationToken {
identifier String
token String
diff --git a/app/src/auth.config.ts b/app/src/auth.config.ts
new file mode 100644
index 0000000..67649b8
--- /dev/null
+++ b/app/src/auth.config.ts
@@ -0,0 +1,34 @@
+import type { NextAuthConfig } from "next-auth";
+import google from 'next-auth/providers/google';
+
+const DOMAIN_RESCTRICT = process.env.AUTH_GOOGLE_RESTRICT_DOMAIN ?? '';
+
+export default {
+ providers: [
+ google({
+ authorization: {
+ params: {
+ prompt: "consent",
+ access_type: "offline",
+ response_type: "code"
+ }
+ }
+ }),
+ ],
+ callbacks: {
+ async signIn(user): Promise {
+ if (DOMAIN_RESCTRICT === '') {
+ return true;
+ }
+
+ if (user.account?.provider === "google") {
+ return user.profile != null
+ && user.profile.email_verified === true
+ && typeof user.profile.email === 'string'
+ && user.profile.email.endsWith(DOMAIN_RESCTRICT);
+ }
+
+ return false;
+ }
+ }
+} satisfies NextAuthConfig;
diff --git a/app/src/auth.ts b/app/src/auth.ts
index 9076803..326b1f1 100644
--- a/app/src/auth.ts
+++ b/app/src/auth.ts
@@ -1,37 +1,10 @@
import NextAuth from "next-auth";
-import google from 'next-auth/providers/google';
import { PrismaAdapter } from "@auth/prisma-adapter";
import client from '@/prisma-client';
-
-const DOMAIN_RESCTRICT = process.env.AUTH_GOOGLE_RESTRICT_DOMAIN ?? '';
+import authConfig from '@/auth.config';
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(client),
- providers: [
- google({
- authorization: {
- params: {
- prompt: "consent",
- access_type: "offline",
- response_type: "code"
- }
- }
- })
- ],
- callbacks: {
- async signIn(user): Promise {
- if (DOMAIN_RESCTRICT === '') {
- return true;
- }
-
- if (user.account?.provider === "google") {
- return user.profile != null
- && user.profile.email_verified === true
- && typeof user.profile.email === 'string'
- && user.profile.email.endsWith(DOMAIN_RESCTRICT);
- }
-
- return false;
- }
- }
+ session: { strategy: "jwt" },
+ ...authConfig,
});
diff --git a/app/src/middleware.ts b/app/src/middleware.ts
new file mode 100644
index 0000000..1876c92
--- /dev/null
+++ b/app/src/middleware.ts
@@ -0,0 +1,14 @@
+import NextAuth from "next-auth";
+import authConfig from "./auth.config";
+
+export const { auth } = NextAuth(authConfig);
+
+export default auth((request) => {
+ if (request.auth === null && !request.nextUrl.pathname.endsWith('/')) {
+ return Response.redirect(new URL('/', request.url));
+ }
+});
+
+export const config = {
+ matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico).*)"],
+};
From f42dddb28c47f916e00c65de67da009634f74ad1 Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Fri, 7 Jun 2024 12:18:48 +0200
Subject: [PATCH 05/13] feat: use unstable_cache with server action
---
.task/checksum/npm-install | 2 +-
.../migrations/20240606161446/migration.sql | 9 --------
app/src/actions/message.ts | 4 +++-
app/src/app/api/messages/route.ts | 11 ----------
app/src/components/header.tsx | 22 ++++++++++++++-----
app/src/middleware.ts | 2 +-
app/src/repositories/MessageRepository.ts | 8 +++----
app/src/resolvers/message/getAllMessages.ts | 21 ++++++++----------
8 files changed, 35 insertions(+), 44 deletions(-)
delete mode 100644 app/src/app/api/messages/route.ts
diff --git a/.task/checksum/npm-install b/.task/checksum/npm-install
index 9572991..48f4b1b 100644
--- a/.task/checksum/npm-install
+++ b/.task/checksum/npm-install
@@ -1 +1 @@
-a37bf263bb7a27ec2c9ccb37408b9bdb
+14ac3c3c6846f5e11a3117756346d349
diff --git a/app/prisma/migrations/20240606161446/migration.sql b/app/prisma/migrations/20240606161446/migration.sql
index 55c6b07..751ae71 100644
--- a/app/prisma/migrations/20240606161446/migration.sql
+++ b/app/prisma/migrations/20240606161446/migration.sql
@@ -1,12 +1,3 @@
--- CreateTable
-CREATE TABLE "Message" (
- "id" SERIAL NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "content" TEXT NOT NULL,
-
- CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
-);
-
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
diff --git a/app/src/actions/message.ts b/app/src/actions/message.ts
index 8acad14..7ac5e05 100644
--- a/app/src/actions/message.ts
+++ b/app/src/actions/message.ts
@@ -11,7 +11,9 @@ export async function createMessage (formData: FormData) {
},
});
- MessageRepository.create(data);
+ const message = await MessageRepository.create(data);
revalidateTag('messages');
+
+ return message;
}
diff --git a/app/src/app/api/messages/route.ts b/app/src/app/api/messages/route.ts
deleted file mode 100644
index c1df620..0000000
--- a/app/src/app/api/messages/route.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-"use server";
-
-import MessageRepository from '@/repositories/MessageRepository';
-
-export type MessageIndexResponse = ReturnType;
-
-export async function GET() {
- const messages = await MessageRepository.getAll();
-
- return Response.json(messages);
-}
diff --git a/app/src/components/header.tsx b/app/src/components/header.tsx
index fc38b3f..3873a0d 100644
--- a/app/src/components/header.tsx
+++ b/app/src/components/header.tsx
@@ -5,12 +5,21 @@ import { auth } from '@/auth';
import AuthButton from '@/components/AuthButton';
import Icon from '@/components/Icon';
import Image from 'next/image';
+import Link from 'next/link';
export default async function Header() {
const session = await auth();
return (
-
+
);
}
diff --git a/app/src/middleware.ts b/app/src/middleware.ts
index 1876c92..02ee96f 100644
--- a/app/src/middleware.ts
+++ b/app/src/middleware.ts
@@ -10,5 +10,5 @@ export default auth((request) => {
});
export const config = {
- matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico).*)"],
+ matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
diff --git a/app/src/repositories/MessageRepository.ts b/app/src/repositories/MessageRepository.ts
index 7a53517..5087951 100644
--- a/app/src/repositories/MessageRepository.ts
+++ b/app/src/repositories/MessageRepository.ts
@@ -2,10 +2,10 @@ import { prismaDisconnect } from '@/decorator/prismaDisconnect';
import client from '@/prisma-client';
import { Prisma } from '@prisma/client';
-class MessageRepository {
+class Repository {
@prismaDisconnect()
public create(data: Prisma.MessageCreateInput) {
- client.message.create({ data });
+ return client.message.create({ data });
}
@prismaDisconnect()
@@ -20,6 +20,6 @@ class MessageRepository {
}
}
-const repository = new MessageRepository();
+const MessageRepository = new Repository();
-export default repository;
+export default MessageRepository;
diff --git a/app/src/resolvers/message/getAllMessages.ts b/app/src/resolvers/message/getAllMessages.ts
index 338428c..b72b96d 100644
--- a/app/src/resolvers/message/getAllMessages.ts
+++ b/app/src/resolvers/message/getAllMessages.ts
@@ -1,17 +1,14 @@
"use server";
-import { MessageIndexResponse } from '@/app/api/messages/route';
-import { handleJsonResponse } from '@/utils/request';
+import MessageRepository from '@/repositories/MessageRepository';
+import { unstable_cache } from 'next/cache';
-export default async function getAllMessages() {
- const response = await fetch(
- `${process.env.BASE_URL}/api/messages`,
- {
- next: {
- tags: ['messages'],
- }
- }
- );
+const getAll = unstable_cache(
+ () => MessageRepository.getAll(),
+ ['message', 'getAll'],
+ { tags: ['messages'] }
+);
- return handleJsonResponse(response, 'An error occurred while fetching messages. Please try again later.');
+export default async function getAllMessages() {
+ return getAll();
}
From 4f2a39075da85bca0f490a3d40ce6e5632b74ee1 Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Fri, 7 Jun 2024 14:14:43 +0200
Subject: [PATCH 06/13] feat: add knp logo + reset form on submit
---
app/src/app/messages/page.tsx | 7 ++--
app/src/assets/logo-knp-white.svg | 61 +++++++++++++++++++++++++++++++
app/src/components/Form.tsx | 23 ++++++++++++
app/src/components/header.tsx | 16 ++++++--
app/tailwind.config.ts | 3 ++
5 files changed, 103 insertions(+), 7 deletions(-)
create mode 100644 app/src/assets/logo-knp-white.svg
create mode 100644 app/src/components/Form.tsx
diff --git a/app/src/app/messages/page.tsx b/app/src/app/messages/page.tsx
index 1f34cc8..7c4a965 100644
--- a/app/src/app/messages/page.tsx
+++ b/app/src/app/messages/page.tsx
@@ -1,6 +1,7 @@
"use server";
import { createMessage } from '@/actions/message';
+import Form from '@/components/Form';
import getAllMessages from '@/resolvers/message/getAllMessages';
import Link from 'next/link';
@@ -9,10 +10,10 @@ export default async function MessagesPage() {
return (
-
+
+
{messages.map(message => (
diff --git a/app/src/assets/logo-knp-white.svg b/app/src/assets/logo-knp-white.svg
new file mode 100644
index 0000000..8198b06
--- /dev/null
+++ b/app/src/assets/logo-knp-white.svg
@@ -0,0 +1,61 @@
+
+
+
diff --git a/app/src/components/Form.tsx b/app/src/components/Form.tsx
new file mode 100644
index 0000000..5aae0cd
--- /dev/null
+++ b/app/src/components/Form.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import { HTMLProps, useRef } from 'react';
+
+type Props = Omit, 'action'> & {
+ action: (data: FormData) => Promise;
+};
+
+export default function Form({ action, children, ...props }: Props) {
+ const ref = useRef(null);
+ return (
+
+ );
+}
diff --git a/app/src/components/header.tsx b/app/src/components/header.tsx
index 3873a0d..74ee0ba 100644
--- a/app/src/components/header.tsx
+++ b/app/src/components/header.tsx
@@ -11,11 +11,19 @@ export default async function Header() {
const session = await auth();
return (
-
+
@@ -40,9 +48,9 @@ export default async function Header() {
className="rounded-full"
/>
) : (
-
+
{session.user.name?.charAt(0)}
-
+
)
}
diff --git a/app/tailwind.config.ts b/app/tailwind.config.ts
index e9a0944..22a2262 100644
--- a/app/tailwind.config.ts
+++ b/app/tailwind.config.ts
@@ -13,6 +13,9 @@ const config: Config = {
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
+ colors: {
+ primary: '#FF2F2D'
+ }
},
},
plugins: [],
From 69f4046b039ade984a34b9691dba142c6f4ea0cb Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Fri, 7 Jun 2024 15:21:01 +0200
Subject: [PATCH 07/13] feat: remove returned value in server action create
message
---
app/src/actions/message.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/app/src/actions/message.ts b/app/src/actions/message.ts
index 7ac5e05..6535d9b 100644
--- a/app/src/actions/message.ts
+++ b/app/src/actions/message.ts
@@ -11,9 +11,7 @@ export async function createMessage (formData: FormData) {
},
});
- const message = await MessageRepository.create(data);
+ await MessageRepository.create(data);
revalidateTag('messages');
-
- return message;
}
From 4fec7a358324d366b2be1a4b1a31b9ea7534465c Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Fri, 7 Jun 2024 16:37:33 +0200
Subject: [PATCH 08/13] chore: clean up
---
app/src/app/messages/error.tsx | 4 ++--
app/src/app/page.tsx | 4 +++-
app/src/utils/request.ts | 19 -------------------
3 files changed, 5 insertions(+), 22 deletions(-)
delete mode 100644 app/src/utils/request.ts
diff --git a/app/src/app/messages/error.tsx b/app/src/app/messages/error.tsx
index 5e72ff0..7a57ae7 100644
--- a/app/src/app/messages/error.tsx
+++ b/app/src/app/messages/error.tsx
@@ -4,8 +4,8 @@ export default function MessagesErrorHandler({
error,
reset
}: {
- error: Error & { digest?: string }
- reset: () => void
+ error: Error & { digest?: string };
+ reset: () => void;
}) {
return (
diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx
index 219c3ec..8c43026 100644
--- a/app/src/app/page.tsx
+++ b/app/src/app/page.tsx
@@ -1,4 +1,6 @@
-export default function Home() {
+"use server";
+
+export default async function Home() {
return (
Home
diff --git a/app/src/utils/request.ts b/app/src/utils/request.ts
deleted file mode 100644
index f46aa8b..0000000
--- a/app/src/utils/request.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export async function handleJsonResponse(
- response: Response,
- unknownErrorMessageNotHandleByTheApi: string = 'An unknown error occurred.'
-): Promise {
- // The original variable name is too long, so I changed it to a shorter one.
- const uEMNHBTA = unknownErrorMessageNotHandleByTheApi;
-
- if (!response.ok) {
- const data = await response.json();
-
- if ('error' in data) {
- throw new Error(data.error);
- }
-
- throw new Error(uEMNHBTA);
- }
-
- return response.json() as Promise;
-}
From 94d78b3977c63773a1a03b6ab48f38ee36c1cbc2 Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Fri, 7 Jun 2024 16:48:35 +0200
Subject: [PATCH 09/13] fix: issue with images imported as module
---
app/src/components/header.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/components/header.tsx b/app/src/components/header.tsx
index 74ee0ba..5f2adde 100644
--- a/app/src/components/header.tsx
+++ b/app/src/components/header.tsx
@@ -6,6 +6,7 @@ import AuthButton from '@/components/AuthButton';
import Icon from '@/components/Icon';
import Image from 'next/image';
import Link from 'next/link';
+import LogoKnpWhite from '@/assets/logo-knp-white.svg';
export default async function Header() {
const session = await auth();
@@ -17,7 +18,7 @@ export default async function Header() {
From 87e5cd05f1072250320795c6d7c4ec4be06ab8dd Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Fri, 7 Jun 2024 16:49:45 +0200
Subject: [PATCH 10/13] chore: remove .task directory
---
.gitignore | 3 +++
.task/checksum/npm-install | 1 -
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 .gitignore
delete mode 100644 .task/checksum/npm-install
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aae9732
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Taskfile
+
+/.task
diff --git a/.task/checksum/npm-install b/.task/checksum/npm-install
deleted file mode 100644
index 48f4b1b..0000000
--- a/.task/checksum/npm-install
+++ /dev/null
@@ -1 +0,0 @@
-14ac3c3c6846f5e11a3117756346d349
From ebad19afa649e434b7ba0d5159d19eec7947aa02 Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Fri, 7 Jun 2024 17:23:16 +0200
Subject: [PATCH 11/13] chore: enhance type safety in middleware
---
app/src/middleware.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/middleware.ts b/app/src/middleware.ts
index 02ee96f..beb95d6 100644
--- a/app/src/middleware.ts
+++ b/app/src/middleware.ts
@@ -1,5 +1,6 @@
import NextAuth from "next-auth";
import authConfig from "./auth.config";
+import { MiddlewareConfig } from 'next/server';
export const { auth } = NextAuth(authConfig);
@@ -11,4 +12,4 @@ export default auth((request) => {
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
-};
+} satisfies MiddlewareConfig;
From 81856861bfc33aa14fcf9c73f61dcfc89d582d8c Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Sun, 9 Jun 2024 20:03:19 +0200
Subject: [PATCH 12/13] fix: use builder target for testing purpose to generate
---
.github/workflows/test.yaml | 28 +++++++++++++------
app/.env.dist | 6 +++-
app/Dockerfile | 28 +++++++------------
app/src/components/{header.tsx => Header.tsx} | 0
app/src/middleware.ts | 4 +--
docker-compose.test.yaml | 2 ++
docker-compose.yaml | 2 --
7 files changed, 38 insertions(+), 32 deletions(-)
rename app/src/components/{header.tsx => Header.tsx} (100%)
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 5e118e7..404412c 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -3,21 +3,31 @@ on:
- push
jobs:
- test:
+ ci-cd:
+ name: CI/CD
+
runs-on: ubuntu-latest
+
env:
COMPOSE_FILE: docker-compose.test.yaml
- strategy:
- fail-fast: false
- matrix:
- cmd: [lint, type-check]
-
steps:
+ - name: Install Task
+ uses: arduino/setup-task@v1
+ with:
+ version: 3.x
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+
- uses: actions/checkout@v4
+ - name: Initialize environment
+ run: task init-env
+
- name: Build images
- run: docker compose build app
+ run: task docker-build
+
+ - name: lint
+ run: docker compose run --no-deps --rm app npm run lint
- - name: ${{ matrix.cmd }}
- run: docker compose run --no-deps --rm app npm run ${{ matrix.cmd }}
+ - name: Types checking
+ run: docker compose run --no-deps --rm app npm run type-check
diff --git a/app/.env.dist b/app/.env.dist
index 988617d..5b1ec2a 100644
--- a/app/.env.dist
+++ b/app/.env.dist
@@ -1,4 +1,8 @@
-BASE_URL=http://localhost:3000
+BASE_URL="http://localhost:3000"
+
+## Prisma
+
+DATABASE_URL="postgresql://postgres:password@postgres:5432/app?schema=public"
## Auth JS
diff --git a/app/Dockerfile b/app/Dockerfile
index 50c5621..15194e6 100644
--- a/app/Dockerfile
+++ b/app/Dockerfile
@@ -8,37 +8,23 @@ COPY package.json package-lock.json ./
RUN corepack enable npm && corepack install
-FROM npm AS deps
-
-ENV NODE_ENV=production
-
RUN npm ci
-FROM npm AS dev-deps
+FROM npm AS development
ENV NODE_ENV=development
-RUN npm ci
-
-FROM dev-deps AS test
-
-ENV NODE_ENV=test
-
-COPY . .
-
-RUN npx prisma generate
-
-FROM npm AS development
-
VOLUME [ "/opt/app/.next" ]
CMD ["npm", "run", "dev"]
FROM npm AS builder
+ENV NODE_ENV=production
+
COPY . .
-COPY --from=dev-deps /opt/app/node_modules /opt/app/node_modules
+COPY --from=npm /opt/app/node_modules /opt/app/node_modules
RUN npx prisma generate
@@ -48,6 +34,12 @@ RUN npm run build \
&& cp -r .next/static .next/standalone/.next/ \
&& cp -r public .next/standalone/
+FROM builder AS test
+
+ENV NODE_ENV=test
+
+COPY . .
+
FROM base AS release
ENV NODE_ENV=production
diff --git a/app/src/components/header.tsx b/app/src/components/Header.tsx
similarity index 100%
rename from app/src/components/header.tsx
rename to app/src/components/Header.tsx
diff --git a/app/src/middleware.ts b/app/src/middleware.ts
index beb95d6..077e9d2 100644
--- a/app/src/middleware.ts
+++ b/app/src/middleware.ts
@@ -10,6 +10,6 @@ export default auth((request) => {
}
});
-export const config = {
+export const config: MiddlewareConfig = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
-} satisfies MiddlewareConfig;
+};
diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml
index 764aa2d..9b799e4 100644
--- a/docker-compose.test.yaml
+++ b/docker-compose.test.yaml
@@ -3,3 +3,5 @@ services:
build:
context: app
target: test
+ env_file:
+ - app/.env.local
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 486538b..0269ede 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -4,8 +4,6 @@ services:
build:
context: app
target: development
- environment:
- - DATABASE_URL=postgresql://postgres:password@postgres:5432/app?schema=public
ports:
- '3000:3000'
volumes:
From b406e0482856dfa432410d7b96a9af513601af9e Mon Sep 17 00:00:00 2001
From: clementvtrd <84911237+clementvtrd@users.noreply.github.com>
Date: Sun, 9 Jun 2024 23:26:19 +0200
Subject: [PATCH 13/13] chore: rename ci cd workflow
---
.github/workflows/{test.yaml => ci-cd.yaml} | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
rename .github/workflows/{test.yaml => ci-cd.yaml} (89%)
diff --git a/.github/workflows/test.yaml b/.github/workflows/ci-cd.yaml
similarity index 89%
rename from .github/workflows/test.yaml
rename to .github/workflows/ci-cd.yaml
index 404412c..5171ec3 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/ci-cd.yaml
@@ -1,6 +1,10 @@
---
on:
- - push
+ push:
+ branches-ignore:
+ - main
+ tags-ignore:
+ - v*
jobs:
ci-cd: