From 7f2cd163aab164db91097ae6bfffe5203d83ff19 Mon Sep 17 00:00:00 2001 From: Josh Daniel Date: Sat, 3 Aug 2024 10:24:43 +0800 Subject: [PATCH 01/21] chore(social): setup deps --- apps/social/package.json | 32 +++-- pnpm-lock.yaml | 259 +++++++-------------------------------- 2 files changed, 60 insertions(+), 231 deletions(-) diff --git a/apps/social/package.json b/apps/social/package.json index 54554677..185ec6e8 100644 --- a/apps/social/package.json +++ b/apps/social/package.json @@ -11,40 +11,46 @@ "lint": "next lint" }, "dependencies": { - "@graphql-yoga/plugin-apq": "^3.3.0", - "@graphql-yoga/plugin-csrf-prevention": "^3.3.0", - "@graphql-yoga/plugin-disable-introspection": "^2.3.0", + "@fingerprintjs/botd": "^1.9.1", + "@graphql-yoga/plugin-csrf-prevention": "^3.6.2", + "@graphql-yoga/plugin-disable-introspection": "^2.6.2", + "@graphql-yoga/plugin-persisted-operations": "^3.6.2", + "@graphql-yoga/plugin-response-cache": "^3.8.2", "@umamin/db": "workspace:*", "@umamin/gql": "workspace:*", "@umamin/ui": "workspace:*", "@lucia-auth/adapter-drizzle": "^1.0.7", - "@urql/core": "^5.0.3", - "@urql/exchange-graphcache": "^7.0.2", + "@urql/core": "^5.0.5", + "@urql/exchange-graphcache": "^7.1.1", + "@urql/exchange-persisted": "^4.3.0", "@urql/next": "^1.1.1", "arctic": "^1.8.1", "geist": "^1.3.0", - "gql.tada": "^1.7.5", - "graphql": "^16.8.1", - "graphql-yoga": "^5.3.1", + "gql.tada": "^1.8.5", + "graphql": "^16.9.0", + "graphql-yoga": "^5.6.2", "nanoid": "^5.0.7", "oslo": "^1.2.0", "urql": "^4.1.0", "lucia": "^3.2.0", - "next": "14.2.3", + "next": "14.2.5", "nextjs-toploader": "^1.6.12", "react": "^18", "react-dom": "^18", - "sonner": "^1.4.41" + "sonner": "^1.5.0" }, "devDependencies": { + "@0no-co/graphqlsp": "^1.12.12", "@umamin/eslint-config": "workspace:*", "@umamin/tsconfig": "workspace:*", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", "eslint": "^8", - "eslint-config-next": "14.2.3", - "postcss": "^8", - "tailwindcss": "^3.4.1" + "eslint-config-next": "14.2.5", + "postcss": "^8.4.40", + "tailwindcss": "^3.4.7", + "typescript": "^5.5.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf41306d..988ef259 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,15 +26,21 @@ importers: apps/social: dependencies: - '@graphql-yoga/plugin-apq': - specifier: ^3.3.0 - version: 3.6.2(@graphql-tools/utils@10.3.2(graphql@16.9.0))(graphql-yoga@5.6.2(graphql@16.9.0)) + '@fingerprintjs/botd': + specifier: ^1.9.1 + version: 1.9.1 '@graphql-yoga/plugin-csrf-prevention': - specifier: ^3.3.0 + specifier: ^3.6.2 version: 3.6.2(graphql-yoga@5.6.2(graphql@16.9.0)) '@graphql-yoga/plugin-disable-introspection': - specifier: ^2.3.0 + specifier: ^2.6.2 version: 2.6.2(graphql-yoga@5.6.2(graphql@16.9.0))(graphql@16.9.0) + '@graphql-yoga/plugin-persisted-operations': + specifier: ^3.6.2 + version: 3.6.2(@graphql-tools/utils@10.3.2(graphql@16.9.0))(graphql-yoga@5.6.2(graphql@16.9.0))(graphql@16.9.0) + '@graphql-yoga/plugin-response-cache': + specifier: ^3.8.2 + version: 3.8.2(@envelop/core@5.0.1)(graphql-yoga@5.6.2(graphql@16.9.0))(graphql@16.9.0) '@lucia-auth/adapter-drizzle': specifier: ^1.0.7 version: 1.0.7(lucia@3.2.0) @@ -48,28 +54,31 @@ importers: specifier: workspace:* version: link:../../packages/ui '@urql/core': - specifier: ^5.0.3 + specifier: ^5.0.5 version: 5.0.5(graphql@16.9.0) '@urql/exchange-graphcache': - specifier: ^7.0.2 + specifier: ^7.1.1 version: 7.1.2(@urql/core@5.0.5(graphql@16.9.0))(graphql@16.9.0) + '@urql/exchange-persisted': + specifier: ^4.3.0 + version: 4.3.0(@urql/core@5.0.5(graphql@16.9.0)) '@urql/next': specifier: ^1.1.1 - version: 1.1.1(next@14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1)) + version: 1.1.1(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1)) arctic: specifier: ^1.8.1 version: 1.9.2 geist: specifier: ^1.3.0 - version: 1.3.1(next@14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.3.1(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gql.tada: - specifier: ^1.7.5 + specifier: ^1.8.5 version: 1.8.5(graphql@16.9.0)(typescript@5.5.4) graphql: - specifier: ^16.8.1 + specifier: ^16.9.0 version: 16.9.0 graphql-yoga: - specifier: ^5.3.1 + specifier: ^5.6.2 version: 5.6.2(graphql@16.9.0) lucia: specifier: ^3.2.0 @@ -78,11 +87,11 @@ importers: specifier: ^5.0.7 version: 5.0.7 next: - specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.2.5 + version: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextjs-toploader: specifier: ^1.6.12 - version: 1.6.12(next@14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.6.12(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) oslo: specifier: ^1.2.0 version: 1.2.1 @@ -93,12 +102,15 @@ importers: specifier: ^18 version: 18.3.1(react@18.3.1) sonner: - specifier: ^1.4.41 + specifier: ^1.5.0 version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) urql: specifier: ^4.1.0 version: 4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1) devDependencies: + '@0no-co/graphqlsp': + specifier: ^1.12.12 + version: 1.12.12(graphql@16.9.0)(typescript@5.5.4) '@types/node': specifier: ^20 version: 20.14.13 @@ -114,18 +126,24 @@ importers: '@umamin/tsconfig': specifier: workspace:* version: link:../../packages/tsconfig + autoprefixer: + specifier: ^10.0.1 + version: 10.4.19(postcss@8.4.40) eslint: specifier: ^8 version: 8.57.0 eslint-config-next: - specifier: 14.2.3 - version: 14.2.3(eslint@8.57.0)(typescript@5.5.4) + specifier: 14.2.5 + version: 14.2.5(eslint@8.57.0)(typescript@5.5.4) postcss: - specifier: ^8 + specifier: ^8.4.40 version: 8.4.40 tailwindcss: - specifier: ^3.4.1 + specifier: ^3.4.7 version: 3.4.7 + typescript: + specifier: ^5.5.4 + version: 5.5.4 apps/www: dependencies: @@ -1249,13 +1267,6 @@ packages: resolution: {integrity: sha512-Mg8psdkAp+YTG1OGmvU+xa6xpsAmSir0hhr3yFYPyLNwzUj95DdIwsMpKadDj9xDpYgJcH3Hp/4JMal9DhQimA==} engines: {node: '>=18.0.0'} - '@graphql-yoga/plugin-apq@3.6.2': - resolution: {integrity: sha512-fJ44a3ol/5Mc54aKzWqm4yHDO1tfF9SSnsblOqaHwBsjSXtEWt0eXUXID9I9jOlvi+IDi2TOBYixHBnW8kAPOA==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@graphql-tools/utils': ^10.0.0 - graphql-yoga: ^5.6.2 - '@graphql-yoga/plugin-csrf-prevention@3.6.2': resolution: {integrity: sha512-frxWfhmhC+DMji2YNddTX03NBgjO1TvT1ZQlcmSgvI3eA+/hsf5Dt0CytjvopuLPP/4Jc0HKf27LVCXIXcAPCQ==} engines: {node: '>=18.0.0'} @@ -1428,15 +1439,9 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - '@next/env@14.2.3': - resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} - '@next/env@14.2.5': resolution: {integrity: sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==} - '@next/eslint-plugin-next@14.2.3': - resolution: {integrity: sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==} - '@next/eslint-plugin-next@14.2.5': resolution: {integrity: sha512-LY3btOpPh+OTIpviNojDpUdIbHW9j0JBYBjsIp8IxtDFfYFyORvw3yNq6N231FVqQA7n7lwaf7xHbVJlA1ED7g==} @@ -1451,108 +1456,54 @@ packages: '@mdx-js/react': optional: true - '@next/swc-darwin-arm64@14.2.3': - resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - '@next/swc-darwin-arm64@14.2.5': resolution: {integrity: sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.3': - resolution: {integrity: sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - '@next/swc-darwin-x64@14.2.5': resolution: {integrity: sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.3': - resolution: {integrity: sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - '@next/swc-linux-arm64-gnu@14.2.5': resolution: {integrity: sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.3': - resolution: {integrity: sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - '@next/swc-linux-arm64-musl@14.2.5': resolution: {integrity: sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.3': - resolution: {integrity: sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@next/swc-linux-x64-gnu@14.2.5': resolution: {integrity: sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.3': - resolution: {integrity: sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@next/swc-linux-x64-musl@14.2.5': resolution: {integrity: sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.3': - resolution: {integrity: sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - '@next/swc-win32-arm64-msvc@14.2.5': resolution: {integrity: sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.3': - resolution: {integrity: sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - '@next/swc-win32-ia32-msvc@14.2.5': resolution: {integrity: sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.3': - resolution: {integrity: sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@next/swc-win32-x64-msvc@14.2.5': resolution: {integrity: sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==} engines: {node: '>= 10'} @@ -3585,15 +3536,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@14.2.3: - resolution: {integrity: sha512-ZkNztm3Q7hjqvB1rRlOX8P9E/cXRL9ajRcs8jufEtwMfTVYRqnmtnaSu57QqHyBlovMuiB8LEzfLBkh5RYV6Fg==} - peerDependencies: - eslint: ^7.23.0 || ^8.0.0 - typescript: '>=3.3.1' - peerDependenciesMeta: - typescript: - optional: true - eslint-config-next@14.2.5: resolution: {integrity: sha512-zogs9zlOiZ7ka+wgUnmcM0KBEDjo4Jis7kxN1jvC0N4wynQ2MIx/KBkg4mVF63J5EK4W0QMCn7xO3vNisjaAoA==} peerDependencies: @@ -4681,24 +4623,6 @@ packages: react: ^16.8 || ^17 || ^18 react-dom: ^16.8 || ^17 || ^18 - next@14.2.3: - resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - sass: - optional: true - next@14.2.5: resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==} engines: {node: '>=18.17.0'} @@ -6552,11 +6476,6 @@ snapshots: dependencies: tslib: 2.6.3 - '@graphql-yoga/plugin-apq@3.6.2(@graphql-tools/utils@10.3.2(graphql@16.9.0))(graphql-yoga@5.6.2(graphql@16.9.0))': - dependencies: - '@graphql-tools/utils': 10.3.2(graphql@16.9.0) - graphql-yoga: 5.6.2(graphql@16.9.0) - '@graphql-yoga/plugin-csrf-prevention@3.6.2(graphql-yoga@5.6.2(graphql@16.9.0))': dependencies: graphql-yoga: 5.6.2(graphql@16.9.0) @@ -6773,14 +6692,8 @@ snapshots: '@neon-rs/load@0.0.4': {} - '@next/env@14.2.3': {} - '@next/env@14.2.5': {} - '@next/eslint-plugin-next@14.2.3': - dependencies: - glob: 10.3.10 - '@next/eslint-plugin-next@14.2.5': dependencies: glob: 10.3.10 @@ -6792,57 +6705,30 @@ snapshots: '@mdx-js/loader': 3.0.1(webpack@5.93.0) '@mdx-js/react': 3.0.1(@types/react@18.3.3)(react@18.3.1) - '@next/swc-darwin-arm64@14.2.3': - optional: true - '@next/swc-darwin-arm64@14.2.5': optional: true - '@next/swc-darwin-x64@14.2.3': - optional: true - '@next/swc-darwin-x64@14.2.5': optional: true - '@next/swc-linux-arm64-gnu@14.2.3': - optional: true - '@next/swc-linux-arm64-gnu@14.2.5': optional: true - '@next/swc-linux-arm64-musl@14.2.3': - optional: true - '@next/swc-linux-arm64-musl@14.2.5': optional: true - '@next/swc-linux-x64-gnu@14.2.3': - optional: true - '@next/swc-linux-x64-gnu@14.2.5': optional: true - '@next/swc-linux-x64-musl@14.2.3': - optional: true - '@next/swc-linux-x64-musl@14.2.5': optional: true - '@next/swc-win32-arm64-msvc@14.2.3': - optional: true - '@next/swc-win32-arm64-msvc@14.2.5': optional: true - '@next/swc-win32-ia32-msvc@14.2.3': - optional: true - '@next/swc-win32-ia32-msvc@14.2.5': optional: true - '@next/swc-win32-x64-msvc@14.2.3': - optional: true - '@next/swc-win32-x64-msvc@14.2.5': optional: true @@ -8327,12 +8213,6 @@ snapshots: '@urql/core': 5.0.5(graphql@16.9.0) wonka: 6.3.4 - '@urql/next@1.1.1(next@14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1))': - dependencies: - next: 14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - urql: 4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1) - '@urql/next@1.1.1(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1))': dependencies: next: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -9097,24 +8977,6 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@14.2.3(eslint@8.57.0)(typescript@5.5.4): - dependencies: - '@next/eslint-plugin-next': 14.2.3 - '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) - eslint: 8.57.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) - eslint-plugin-react: 7.35.0(eslint@8.57.0) - eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - supports-color - eslint-config-next@14.2.5(eslint@8.57.0)(typescript@5.5.4): dependencies: '@next/eslint-plugin-next': 14.2.5 @@ -9123,7 +8985,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -9177,7 +9039,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -9252,7 +9114,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -9619,10 +9481,6 @@ snapshots: functions-have-names@1.2.3: {} - geist@1.3.1(next@14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): - dependencies: - next: 14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - geist@1.3.1(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: next: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10699,33 +10557,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@next/env': 14.2.3 - '@swc/helpers': 0.5.5 - busboy: 1.6.0 - caniuse-lite: 1.0.30001646 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(react@18.3.1) - optionalDependencies: - '@next/swc-darwin-arm64': 14.2.3 - '@next/swc-darwin-x64': 14.2.3 - '@next/swc-linux-arm64-gnu': 14.2.3 - '@next/swc-linux-arm64-musl': 14.2.3 - '@next/swc-linux-x64-gnu': 14.2.3 - '@next/swc-linux-x64-musl': 14.2.3 - '@next/swc-win32-arm64-msvc': 14.2.3 - '@next/swc-win32-ia32-msvc': 14.2.3 - '@next/swc-win32-x64-msvc': 14.2.3 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.45.3 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.5 @@ -10753,14 +10584,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextjs-toploader@1.6.12(next@14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - next: 14.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nprogress: 0.2.0 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - nextjs-toploader@1.6.12(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: next: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) From f349f13a2ef57fc41ccf8de44d5a0e040416d964 Mon Sep 17 00:00:00 2001 From: Josh Daniel Date: Sat, 3 Aug 2024 10:46:02 +0800 Subject: [PATCH 02/21] feat(social): setup graphql server --- apps/social/package.json | 3 +- apps/social/postcss.config.mjs | 9 +-- apps/social/src/app/api/graphql/route.ts | 56 ++++++++++++++++ apps/social/src/lib/auth.ts | 84 ++++++++++++++++++++++++ apps/social/tsconfig.json | 6 +- pnpm-lock.yaml | 9 ++- 6 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 apps/social/src/app/api/graphql/route.ts create mode 100644 apps/social/src/lib/auth.ts diff --git a/apps/social/package.json b/apps/social/package.json index 185ec6e8..874e4191 100644 --- a/apps/social/package.json +++ b/apps/social/package.json @@ -37,7 +37,8 @@ "nextjs-toploader": "^1.6.12", "react": "^18", "react-dom": "^18", - "sonner": "^1.5.0" + "sonner": "^1.5.0", + "@whatwg-node/server": "^0.9.46" }, "devDependencies": { "@0no-co/graphqlsp": "^1.12.12", diff --git a/apps/social/postcss.config.mjs b/apps/social/postcss.config.mjs index 1a69fd2a..a4917b21 100644 --- a/apps/social/postcss.config.mjs +++ b/apps/social/postcss.config.mjs @@ -1,8 +1 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; +module.exports = require("@umamin/ui/postcss.config"); diff --git a/apps/social/src/app/api/graphql/route.ts b/apps/social/src/app/api/graphql/route.ts new file mode 100644 index 00000000..9612093b --- /dev/null +++ b/apps/social/src/app/api/graphql/route.ts @@ -0,0 +1,56 @@ +import { cookies } from "next/headers"; +import { createYoga } from "graphql-yoga"; +import { getSession, lucia } from "@/lib/auth"; +import { gqlSchema, initContextCache } from "@umamin/gql"; +import { useResponseCache } from "@graphql-yoga/plugin-response-cache"; +import { useCSRFPrevention } from "@graphql-yoga/plugin-csrf-prevention"; +import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection"; + +const { handleRequest } = createYoga({ + schema: gqlSchema, + context: async () => { + const { session } = await getSession(); + + return { + ...initContextCache(), + userId: session?.userId, + }; + }, + graphqlEndpoint: "/api/graphql", + graphiql: process.env.NODE_ENV === "development", + fetchAPI: { Response }, + cors: { + origin: + process.env.NODE_ENV === "production" + ? "https://social.umamin.link" + : "http://localhost:3000", + credentials: true, + methods: ["POST", "GET", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + }, + plugins: [ + useCSRFPrevention({ + requestHeaders: ["x-graphql-yoga-csrf"], + }), + useResponseCache({ + session: () => cookies().get(lucia.sessionCookieName)?.value, + invalidateViaMutation: false, + scopePerSchemaCoordinate: { + "Query.user": "PRIVATE", + }, + ttl: 30_000, + ttlPerSchemaCoordinate: { + "Query.userByUsername": 120_000, + }, + }), + useDisableIntrospection({ + isDisabled: () => process.env.NODE_ENV === "production", + }), + ], +}); + +export { + handleRequest as GET, + handleRequest as POST, + handleRequest as OPTIONS, +}; diff --git a/apps/social/src/lib/auth.ts b/apps/social/src/lib/auth.ts new file mode 100644 index 00000000..05f347e7 --- /dev/null +++ b/apps/social/src/lib/auth.ts @@ -0,0 +1,84 @@ +import { cache } from "react"; +import { Google } from "arctic"; +import { cookies } from "next/headers"; +import { Lucia, Session, User } from "lucia"; +import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle"; + +import { db } from "@umamin/db"; +import { + SelectUser, + user as userSchema, + session as sessionSchema, +} from "@umamin/db/schema/user"; + +export const google = new Google( + process.env.GOOGLE_CLIENT_ID!, + process.env.GOOGLE_CLIENT_SECRET!, + process.env.GOOGLE_REDIRECT_URI!, +); + +const adapter = new DrizzleSQLiteAdapter(db, sessionSchema, userSchema); + +export const lucia = new Lucia(adapter, { + sessionCookie: { + expires: false, + attributes: { + secure: process.env.NODE_ENV === "production", + }, + }, + getUserAttributes: (attributes) => { + return { + ...attributes, + }; + }, +}); + +export const getSession = cache( + async (): Promise< + { user: User; session: Session } | { user: null; session: null } + > => { + const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null; + + if (!sessionId) { + return { + user: null, + session: null, + }; + } + + const result = await lucia.validateSession(sessionId); + + try { + if (result.session && result.session.fresh) { + const sessionCookie = lucia.createSessionCookie(result.session.id); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + } + + if (!result.session) { + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + } + } catch (err) { + console.log(err); + } + return result; + }, +); + +declare module "lucia" { + // eslint-disable-next-line no-unused-vars + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes extends SelectUser {} diff --git a/apps/social/tsconfig.json b/apps/social/tsconfig.json index 835521f3..b0685457 100644 --- a/apps/social/tsconfig.json +++ b/apps/social/tsconfig.json @@ -7,8 +7,10 @@ }, { "name": "@0no-co/graphqlsp", - "schema": "http://localhost:3000/api/graphql", - "tadaOutputLocation": "./src/graphql-env.d.ts" + "schema": "./src/schema.graphql", + "tadaOutputLocation": "./src/graphql-env.d.ts", + "tadaPersistedLocation": "./src/persisted-operations.json", + "trackFieldUsage": false } ], "paths": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 988ef259..319712b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: '@urql/next': specifier: ^1.1.1 version: 1.1.1(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1)) + '@whatwg-node/server': + specifier: ^0.9.46 + version: 0.9.46 arctic: specifier: ^1.8.1 version: 1.9.2 @@ -8985,7 +8988,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -9039,7 +9042,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -9114,7 +9117,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 From a28f734db4b56eb653c50e3a58652d0b98f17712 Mon Sep 17 00:00:00 2001 From: Josh Daniel Date: Sun, 4 Aug 2024 16:29:55 +0800 Subject: [PATCH 03/21] feat(social): add auth pages --- apps/social/next.config.mjs | 10 +- apps/social/package.json | 19 +- ...{postcss.config.mjs => postcss.config.cjs} | 0 apps/social/src/actions.ts | 259 ++++++++++++++++++ .../login/components/form.tsx | 43 +++ .../login/components/login-button.tsx | 25 ++ .../login/google/callback/route.ts | 158 +++++++++++ .../(authentication)/login/google/route.ts | 31 +++ .../app/(authentication)/login/loading.tsx | 28 ++ .../src/app/(authentication)/login/page.tsx | 70 +++++ .../register/components/button.tsx | 25 ++ .../register/components/form.tsx | 71 +++++ .../app/(authentication)/register/loading.tsx | 44 +++ .../app/(authentication)/register/page.tsx | 77 ++++++ apps/social/src/app/layout.tsx | 60 +++- apps/social/src/app/page.tsx | 15 +- pnpm-lock.yaml | 20 +- 17 files changed, 936 insertions(+), 19 deletions(-) rename apps/social/{postcss.config.mjs => postcss.config.cjs} (100%) create mode 100644 apps/social/src/actions.ts create mode 100644 apps/social/src/app/(authentication)/login/components/form.tsx create mode 100644 apps/social/src/app/(authentication)/login/components/login-button.tsx create mode 100644 apps/social/src/app/(authentication)/login/google/callback/route.ts create mode 100644 apps/social/src/app/(authentication)/login/google/route.ts create mode 100644 apps/social/src/app/(authentication)/login/loading.tsx create mode 100644 apps/social/src/app/(authentication)/login/page.tsx create mode 100644 apps/social/src/app/(authentication)/register/components/button.tsx create mode 100644 apps/social/src/app/(authentication)/register/components/form.tsx create mode 100644 apps/social/src/app/(authentication)/register/loading.tsx create mode 100644 apps/social/src/app/(authentication)/register/page.tsx diff --git a/apps/social/next.config.mjs b/apps/social/next.config.mjs index dcecbf43..32f7991c 100644 --- a/apps/social/next.config.mjs +++ b/apps/social/next.config.mjs @@ -1,6 +1,14 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - transpilePackages: ["@umamin/ui", "@umamin/server"], + pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"], + experimental: { + serverComponentsExternalPackages: ["@node-rs/argon2"], + }, + compiler: { + removeConsole: process.env.NODE_ENV === "production", + }, + + transpilePackages: ["@umamin/ui", "@umamin/db", "@umamin/gql"], images: { remotePatterns: [ { diff --git a/apps/social/package.json b/apps/social/package.json index 874e4191..634e8263 100644 --- a/apps/social/package.json +++ b/apps/social/package.json @@ -16,37 +16,40 @@ "@graphql-yoga/plugin-disable-introspection": "^2.6.2", "@graphql-yoga/plugin-persisted-operations": "^3.6.2", "@graphql-yoga/plugin-response-cache": "^3.8.2", + "@lucia-auth/adapter-drizzle": "^1.0.7", + "@node-rs/argon2": "^1.8.3", "@umamin/db": "workspace:*", "@umamin/gql": "workspace:*", "@umamin/ui": "workspace:*", - "@lucia-auth/adapter-drizzle": "^1.0.7", "@urql/core": "^5.0.5", "@urql/exchange-graphcache": "^7.1.1", "@urql/exchange-persisted": "^4.3.0", "@urql/next": "^1.1.1", + "@whatwg-node/server": "^0.9.46", "arctic": "^1.8.1", - "geist": "^1.3.0", + "geist": "^1.3.1", "gql.tada": "^1.8.5", "graphql": "^16.9.0", "graphql-yoga": "^5.6.2", - "nanoid": "^5.0.7", - "oslo": "^1.2.0", - "urql": "^4.1.0", "lucia": "^3.2.0", + "lucide-react": "^0.424.0", + "nanoid": "^5.0.7", "next": "14.2.5", "nextjs-toploader": "^1.6.12", + "oslo": "^1.2.0", "react": "^18", "react-dom": "^18", "sonner": "^1.5.0", - "@whatwg-node/server": "^0.9.46" + "urql": "^4.1.0", + "zod": "^3.23.8" }, "devDependencies": { "@0no-co/graphqlsp": "^1.12.12", - "@umamin/eslint-config": "workspace:*", - "@umamin/tsconfig": "workspace:*", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@umamin/eslint-config": "workspace:*", + "@umamin/tsconfig": "workspace:*", "autoprefixer": "^10.0.1", "eslint": "^8", "eslint-config-next": "14.2.5", diff --git a/apps/social/postcss.config.mjs b/apps/social/postcss.config.cjs similarity index 100% rename from apps/social/postcss.config.mjs rename to apps/social/postcss.config.cjs diff --git a/apps/social/src/actions.ts b/apps/social/src/actions.ts new file mode 100644 index 00000000..2b9d9197 --- /dev/null +++ b/apps/social/src/actions.ts @@ -0,0 +1,259 @@ +"use server"; + +import { nanoid } from "nanoid"; +import { db, eq } from "@umamin/db"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { hash, verify } from "@node-rs/argon2"; +import { + user as userSchema, + account as accountSchema, +} from "@umamin/db/schema/user"; +import { note as noteSchema } from "@umamin/db/schema/note"; +import { message as messageSchema } from "@umamin/db/schema/message"; + +import { getSession, lucia } from "./lib/auth"; +import { z } from "zod"; + +export async function logout(): Promise { + const { session } = await getSession(); + + if (!session) { + throw new Error("Unauthorized"); + } + + await lucia.invalidateSession(session.id); + + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + + return redirect("/login"); +} + +const signupSchema = z + .object({ + username: z + .string() + .min(5, { + message: "Username must be at least 5 characters", + }) + .max(20, { + message: "Username must not exceed 20 characters", + }) + .refine((url) => /^[a-zA-Z0-9_-]+$/.test(url), { + message: "Username must be alphanumeric with no spaces", + }), + password: z + .string() + .min(5, { + message: "Password must be at least 5 characters", + }) + .max(255, { + message: "Password must not exceed 255 characters", + }), + confirmPassword: z.string(), + }) + .refine( + (values) => { + return values.password === values.confirmPassword; + }, + { + message: "Password does not match", + path: ["confirmPassword"], + }, + ); + +export async function signup(_: any, formData: FormData) { + const validatedFields = signupSchema.safeParse({ + username: formData.get("username"), + password: formData.get("password"), + confirmPassword: formData.get("confirmPassword"), + }); + + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + }; + } + + const passwordHash = await hash(validatedFields.data.password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + + const userId = nanoid(); + + try { + await db.insert(userSchema).values({ + id: userId, + username: validatedFields.data.username.toLowerCase(), + passwordHash, + }); + } catch (err: any) { + if (err.code === "SQLITE_CONSTRAINT") { + if (err.message.includes("user.username")) { + return { + errors: { + username: ["Username already taken"], + }, + }; + } + } + + throw new Error("Something went wrong"); + } + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + + return redirect("/"); +} + +export async function login(_: any, formData: FormData): Promise { + const username = formData.get("username"); + + if ( + typeof username !== "string" || + username.length < 5 || + username.length > 20 || + !/^[a-zA-Z0-9_-]+$/.test(username) + ) { + return { + error: "Incorrect username or password", + }; + } + + const password = formData.get("password"); + + if ( + typeof password !== "string" || + password.length < 5 || + password.length > 255 + ) { + return { + error: "Incorrect username or password", + }; + } + + const existingUser = await db.query.user.findFirst({ + where: eq(userSchema.username, username.toLowerCase()), + }); + + if (!existingUser || !existingUser.passwordHash) { + return { + error: "Incorrect username or password", + }; + } + + const validPassword = await verify(existingUser.passwordHash, password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + + if (!validPassword) { + return { + error: "Incorrect username or password", + }; + } + + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + + return redirect("/"); +} + +export async function updatePassword({ + currentPassword, + password, +}: { + currentPassword?: string; + password: string; +}): Promise { + const { user } = await getSession(); + + if (!user) { + throw new Error("Unauthorized"); + } + + if (currentPassword && user.passwordHash) { + const validPassword = await verify(user.passwordHash, currentPassword, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + + if (!validPassword) { + return { + error: "Incorrect password", + }; + } + } + + const passwordHash = await hash(password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + + await db + .update(userSchema) + .set({ passwordHash }) + .where(eq(userSchema.id, user.id)); + + return redirect("/settings"); +} + +export async function deleteAccount() { + const { user } = await getSession(); + + if (!user) { + throw new Error("Unauthorized"); + } + + try { + await db.batch([ + db.delete(messageSchema).where(eq(messageSchema.receiverId, user.id)), + db.delete(accountSchema).where(eq(accountSchema.userId, user.id)), + db.delete(noteSchema).where(eq(noteSchema.userId, user.id)), + db.delete(userSchema).where(eq(userSchema.id, user.id)), + ]); + + await lucia.invalidateSession(user.id); + + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + } catch (err) { + throw new Error("Failed to delete account"); + } + + return redirect("/login"); +} + +interface ActionResult { + error: string | null; +} diff --git a/apps/social/src/app/(authentication)/login/components/form.tsx b/apps/social/src/app/(authentication)/login/components/form.tsx new file mode 100644 index 00000000..3d0d937a --- /dev/null +++ b/apps/social/src/app/(authentication)/login/components/form.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { login } from "@/actions"; +import { useFormState } from "react-dom"; + +import { LoginButton } from "./login-button"; +import { Input } from "@umamin/ui/components/input"; +import { Label } from "@umamin/ui/components/label"; + +export function LoginForm() { + const [state, formAction] = useFormState(login, { error: "" }); + + return ( +
+
+ + +
+ +
+ + + {!!state?.error && ( +

{state.error}

+ )} +
+ + + + ); +} diff --git a/apps/social/src/app/(authentication)/login/components/login-button.tsx b/apps/social/src/app/(authentication)/login/components/login-button.tsx new file mode 100644 index 00000000..1ca484aa --- /dev/null +++ b/apps/social/src/app/(authentication)/login/components/login-button.tsx @@ -0,0 +1,25 @@ +"use client"; + +import Link from "next/link"; +import { Loader2 } from "lucide-react"; +import { useFormStatus } from "react-dom"; +import { Button } from "@umamin/ui/components/button"; + +export function LoginButton() { + const { pending } = useFormStatus(); + + return ( +
+ + + +
+ ); +} diff --git a/apps/social/src/app/(authentication)/login/google/callback/route.ts b/apps/social/src/app/(authentication)/login/google/callback/route.ts new file mode 100644 index 00000000..261b8560 --- /dev/null +++ b/apps/social/src/app/(authentication)/login/google/callback/route.ts @@ -0,0 +1,158 @@ +import { nanoid } from "nanoid"; +import { generateId } from "lucia"; +import { cookies } from "next/headers"; +import { db, and, eq } from "@umamin/db"; +import { OAuth2RequestError } from "arctic"; +import { + user as userSchema, + account as accountSchema, +} from "@umamin/db/schema/user"; + +import { getSession, google, lucia } from "@/lib/auth"; + +export async function GET(request: Request): Promise { + const url = new URL(request.url); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + + const storedState = cookies().get("google_oauth_state")?.value ?? null; + const storedCodeVerifier = cookies().get("code_verifier")?.value ?? null; + + if ( + !code || + !state || + !storedState || + !storedCodeVerifier || + state !== storedState + ) { + return new Response(null, { + status: 400, + }); + } + + try { + const tokens = await google.validateAuthorizationCode( + code, + storedCodeVerifier, + ); + + const googleUserResponse = await fetch( + "https://openidconnect.googleapis.com/v1/userinfo", + { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }, + ); + + const googleUser: GoogleUser = await googleUserResponse.json(); + + const { user } = await getSession(); + + const existingUser = await db.query.account.findFirst({ + where: and( + eq(accountSchema.providerId, "google"), + eq(accountSchema.providerUserId, googleUser.sub), + ), + }); + + if (user && existingUser) { + return new Response(null, { + status: 302, + headers: { + Location: "/settings?error=already_linked", + }, + }); + } else if (user) { + await db + .update(userSchema) + .set({ + imageUrl: googleUser.picture, + }) + .where(eq(userSchema.id, user.id)); + + await db.insert(accountSchema).values({ + providerId: "google", + providerUserId: googleUser.sub, + userId: user.id, + picture: googleUser.picture, + email: googleUser.email, + }); + + return new Response(null, { + status: 302, + headers: { + Location: "/settings", + }, + }); + } + + if (existingUser) { + const session = await lucia.createSession(existingUser.userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + + return new Response(null, { + status: 302, + headers: { + Location: "/login", + }, + }); + } + + const usernameId = generateId(5); + const userId = nanoid(); + + await db.insert(userSchema).values({ + id: userId, + imageUrl: googleUser.picture, + username: `umamin_${usernameId}`, + }); + + await db.insert(accountSchema).values({ + providerId: "google", + providerUserId: googleUser.sub, + userId, + picture: googleUser.picture, + email: googleUser.email, + }); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + + return new Response(null, { + status: 302, + headers: { + Location: "/login", + }, + }); + } catch (err: any) { + console.log(err); + if (err instanceof OAuth2RequestError) { + return new Response(null, { + status: 400, + }); + } + + return new Response(null, { + status: 500, + }); + } +} + +interface GoogleUser { + sub: string; + picture: string; + email: string; +} diff --git a/apps/social/src/app/(authentication)/login/google/route.ts b/apps/social/src/app/(authentication)/login/google/route.ts new file mode 100644 index 00000000..88f65bc7 --- /dev/null +++ b/apps/social/src/app/(authentication)/login/google/route.ts @@ -0,0 +1,31 @@ +import { generateState, generateCodeVerifier } from "arctic"; +import { cookies } from "next/headers"; +import { google } from "@/lib/auth"; + +export async function GET(): Promise { + const state = generateState(); + const codeVerifier = generateCodeVerifier(); + const url = await google.createAuthorizationURL(state, codeVerifier, { + scopes: ["profile", "email"], + }); + + url.searchParams.set("access_type", "offline"); + + cookies().set("google_oauth_state", state, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax", + }); + + cookies().set("code_verifier", codeVerifier, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax", + }); + + return Response.redirect(url); +} diff --git a/apps/social/src/app/(authentication)/login/loading.tsx b/apps/social/src/app/(authentication)/login/loading.tsx new file mode 100644 index 00000000..5ba9d85f --- /dev/null +++ b/apps/social/src/app/(authentication)/login/loading.tsx @@ -0,0 +1,28 @@ +import { Skeleton } from "@umamin/ui/components/skeleton"; + +export default function Loading() { + return ( +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+
+ ); +} diff --git a/apps/social/src/app/(authentication)/login/page.tsx b/apps/social/src/app/(authentication)/login/page.tsx new file mode 100644 index 00000000..36540fe9 --- /dev/null +++ b/apps/social/src/app/(authentication)/login/page.tsx @@ -0,0 +1,70 @@ +import Link from "next/link"; +import dynamic from "next/dynamic"; +import { getSession } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import { LoginForm } from "./components/form"; + +const BrowserWarning = dynamic( + () => import("@umamin/ui/components/browser-warning"), + { ssr: false } +); + +export const metadata = { + title: "Umamin Social — Login", + description: + "Log in to Umamin to send and receive encrypted anonymous messages. Secure your privacy and communicate freely.", + keywords: [ + "Umamin login", + "anonymous messaging login", + "encrypted messages login", + ], + robots: { + index: false, + follow: false, + }, + openGraph: { + type: "website", + title: "Umamin Social — Login", + description: + "Log in to Umamin to send and receive encrypted anonymous messages. Secure your privacy and communicate freely.", + url: "https://social.umamin.link/login", + }, + twitter: { + card: "summary_large_image", + title: "Umamin Social — Login", + description: + "Log in to Umamin to send and receive encrypted anonymous messages. Secure your privacy and communicate freely.", + }, +}; + +export default async function Login() { + const { user } = await getSession(); + + if (user) { + redirect("/"); + } + + return ( +
+ + +
+

+ Umamin Account +

+

+ Proceed with your Umamin v2.0 profile +

+
+ + + +
+ Don't have an account?{" "} + + Sign up + +
+
+ ); +} diff --git a/apps/social/src/app/(authentication)/register/components/button.tsx b/apps/social/src/app/(authentication)/register/components/button.tsx new file mode 100644 index 00000000..047ce82d --- /dev/null +++ b/apps/social/src/app/(authentication)/register/components/button.tsx @@ -0,0 +1,25 @@ +"use client"; + +import Link from "next/link"; +import { Loader2 } from "lucide-react"; +import { useFormStatus } from "react-dom"; +import { Button } from "@umamin/ui/components/button"; + +export function RegisterButton() { + const { pending } = useFormStatus(); + + return ( +
+ + + +
+ ); +} diff --git a/apps/social/src/app/(authentication)/register/components/form.tsx b/apps/social/src/app/(authentication)/register/components/form.tsx new file mode 100644 index 00000000..03f325ac --- /dev/null +++ b/apps/social/src/app/(authentication)/register/components/form.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { signup } from "@/actions"; +import { useFormState } from "react-dom"; +import { RegisterButton } from "./button"; +import { cn } from "@umamin/ui/lib/utils"; +import { Input } from "@umamin/ui/components/input"; +import { Label } from "@umamin/ui/components/label"; + +export function RegisterForm() { + const [state, formAction] = useFormState(signup, { errors: {} }); + + return ( +
+
+ + +

+ {state.errors.username + ? state.errors.username[0] + : "You can still change this later"} +

+
+ +
+ + + {state.errors.password && ( +

+ {state.errors.password[0]} +

+ )} +
+ +
+ + + {state.errors.confirmPassword && ( +

+ {state.errors.confirmPassword[0]} +

+ )} +
+ + + + ); +} diff --git a/apps/social/src/app/(authentication)/register/loading.tsx b/apps/social/src/app/(authentication)/register/loading.tsx new file mode 100644 index 00000000..d623ffdd --- /dev/null +++ b/apps/social/src/app/(authentication)/register/loading.tsx @@ -0,0 +1,44 @@ +import { Skeleton } from "@umamin/ui/components/skeleton"; + +export default function Loading() { + return ( +
+
+
+ + +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ +
+
+
+ + +
+ + +
+ + + +
+
+ ); +} diff --git a/apps/social/src/app/(authentication)/register/page.tsx b/apps/social/src/app/(authentication)/register/page.tsx new file mode 100644 index 00000000..eb2d6a35 --- /dev/null +++ b/apps/social/src/app/(authentication)/register/page.tsx @@ -0,0 +1,77 @@ +import Link from "next/link"; +import dynamic from "next/dynamic"; +import { getSession } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import { RegisterForm } from "./components/form"; + +const BrowserWarning = dynamic( + () => import("@umamin/ui/components/browser-warning"), + { ssr: false } +); + +export const metadata = { + title: "Umamin Social — Register", + description: + "Create an account on Umamin to start sending and receiving encrypted anonymous messages. Join our secure platform and ensure your privacy today.", + keywords: [ + "Umamin Social register", + "sign up for Umamin", + "anonymous messaging sign up", + ], + robots: { + index: false, + follow: false, + }, + openGraph: { + type: "website", + title: "Umamin Social — Register", + description: + "Create an account on Umamin to start sending and receiving encrypted anonymous messages. Join our secure platform and ensure your privacy today.", + url: "https://social.umamin.link/register", + }, + twitter: { + card: "summary_large_image", + title: "Umamin Social — Register", + description: + "Create an account on Umamin to start sending and receiving encrypted anonymous messages. Join our secure platform and ensure your privacy today.", + }, +}; + +export default async function Register() { + const { user } = await getSession(); + + if (user) { + redirect("/"); + } + + return ( +
+ + +
+

+ Umamin Account +

+

+ By creating an account, you agree to our{" "} + + Privacy Policy + {" "} + and{" "} + + Terms of Service + +

+
+ + + +
+ Already have an account?{" "} + + Login + +
+
+ ); +} diff --git a/apps/social/src/app/layout.tsx b/apps/social/src/app/layout.tsx index 26b5c20b..fa5d08a6 100644 --- a/apps/social/src/app/layout.tsx +++ b/apps/social/src/app/layout.tsx @@ -1,25 +1,59 @@ import "@umamin/ui/globals.css"; - +import Script from "next/script"; import { Toaster } from "sonner"; -import type { Metadata } from "next"; import { GeistSans } from "geist/font/sans"; import NextTopLoader from "nextjs-toploader"; +import type { Metadata, Viewport } from "next"; + import { ThemeProvider } from "@umamin/ui/components/theme-provider"; +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + themeColor: "black", +}; + export const metadata: Metadata = { - title: "Umamin Social — A Platform Built to Share Stories", + metadataBase: new URL("https://www.umamin.link"), + alternates: { + canonical: "/", + }, + title: "Umamin — The Platform for Anonymity", + authors: [{ name: "Omsimos Collective" }], description: - "A social platform built for the Umamin community, connect with others by sharing your stories and experiences.", - robots: "index, follow", + "Umamin is an open-source social platform for sending and receiving encrypted anonymous messages. Ensure your privacy and share your thoughts freely without revealing your identity. Perfect for secure communication and anonymous interactions.", + keywords: [ + "anonymous messaging", + "open-source platform", + "encrypted messages", + "privacy", + "anonymity", + ], openGraph: { type: "website", - title: "Umamin Social — A Platform Built to Share Stories", + siteName: "Umamin", + url: "https://www.umamin.link", + title: "Umamin — The Platform for Anonymity", description: - "A social platform built for the Umamin community, connect with others by sharing your stories and experiences.", - // images: [], + "Umamin is an open-source social platform for sending and receiving encrypted anonymous messages. Ensure your privacy and share your thoughts freely without revealing your identity. Perfect for secure communication and anonymous interactions.", }, twitter: { card: "summary_large_image", + title: "Umamin — The Platform for Anonymity", + description: + "Umamin is an open-source social platform for sending and receiving encrypted anonymous messages. Ensure your privacy and share your thoughts freely without revealing your identity. Perfect for secure communication and anonymous interactions.", + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + noimageindex: false, + "max-video-preview": -1, + "max-image-preview": "large", + "max-snippet": -1, + }, }, }; @@ -41,6 +75,7 @@ export default function RootLayout({ + + {process.env.NODE_ENV === "production" && ( +