Add: tRPC + register on first time login

This commit is contained in:
Sonny
2023-02-15 00:02:55 +01:00
parent 1636f9152b
commit 7074901dcb
12 changed files with 4443 additions and 1807 deletions

View File

@@ -1,12 +1,12 @@
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { FormEvent } from "react";
import { config } from "../config";
import MessageManager from "./MessageManager"; import MessageManager from "./MessageManager";
import styles from "../styles/create.module.scss"; import styles from "../styles/create.module.scss";
import { config } from "../config";
interface FormProps { interface FormProps {
title: string; title: string;
errorMessage?: string; errorMessage?: string;
@@ -14,7 +14,7 @@ interface FormProps {
infoMessage?: string; infoMessage?: string;
canSubmit: boolean; canSubmit: boolean;
handleSubmit: (event) => void; handleSubmit: (event: FormEvent<HTMLFormElement>) => void;
textBtnConfirm?: string; textBtnConfirm?: string;
classBtnConfirm?: string; classBtnConfirm?: string;

5739
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,11 @@
"dependencies": { "dependencies": {
"@prisma/client": "^4.10.0", "@prisma/client": "^4.10.0",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@tanstack/react-query": "^4.24.6",
"@trpc/client": "^10.11.1",
"@trpc/next": "^10.11.1",
"@trpc/react-query": "^10.11.1",
"@trpc/server": "^10.11.1",
"axios": "^1.3.2", "axios": "^1.3.2",
"next": "^13.1.6", "next": "^13.1.6",
"next-auth": "^4.19.2", "next-auth": "^4.19.2",
@@ -21,7 +26,8 @@
"react-select": "^5.7.0", "react-select": "^5.7.0",
"sass": "^1.58.0", "sass": "^1.58.0",
"sharp": "^0.31.3", "sharp": "^0.31.3",
"toastr": "^2.1.4" "toastr": "^2.1.4",
"zod": "^3.20.6"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.13.0", "@types/node": "^18.13.0",

View File

@@ -1,44 +1,49 @@
import { useEffect } from 'react'; import { SessionProvider } from "next-auth/react";
import { SessionProvider } from 'next-auth/react'; import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import nProgress from "nprogress";
import { useEffect } from "react";
import { useRouter } from 'next/router'; import AuthRequired from "../components/AuthRequired";
import nProgress from 'nprogress'; import { trpc } from "../utils/trpc";
import 'nprogress/nprogress.css';
import AuthRequired from '../components/AuthRequired'; import "nprogress/nprogress.css";
import "../styles/globals.scss";
import '../styles/globals.scss';
interface MyAppProps extends AppProps {
Component: any; // TODO: fix type
}
function MyApp({ function MyApp({
Component, Component,
pageProps: { session, ...pageProps } pageProps: { session, ...pageProps },
}) { }: MyAppProps) {
const router = useRouter(); const router = useRouter();
useEffect(() => { // Chargement pages useEffect(() => {
router.events.on('routeChangeStart', nProgress.start); // Chargement pages
router.events.on('routeChangeComplete', nProgress.done); router.events.on("routeChangeStart", nProgress.start);
router.events.on('routeChangeError', nProgress.done); router.events.on("routeChangeComplete", nProgress.done);
router.events.on("routeChangeError", nProgress.done);
return () => { return () => {
router.events.off('routeChangeStart', nProgress.start); router.events.off("routeChangeStart", nProgress.start);
router.events.off('routeChangeComplete', nProgress.done); router.events.off("routeChangeComplete", nProgress.done);
router.events.off('routeChangeError', nProgress.done); router.events.off("routeChangeError", nProgress.done);
} };
}); });
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
{Component.authRequired ? ( {Component.authRequired ? (
<AuthRequired> <AuthRequired>
<Component {...pageProps} /> <Component {...pageProps} />
</AuthRequired> </AuthRequired>
) : ( ) : (
<Component {...pageProps} /> <Component {...pageProps} />
)} )}
</SessionProvider> </SessionProvider>
); );
} }
export default MyApp; export default trpc.withTRPC(MyApp);

View File

@@ -1,64 +1,101 @@
import NextAuth from 'next-auth'; import NextAuth from "next-auth";
import GoogleProvider from 'next-auth/providers/google'; import GoogleProvider from "next-auth/providers/google";
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
export default NextAuth({ export default NextAuth({
providers: [ providers: [
GoogleProvider({ GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID, clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET, clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: { authorization: {
params: { params: {
prompt: 'consent', prompt: "consent",
access_type: 'offline', access_type: "offline",
response_type: 'code' response_type: "code",
} },
} },
}) }),
], ],
callbacks: { callbacks: {
async signIn({ account: accountParam, profile }) { // TODO: Auth async signIn({ account: accountParam, profile }) {
console.log('Connexion via', accountParam.provider, accountParam.providerAccountId, profile.email, profile.name) // TODO: Auth
if (accountParam.provider !== 'google') { console.log(
return '/signin?error=' + encodeURI('Authentitifcation via Google requise'); "Connexion via",
} accountParam.provider,
accountParam.providerAccountId,
profile.email,
profile.name
);
if (accountParam.provider !== "google") {
return (
"/signin?error=" + encodeURI("Authentitifcation via Google requise")
);
}
const email = profile?.email; const email = profile?.email;
if (email === '') { if (email === "") {
return '/signin?error=' + encodeURI('Impossible de récupérer l\'email associé à ce compte Google'); return (
} "/signin?error=" +
encodeURI(
"Impossible de récupérer l'email associé à ce compte Google"
)
);
}
const googleId = profile?.sub; const googleId = profile?.sub;
if (googleId === '') { if (googleId === "") {
return '/signin?error=' + encodeURI('Impossible de récupérer l\'identifiant associé à ce compte Google'); return (
} "/signin?error=" +
encodeURI(
"Impossible de récupérer l'identifiant associé à ce compte Google"
)
);
}
try { try {
const account = await prisma.user.findFirst({ const account = await prisma.user.findFirst({
where: { where: {
google_id: googleId, google_id: googleId,
email email,
} },
}); });
const accountCount = await prisma.user.count();
if (!account) { if (!account) {
return '/signin?error=' + encodeURI('Vous n\'êtes pas autorisé à vous connecter avec ce compte Google'); if (accountCount === 0) {
} else { await prisma.user.create({
return true; data: {
} email,
} catch (error) { google_id: googleId,
console.error(error); },
return '/signin?error=' + encodeURI('Une erreur est survenue lors de l\'authentification'); });
} return true;
}
return (
"/signin?error=" +
encodeURI(
"Vous n'êtes pas autorisé à vous connecter avec ce compte Google"
)
);
} else {
return true;
} }
} catch (error) {
console.error(error);
return (
"/signin?error=" +
encodeURI("Une erreur est survenue lors de l'authentification")
);
}
}, },
pages: { },
signIn: '/signin', pages: {
error: '/signin' signIn: "/signin",
}, error: "/signin",
session: { },
maxAge: 60 * 60 * 6 // Session de 6 heures session: {
} maxAge: 60 * 60 * 6, // Session de 6 heures
},
}); });

10
pages/api/trpc/[trpc].ts Normal file
View File

@@ -0,0 +1,10 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { appRouter } from "../../../server/routers/_app";
// export API handler
// @see https://trpc.io/docs/api-handler
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: () => ({}),
});

View File

@@ -1,64 +1,72 @@
import { useEffect, useState } from 'react'; import axios, { AxiosResponse } from "axios";
import { useRouter } from "next/router";
import nProgress from "nprogress";
import { FormEvent, useMemo, useState } from "react";
import nProgress from 'nprogress'; import FormLayout from "../../components/FormLayout";
import axios, { AxiosResponse } from 'axios'; import TextBox from "../../components/TextBox";
import FormLayout from '../../components/FormLayout'; import { Category } from "../../types";
import TextBox from '../../components/TextBox'; import { HandleAxiosError } from "../../utils/front";
import { trpc } from "../../utils/trpc";
import styles from '../../styles/create.module.scss'; import styles from "../../styles/create.module.scss";
import { HandleAxiosError } from '../../utils/front';
import { useRouter } from 'next/router';
function CreateCategory() { function CreateCategory() {
const info = useRouter().query?.info as string; const hello = trpc.hello.useQuery({ text: "client" });
const [name, setName] = useState<string>(''); const info = useRouter().query?.info as string;
const [name, setName] = useState<string>("");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | undefined>();
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | undefined>();
const [canSubmit, setCanSubmit] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
useEffect(() => setCanSubmit(name.length !== 0), [name]); const canSubmit = useMemo<boolean>(
() => name.length !== 0 && !loading,
[loading, name.length]
);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSuccess(undefined);
setError(undefined);
setLoading(true);
nProgress.start();
const handleSubmit = async (event) => { try {
event.preventDefault(); const payload = { name };
setSuccess(null); const { data }: AxiosResponse<{ success: string; category: Category }> =
setError(null); await axios.post("/api/category/create", payload);
setCanSubmit(false);
nProgress.start();
try { console.log(data);
const payload = { name }; setSuccess(data.success);
const { data }: AxiosResponse<any> = await axios.post('/api/category/create', payload); } catch (error) {
setSuccess(data?.success || 'Categorie créée avec succès'); setError(HandleAxiosError(error));
setCanSubmit(false); } finally {
} catch (error) { setLoading(false);
setError(HandleAxiosError(error)); nProgress.done();
setCanSubmit(true);
} finally {
nProgress.done();
}
} }
};
return (<> return (
<FormLayout <FormLayout
title='Créer une catégorie' title="Créer une catégorie"
errorMessage={error} errorMessage={error}
successMessage={success} successMessage={success}
infoMessage={info} infoMessage={info}
canSubmit={canSubmit} canSubmit={canSubmit}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
> >
<TextBox <TextBox
name='name' name="name"
label='Nom de la catégorie' label="Nom de la catégorie"
onChangeCallback={(value) => setName(value)} onChangeCallback={(value) => setName(value)}
value={name} value={name}
fieldClass={styles['input-field']} fieldClass={styles["input-field"]}
placeholder='Nom...' placeholder="Nom..."
/> />
</FormLayout> {JSON.stringify(hello)}
</>); </FormLayout>
);
} }
CreateCategory.authRequired = true; CreateCategory.authRequired = true;

View File

@@ -13,67 +13,64 @@ import { prisma } from "../utils/back";
import { config } from "../config"; import { config } from "../config";
interface HomeProps { interface HomeProps {
categories: Category[]; categories: Category[];
favorites: Link[]; favorites: Link[];
} }
function Home({ categories, favorites }: HomeProps) { function Home({ categories, favorites }: HomeProps) {
const { data } = useSession({ required: true }); const { data } = useSession({ required: true });
const [categoryActive, setCategoryActive] = useState<Category | null>( const [categoryActive, setCategoryActive] = useState<Category | null>(
categories?.[0] categories?.[0]
); );
const handleSelectCategory = (category: Category) => const handleSelectCategory = (category: Category) =>
setCategoryActive(category); setCategoryActive(category);
return ( return (
<> <>
<Head> <Head>
<title>{config.siteName}</title> <title>{config.siteName}</title>
</Head> </Head>
<div className="App"> <div className="App">
<Menu <Menu
categories={categories} categories={categories}
favorites={favorites} favorites={favorites}
handleSelectCategory={handleSelectCategory} handleSelectCategory={handleSelectCategory}
categoryActive={categoryActive} categoryActive={categoryActive}
session={data} session={data}
/> />
<Links category={categoryActive} /> <Links category={categoryActive} />
</div> </div>
</> </>
); );
} }
export async function getServerSideProps() { export async function getServerSideProps() {
const categoriesDB = await prisma.category.findMany({ const categoriesDB = await prisma.category.findMany({
include: { links: true }, include: { links: true },
}); });
const favorites = [] as Link[]; const favorites = [] as Link[];
const categories = categoriesDB.map((categoryDB) => { const categories = categoriesDB.map((categoryDB) => {
const category = BuildCategory(categoryDB); const category = BuildCategory(categoryDB);
category.links.map((link) => category.links.map((link) => (link.favorite ? favorites.push(link) : null));
link.favorite ? favorites.push(link) : null return category;
); });
return category;
});
if (categories.length === 0) {
return {
redirect: {
destination:
"/category/create?info=Veuillez créer une catégorie",
},
};
}
if (categories.length === 0) {
return { return {
props: { redirect: {
categories: JSON.parse(JSON.stringify(categories)), destination: "/category/create",
favorites: JSON.parse(JSON.stringify(favorites)), },
},
}; };
}
return {
props: {
categories: JSON.parse(JSON.stringify(categories)),
favorites: JSON.parse(JSON.stringify(favorites)),
},
};
} }
Home.authRequired = true; Home.authRequired = true;

19
server/routers/_app.ts Normal file
View File

@@ -0,0 +1,19 @@
import { z } from "zod";
import { procedure, router } from "../trpc";
export const appRouter = router({
hello: procedure
.input(
z.object({
text: z.string(),
})
)
.query(({ input }) => {
return {
greeting: `hello ${input.text}`,
};
}),
});
// export type definition of API
export type AppRouter = typeof appRouter;

9
server/trpc.ts Normal file
View File

@@ -0,0 +1,9 @@
import { initTRPC } from "@trpc/server";
// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC.create();
// Base router and procedure helpers
export const router = t.router;
export const procedure = t.procedure;

View File

@@ -1,5 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": [
"dom", "dom",
@@ -8,7 +8,7 @@
], ],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
@@ -18,14 +18,13 @@
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true "incremental": true
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"**/*.ts", "**/*.ts",
"**/*.tsx" "**/*.tsx"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"
] ]
} }

21
utils/trpc.ts Normal file
View File

@@ -0,0 +1,21 @@
import { httpBatchLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next";
import type { AppRouter } from "../server/routers/_app";
const getBaseUrl = () =>
typeof window !== "undefined"
? ""
: `http://localhost:${process.env.PORT ?? 3000}`;
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
};
},
ssr: false,
});