apps/web: add next-auth

This commit is contained in:
vchikalkin 2024-12-24 18:34:50 +03:00
parent ec4fef85d3
commit 9b6df05f4a
16 changed files with 243 additions and 14 deletions

View File

@ -1,6 +1,6 @@
import { Root as TelegramRoot } from '@/components/telegram';
import { TelegramProvider } from '@/providers';
import { type PropsWithChildren } from 'react';
export default async function RootLayout({ children }: Readonly<PropsWithChildren>) {
return <TelegramRoot>{children}</TelegramRoot>;
return <TelegramProvider>{children}</TelegramProvider>;
}

View File

@ -1,14 +1,42 @@
'use client';
import { initData, isMiniAppDark, useSignal } from '@telegram-apps/sdk-react';
import { signIn, useSession } from 'next-auth/react';
import { redirect } from 'next/navigation';
import { useEffect } from 'react';
export default function Auth() {
const initDataState = useSignal(initData.state);
const isDark = isMiniAppDark();
const { data: session, status } = useSession();
useEffect(() => {
if (status === 'authenticated') {
redirect('/profile');
}
if (status === 'unauthenticated' && initDataState?.user?.id) {
signIn('telegram', {
callbackUrl: '/profile',
redirect: false,
telegramId: String(initDataState.user.id),
});
}
}, [initDataState, status]);
if (status === 'loading') {
return <div>Loading Auth...</div>;
}
return (
<div>
Hello, {initDataState?.user?.firstName} <br />
Dark: {JSON.stringify(isDark)}
{session ? (
<div>
Hello, {JSON.stringify(session)} <br />
Dark: {JSON.stringify(isDark)}
</div>
) : (
<div>Not authenticated</div>
)}
</div>
);
}

View File

@ -1,3 +1,8 @@
export default function ProfilePage() {
return 'Profile';
import { authOptions } from '@/config/auth';
import { getServerSession } from 'next-auth/next';
export default async function ProfilePage() {
const session = await getServerSession(authOptions);
return <pre>{JSON.stringify(session, null, 2)}</pre>;
}

View File

@ -0,0 +1,6 @@
import { authOptions } from '@/config/auth';
import NextAuth from 'next-auth';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View File

@ -1,7 +1,8 @@
import { AuthProvider } from '@/providers';
import { I18nProvider } from '@/utils/i18n/provider';
import { type Metadata } from 'next';
import { getLocale } from 'next-intl/server';
import '@repo/ui/globals.css';
import { getLocale } from 'next-intl/server';
import { type PropsWithChildren } from 'react';
export const metadata: Metadata = {
@ -15,7 +16,9 @@ export default async function RootLayout({ children }: Readonly<PropsWithChildre
return (
<html lang={locale}>
<body>
<I18nProvider>{children}</I18nProvider>
<I18nProvider>
<AuthProvider>{children}</AuthProvider>
</I18nProvider>
</body>
</html>
);

View File

@ -1 +0,0 @@
export * from './Root';

42
apps/web/config/auth.ts Normal file
View File

@ -0,0 +1,42 @@
import { type AuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export const authOptions: AuthOptions = {
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (token?.id && session?.user) {
session.user.telegramId = token.id as string;
}
return session;
},
},
providers: [
CredentialsProvider({
async authorize(credentials) {
const { telegramId } = credentials ?? {};
if (!telegramId) {
throw new Error('Invalid Telegram ID');
}
return { id: telegramId };
},
credentials: {
telegramId: { label: 'Telegram ID', type: 'text' },
},
id: 'telegram',
name: 'Telegram',
}),
],
session: {
strategy: 'jwt',
},
};

8
apps/web/config/env.ts Normal file
View File

@ -0,0 +1,8 @@
/* eslint-disable unicorn/prevent-abbreviations */
import { z } from 'zod';
export const envSchema = z.object({
NEXTAUTH_SECRET: z.string(),
});
export const env = envSchema.parse(process.env);

View File

@ -19,16 +19,18 @@
"graphql": "catalog:",
"lucide-react": "catalog:",
"next": "catalog:",
"next-auth": "^4.24.11",
"next-intl": "catalog:",
"react": "catalog:",
"react-dom": "catalog:"
"react-dom": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@playwright/test": "catalog:",
"@repo/eslint-config": "workspace:*",
"@repo/graphql": "workspace:*",
"@repo/lint-staged-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@repo/graphql": "workspace:*",
"@testing-library/react": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",

View File

@ -0,0 +1,6 @@
'use client';
import { SessionProvider } from 'next-auth/react';
export function AuthProvider({ children }: { readonly children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}

View File

@ -0,0 +1,2 @@
export * from './auth';
export * from './telegram';

View File

@ -2,11 +2,11 @@
'use client';
import { useClientOnce, useDidMount } from '@/hooks/telegram';
import { setLocale } from '@/utils/i18n/locale';
import { init } from '@/utils/init';
import { init } from '@/utils/telegram/init';
import { initData, useSignal } from '@telegram-apps/sdk-react';
import { type PropsWithChildren, useEffect } from 'react';
export function Root(props: Readonly<PropsWithChildren>) {
export function TelegramProvider(props: Readonly<PropsWithChildren>) {
// Unfortunately, Telegram Mini Apps does not allow us to use all features of
// the Server Side Rendering. That's why we are showing loader on the server
// side.

View File

@ -16,7 +16,8 @@
"next.config.js",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
".next/types/**/*.ts",
"types/*.ts"
],
"exclude": ["node_modules"]
}

10
apps/web/types/next-auth.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { type DefaultSession } from 'next-auth';
declare module 'next-auth' {
interface Session extends DefaultSession {
user?: {
telegramId?: null | string;
};
}
}

117
pnpm-lock.yaml generated
View File

@ -228,6 +228,9 @@ importers:
next:
specifier: 'catalog:'
version: 15.1.0(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-auth:
specifier: ^4.24.11
version: 4.24.11(next@15.1.0(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-intl:
specifier: 'catalog:'
version: 3.26.0(next@15.1.0(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
@ -237,6 +240,9 @@ importers:
react-dom:
specifier: 'catalog:'
version: 19.0.0(react@19.0.0)
zod:
specifier: 'catalog:'
version: 3.24.1
devDependencies:
'@playwright/test':
specifier: 'catalog:'
@ -1938,6 +1944,9 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
'@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
'@peculiar/asn1-schema@2.3.13':
resolution: {integrity: sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==}
@ -2859,6 +2868,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
core-js-compat@3.39.0:
resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==}
@ -4079,6 +4092,9 @@ packages:
resolution: {integrity: sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==}
hasBin: true
jose@4.15.9:
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
jose@5.9.6:
resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==}
@ -4290,6 +4306,10 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-react@0.462.0:
resolution: {integrity: sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==}
peerDependencies:
@ -4404,6 +4424,20 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
next-auth@4.24.11:
resolution: {integrity: sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==}
peerDependencies:
'@auth/core': 0.34.2
next: ^12.2.5 || ^13 || ^14 || ^15
nodemailer: ^6.6.5
react: ^17.0.2 || ^18 || ^19
react-dom: ^17.0.2 || ^18 || ^19
peerDependenciesMeta:
'@auth/core':
optional: true
nodemailer:
optional: true
next-intl@3.26.0:
resolution: {integrity: sha512-gkamnHIANQzeW8xpTGRxd0xiOCztQhY8GDp79fgdlw0GioqrjTEfSWLhHkgaAtvHRbuh/ByJdwiEY5eNK9bUSQ==}
peerDependencies:
@ -4474,6 +4508,9 @@ packages:
nwsapi@2.2.16:
resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==}
oauth@0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
obj-props@1.4.0:
resolution: {integrity: sha512-p7p/7ltzPDiBs6DqxOrIbtRdwxxVRBj5ROukeNb9RgA+fawhrz5n2hpNz8DDmYR//tviJSj7nUnlppGmONkjiQ==}
engines: {node: '>=0.10.0'}
@ -4482,6 +4519,10 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@2.2.0:
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
engines: {node: '>= 6'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
@ -4510,6 +4551,10 @@ packages:
resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==}
engines: {node: '>= 0.4'}
oidc-token-hash@5.0.3:
resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==}
engines: {node: ^10.13.0 || >=12.0.0}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@ -4525,6 +4570,9 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
openid-client@5.7.1:
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
optimism@0.18.1:
resolution: {integrity: sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==}
@ -4754,6 +4802,14 @@ packages:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
preact-render-to-string@5.2.6:
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
preact: '>=10'
preact@10.25.3:
resolution: {integrity: sha512-dzQmIFtM970z+fP9ziQ3yG4e3ULIbwZzJ734vaMVUTaKQ2+Ru1Ou/gjshOYVHCcd1rpAelC6ngjvjDXph98unQ==}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@ -4776,6 +4832,9 @@ packages:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
pretty-format@3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
promise@7.3.1:
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
@ -5553,6 +5612,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
@ -5760,6 +5823,9 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml-ast-parser@0.0.43:
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
@ -7664,6 +7730,8 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
'@panva/hkdf@1.2.1': {}
'@peculiar/asn1-schema@2.3.13':
dependencies:
asn1js: 3.0.5
@ -8702,6 +8770,8 @@ snapshots:
convert-source-map@2.0.0: {}
cookie@0.7.2: {}
core-js-compat@3.39.0:
dependencies:
browserslist: 4.24.2
@ -10219,6 +10289,8 @@ snapshots:
jiti@2.4.1: {}
jose@4.15.9: {}
jose@5.9.6: {}
js-tokens@4.0.0: {}
@ -10456,6 +10528,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
lucide-react@0.462.0(react@19.0.0):
dependencies:
react: 19.0.0
@ -10537,6 +10613,21 @@ snapshots:
negotiator@1.0.0: {}
next-auth@4.24.11(next@15.1.0(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@babel/runtime': 7.26.0
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
next: 15.1.0(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.25.3
preact-render-to-string: 5.2.6(preact@10.25.3)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
uuid: 8.3.2
next-intl@3.26.0(next@15.1.0(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
dependencies:
'@formatjs/intl-localematcher': 0.5.9
@ -10607,10 +10698,14 @@ snapshots:
nwsapi@2.2.16: {}
oauth@0.9.15: {}
obj-props@1.4.0: {}
object-assign@4.1.1: {}
object-hash@2.2.0: {}
object-hash@3.0.0: {}
object-inspect@1.13.3: {}
@ -10643,6 +10738,8 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.0.0
oidc-token-hash@5.0.3: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
@ -10659,6 +10756,13 @@ snapshots:
dependencies:
mimic-function: 5.0.1
openid-client@5.7.1:
dependencies:
jose: 4.15.9
lru-cache: 6.0.0
object-hash: 2.2.0
oidc-token-hash: 5.0.3
optimism@0.18.1:
dependencies:
'@wry/caches': 1.0.1
@ -10868,6 +10972,13 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
preact-render-to-string@5.2.6(preact@10.25.3):
dependencies:
preact: 10.25.3
pretty-format: 3.8.0
preact@10.25.3: {}
prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.0:
@ -10884,6 +10995,8 @@ snapshots:
ansi-styles: 5.2.0
react-is: 17.0.2
pretty-format@3.8.0: {}
promise@7.3.1:
dependencies:
asap: 2.0.6
@ -11722,6 +11835,8 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@8.3.2: {}
validate-npm-package-license@3.0.4:
dependencies:
spdx-correct: 3.2.0
@ -11937,6 +12052,8 @@ snapshots:
yallist@3.1.1: {}
yallist@4.0.0: {}
yaml-ast-parser@0.0.43: {}
yaml-eslint-parser@1.2.3: