updated codegen, fixed tsconfig, added prisma
added login, registration and session check
This commit is contained in:
parent
9518787206
commit
04b606b7fa
24 changed files with 740 additions and 1747 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
VITE_HOST=localhost
|
VITE_HOST=localhost
|
||||||
VITE_PORT=4000
|
VITE_PORT=4000
|
||||||
|
|
||||||
ACCESS_TOKEN_SECRET=sometoken1
|
ACCESS_TOKEN_SECRET=sometoken1
|
||||||
REFRESH_TOKEN_SECRET=sometoken2
|
REFRESH_TOKEN_SECRET=sometoken2
|
||||||
|
DATABASE_URL="mysql://janedoe:mypassword@localhost:3306/mydb"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { Theme, ChakraProvider, defaultSystem } from "@chakra-ui/react";
|
import { ChakraProvider, defaultSystem, Theme } from '@chakra-ui/react';
|
||||||
import { ColorModeProvider, type ColorModeProviderProps } from "./color-mode";
|
import { ColorModeProvider, type ColorModeProviderProps } from './color-mode';
|
||||||
|
|
||||||
export function Provider(props: ColorModeProviderProps) {
|
export function Provider(props: ColorModeProviderProps) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
|
||||||
import '@client/assets/styles/index.css';
|
import '@client/assets/styles/index.css';
|
||||||
import '@client/assets/styles/nerd-fonts-icons.css';
|
import '@client/assets/styles/nerd-fonts-icons.css';
|
||||||
import { StrictMode } from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import { Provider } from '@client/components/ui/provider';
|
import { Provider } from '@client/components/ui/provider';
|
||||||
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
||||||
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
|
import { StrictMode } from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
// TanStackRouter - Import the generated route tree
|
// TanStackRouter - Import the generated route tree
|
||||||
import { routeTree } from './routeTree.gen';
|
import { routeTree } from './routeTree.gen';
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
import { Button, Stack } from "@chakra-ui/react";
|
import { Button, Stack } from '@chakra-ui/react';
|
||||||
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
|
import { createRootRoute, Link, Outlet } from '@tanstack/react-router';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
|
|
||||||
const TanStackRouterDevtools =
|
const TanStackRouterDevtools =
|
||||||
process.env.NODE_ENV === "production"
|
process.env.NODE_ENV === 'production'
|
||||||
? () => null // Render nothing in production
|
? () => null // Render nothing in production
|
||||||
: React.lazy(() =>
|
: React.lazy(() =>
|
||||||
// Lazy load in development
|
// Lazy load in development
|
||||||
import("@tanstack/router-devtools").then((res) => ({
|
import('@tanstack/router-devtools').then(res => ({
|
||||||
default: res.TanStackRouterDevtools,
|
default: res.TanStackRouterDevtools
|
||||||
// For Embedded Mode
|
// For Embedded Mode
|
||||||
// default: res.TanStackRouterDevtoolsPanel
|
// default: res.TanStackRouterDevtoolsPanel
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: () => (
|
component: () => (
|
||||||
<Stack padding={3}>
|
<Stack padding={3}>
|
||||||
<Stack flexDirection={"row"} justifyContent={"space-between"}>
|
<Stack flexDirection={'row'} justifyContent={'space-between'}>
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<Button>Home</Button>
|
<Button>Home</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Stack flexDirection={"row"}>
|
<Stack flexDirection={'row'}>
|
||||||
<Link to="/login">
|
<Link to="/login">
|
||||||
<Button>Sign in</Button>
|
<Button>Sign in</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -31,9 +31,9 @@ export const Route = createRootRoute({
|
||||||
</Link>
|
</Link>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<hr style={{ padding: "4px" }} />
|
<hr style={{ padding: '4px' }} />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
</Stack>
|
</Stack>
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
// import { gql } from '@apollo/client';
|
||||||
import { gql, useQuery } from "@apollo/client";
|
import { createLazyFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/book")({
|
export const Route = createLazyFileRoute('/book')({
|
||||||
component: RouteComponent,
|
component: RouteComponent
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <div>"/book"</div>;
|
return <div>"/book"</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBookQuery = gql`
|
// const getBookQuery = gql`
|
||||||
query BookOne {
|
// query BookOne {
|
||||||
booksMany {
|
// booksMany {
|
||||||
id
|
// id
|
||||||
author
|
// author
|
||||||
title
|
// title
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
`;
|
// `;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Text, Stack } from '@chakra-ui/react';
|
import { Stack, Text } from '@chakra-ui/react';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
import { gql, useQuery } from '@apollo/client';
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { gql, useQuery } from '@apollo/client';
|
// import { gql, useQuery } from '@apollo/client';
|
||||||
import { Heading, Input, Stack } from '@chakra-ui/react';
|
import { Heading, Input, Stack } from '@chakra-ui/react';
|
||||||
import { Button } from '@client/components/ui/button';
|
import { Button } from '@client/components/ui/button';
|
||||||
import { Field } from '@client/components/ui/field';
|
import { Field } from '@client/components/ui/field';
|
||||||
|
import { valibotResolver } from '@hookform/resolvers/valibot';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { valibotResolver } from '@hookform/resolvers/valibot';
|
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
|
|
||||||
export const Route = createFileRoute('/login')({
|
export const Route = createFileRoute('/login')({
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { gql, useQuery } from '@apollo/client';
|
// import { gql, useQuery } from '@apollo/client';
|
||||||
import { Heading, Input, Stack } from '@chakra-ui/react';
|
import { Heading, Input, Stack } from '@chakra-ui/react';
|
||||||
import { Button } from '@client/components/ui/button';
|
import { Button } from '@client/components/ui/button';
|
||||||
import { Field } from '@client/components/ui/field';
|
import { Field } from '@client/components/ui/field';
|
||||||
|
import { valibotResolver } from '@hookform/resolvers/valibot';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { valibotResolver } from '@hookform/resolvers/valibot';
|
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
|
|
||||||
export const Route = createFileRoute('/register')({
|
export const Route = createFileRoute('/register')({
|
||||||
|
|
@ -53,7 +53,7 @@ function Register() {
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
} = useForm<FormValues>({ resolver: valibotResolver(schema) });
|
} = useForm<FormValues>({ resolver: valibotResolver(schema) });
|
||||||
|
|
||||||
const onSubmit = handleSubmit(data => console.log(data));
|
const onSubmit = handleSubmit(data => alert(data));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
|
|
|
||||||
|
|
@ -8,130 +8,130 @@
|
||||||
// You should NOT make any changes in this file as it will be overwritten.
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from './pages/__root'
|
import { Route as rootRoute } from './pages/__root';
|
||||||
import { Route as RegisterImport } from './pages/register'
|
import { Route as RegisterImport } from './pages/register';
|
||||||
import { Route as LoginImport } from './pages/login'
|
import { Route as LoginImport } from './pages/login';
|
||||||
import { Route as IndexImport } from './pages/index'
|
import { Route as IndexImport } from './pages/index';
|
||||||
|
|
||||||
// Create Virtual Routes
|
// Create Virtual Routes
|
||||||
|
|
||||||
const BookLazyImport = createFileRoute('/book')()
|
const BookLazyImport = createFileRoute('/book')();
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
const BookLazyRoute = BookLazyImport.update({
|
const BookLazyRoute = BookLazyImport.update({
|
||||||
id: '/book',
|
id: '/book',
|
||||||
path: '/book',
|
path: '/book',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute
|
||||||
} as any).lazy(() => import('./pages/book.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./pages/book.lazy').then(d => d.Route));
|
||||||
|
|
||||||
const RegisterRoute = RegisterImport.update({
|
const RegisterRoute = RegisterImport.update({
|
||||||
id: '/register',
|
id: '/register',
|
||||||
path: '/register',
|
path: '/register',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute
|
||||||
} as any)
|
} as any);
|
||||||
|
|
||||||
const LoginRoute = LoginImport.update({
|
const LoginRoute = LoginImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute
|
||||||
} as any)
|
} as any);
|
||||||
|
|
||||||
const IndexRoute = IndexImport.update({
|
const IndexRoute = IndexImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute
|
||||||
} as any)
|
} as any);
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/';
|
||||||
path: '/'
|
path: '/';
|
||||||
fullPath: '/'
|
fullPath: '/';
|
||||||
preLoaderRoute: typeof IndexImport
|
preLoaderRoute: typeof IndexImport;
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute;
|
||||||
}
|
};
|
||||||
'/login': {
|
'/login': {
|
||||||
id: '/login'
|
id: '/login';
|
||||||
path: '/login'
|
path: '/login';
|
||||||
fullPath: '/login'
|
fullPath: '/login';
|
||||||
preLoaderRoute: typeof LoginImport
|
preLoaderRoute: typeof LoginImport;
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute;
|
||||||
}
|
};
|
||||||
'/register': {
|
'/register': {
|
||||||
id: '/register'
|
id: '/register';
|
||||||
path: '/register'
|
path: '/register';
|
||||||
fullPath: '/register'
|
fullPath: '/register';
|
||||||
preLoaderRoute: typeof RegisterImport
|
preLoaderRoute: typeof RegisterImport;
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute;
|
||||||
}
|
};
|
||||||
'/book': {
|
'/book': {
|
||||||
id: '/book'
|
id: '/book';
|
||||||
path: '/book'
|
path: '/book';
|
||||||
fullPath: '/book'
|
fullPath: '/book';
|
||||||
preLoaderRoute: typeof BookLazyImport
|
preLoaderRoute: typeof BookLazyImport;
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and export the route tree
|
// Create and export the route tree
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute;
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute;
|
||||||
'/register': typeof RegisterRoute
|
'/register': typeof RegisterRoute;
|
||||||
'/book': typeof BookLazyRoute
|
'/book': typeof BookLazyRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute;
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute;
|
||||||
'/register': typeof RegisterRoute
|
'/register': typeof RegisterRoute;
|
||||||
'/book': typeof BookLazyRoute
|
'/book': typeof BookLazyRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRoute;
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute;
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute;
|
||||||
'/register': typeof RegisterRoute
|
'/register': typeof RegisterRoute;
|
||||||
'/book': typeof BookLazyRoute
|
'/book': typeof BookLazyRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||||
fullPaths: '/' | '/login' | '/register' | '/book'
|
fullPaths: '/' | '/login' | '/register' | '/book';
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo;
|
||||||
to: '/' | '/login' | '/register' | '/book'
|
to: '/' | '/login' | '/register' | '/book';
|
||||||
id: '__root__' | '/' | '/login' | '/register' | '/book'
|
id: '__root__' | '/' | '/login' | '/register' | '/book';
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute;
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute;
|
||||||
RegisterRoute: typeof RegisterRoute
|
RegisterRoute: typeof RegisterRoute;
|
||||||
BookLazyRoute: typeof BookLazyRoute
|
BookLazyRoute: typeof BookLazyRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
RegisterRoute: RegisterRoute,
|
RegisterRoute: RegisterRoute,
|
||||||
BookLazyRoute: BookLazyRoute,
|
BookLazyRoute: BookLazyRoute
|
||||||
}
|
};
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
._addFileTypes<FileRouteTypes>()
|
._addFileTypes<FileRouteTypes>();
|
||||||
|
|
||||||
/* ROUTE_MANIFEST_START
|
/* ROUTE_MANIFEST_START
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
/* gql imports */
|
/* gql imports */
|
||||||
import { QueryResolvers } from "@generated/graphql";
|
import { QueryResolvers } from '@generated/types';
|
||||||
|
|
||||||
/* Query */
|
/* Query */
|
||||||
import { BooksManyResolver } from "./resolvers/booksManyResolver";
|
import { BooksManyResolver } from './resolvers/booksManyResolver';
|
||||||
import { BookOneResolver } from "./resolvers/bookOneResolver";
|
import { BookOneResolver } from './resolvers/bookOneResolver';
|
||||||
|
|
||||||
|
/* Mutation */
|
||||||
|
import { LoginResolver } from './resolvers/auth/login';
|
||||||
|
|
||||||
export const Query: QueryResolvers = {
|
export const Query: QueryResolvers = {
|
||||||
booksMany: BooksManyResolver,
|
booksMany: BooksManyResolver,
|
||||||
bookOne: BookOneResolver,
|
bookOne: BookOneResolver
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Mutation = {};
|
export const Mutation = {
|
||||||
|
login: LoginResolver
|
||||||
|
};
|
||||||
|
|
||||||
export const Subscription = {};
|
export const Subscription = {};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,44 @@
|
||||||
|
import { prisma } from '@app/server/prisma/client';
|
||||||
|
import { MutationLoginArgs, Session } from '@generated/types';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
const usersDB = {
|
export async function LoginResolver(_: any, { input }: MutationLoginArgs): Promise<Session> {
|
||||||
users: require('../model/users.json'),
|
const error = 'Invalid username or password';
|
||||||
setUsers: function (data) {
|
|
||||||
this.users = data;
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: input.email }
|
||||||
|
});
|
||||||
|
if (!user) throw new Error(error);
|
||||||
|
|
||||||
|
const match = await bcrypt.compare(input.password, user.password);
|
||||||
|
if (!match) throw new Error(error);
|
||||||
|
|
||||||
|
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET;
|
||||||
|
const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET;
|
||||||
|
|
||||||
|
if (accessTokenSecret == null || refreshTokenSecret == null) throw new Error('No secrets found');
|
||||||
|
|
||||||
|
const accessToken = jwt.sign({ userId: user.id }, accessTokenSecret, {
|
||||||
|
expiresIn: '30s'
|
||||||
|
});
|
||||||
|
const refreshToken = jwt.sign({ userId: user.id }, refreshTokenSecret, {
|
||||||
|
expiresIn: '1d'
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { refreshToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken: {
|
||||||
|
token: refreshToken,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
maxAge: 24 * 60 * 60 * 1000,
|
||||||
|
sameSite: 'None'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
const handleLogin = async (req, res) => {
|
|
||||||
const { user, pwd } = req.body;
|
|
||||||
if (!user || !pwd) res.status(400).json({ message: 'Username and password are required.' });
|
|
||||||
const foundUser = usersDB.users.find(person => person.username === user);
|
|
||||||
if (!foundUser) return res.sendStatus(401); // unauthorized
|
|
||||||
// evaluate password
|
|
||||||
const match = await bcrypt.compare(pwd, foundUser.password);
|
|
||||||
if (match) {
|
|
||||||
// create JWTs
|
|
||||||
res.json({ success: `User ${user} is logged in.` });
|
|
||||||
} else {
|
|
||||||
res.sendStatus(401);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = { handleLogin };
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,28 @@
|
||||||
|
import { prisma, uuid } from '@app/server/prisma/client';
|
||||||
|
import { RegisterInput } from '@generated/types';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
|
|
||||||
export async function SignUpResolver(_: any, input: any): Promise<any> {
|
export async function SignUpResolver(_: any, input: RegisterInput): Promise<void> {
|
||||||
// validate
|
const existingUsername = await prisma.user.findUnique({
|
||||||
|
where: { username: input.username },
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
if (existingUsername != null) throw Error('User with this username already exists.');
|
||||||
|
|
||||||
// db
|
const existingEmail = await prisma.user.findUnique({
|
||||||
const users = [
|
where: { email: input.email },
|
||||||
{
|
select: { id: true }
|
||||||
id: '1',
|
});
|
||||||
email: 'joe@test.com',
|
if (existingEmail != null) throw Error('Email is already in use.');
|
||||||
username: 'Joe',
|
|
||||||
password: 'test'
|
const hashedPassword = await bcrypt.hash(input.password, 10);
|
||||||
|
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
id: uuid(),
|
||||||
|
email: input.email,
|
||||||
|
username: input.username,
|
||||||
|
password: hashedPassword
|
||||||
}
|
}
|
||||||
];
|
});
|
||||||
const usersDB = { users };
|
|
||||||
|
|
||||||
// check for duplicate email+username
|
|
||||||
|
|
||||||
// encrypt the password
|
|
||||||
const hashedPwd = await bcrypt.hash(input.pwd, 10);
|
|
||||||
|
|
||||||
// store user in the db, return new user
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewUser = async (req, res) => {
|
|
||||||
const { user, pwd } = req.body;
|
|
||||||
if (!user || !pwd)
|
|
||||||
return res.status(400).json({ message: 'Username and password are required.' });
|
|
||||||
// check for duplicate usernames in the db.
|
|
||||||
const duplicate = usersDB.users.find(person => person.username === user);
|
|
||||||
if (duplicate) return res.status(409);
|
|
||||||
try {
|
|
||||||
// encrypt the password
|
|
||||||
const hashedPwd = await brcypt.hash(pwd, 10);
|
|
||||||
// store the new user
|
|
||||||
const newUser = { username: user, password: hashedPwd };
|
|
||||||
usersDB.setUsers([...usersDB.users, newUser]);
|
|
||||||
await fs_promises.writeFile(
|
|
||||||
path.join(__dirname, '..', 'model', 'users.json'),
|
|
||||||
JSON.stringify(usersDB.users)
|
|
||||||
);
|
|
||||||
console.log(usersDB.users);
|
|
||||||
res.status(201).json({ success: `New user ${user} created!` });
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ message: err.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { BookInput, Book } from "@generated/graphql";
|
import { Book, QueryBookOneArgs } from '@generated/types';
|
||||||
import { books } from "./booksManyResolver";
|
import { books } from './booksManyResolver';
|
||||||
|
|
||||||
export async function BookOneResolver(_: any, input: BookInput): Promise<Book> {
|
export async function BookOneResolver(_: any, { id }: QueryBookOneArgs): Promise<Book> {
|
||||||
console.log("Called 'BookOneResolver' resolver...");
|
const book = books.filter(item => item.id === id)[0];
|
||||||
|
|
||||||
const book = books.filter((item) => item.id === input.id)[0];
|
|
||||||
|
|
||||||
return book;
|
return book;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
import { Book } from "@generated/graphql";
|
import { Book } from '@generated/types';
|
||||||
|
|
||||||
export const books = [
|
export const books = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: '1',
|
||||||
title: "some-title-1",
|
title: 'some-title-1',
|
||||||
author: "someone",
|
author: 'someone'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2",
|
id: '2',
|
||||||
title: "some-title-1",
|
title: 'some-title-1',
|
||||||
author: "someone",
|
author: 'someone'
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function BooksManyResolver(_: any): Promise<Book[]> {
|
export async function BooksManyResolver(_: any): Promise<Book[]> {
|
||||||
console.log("Called 'BooksManyResolver' resolver...");
|
|
||||||
|
|
||||||
return books;
|
return books;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,37 @@
|
||||||
|
scalar Void
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
booksMany: [Book]!
|
booksMany: [Book!]!
|
||||||
bookOne(input: BookInput!): [Book]!
|
bookOne(id: ID!): Book!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
login(input: LoginInput!): Session!
|
||||||
|
register(input: RegisterInput!): Void!
|
||||||
|
}
|
||||||
|
|
||||||
|
input LoginInput {
|
||||||
|
email: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input RegisterInput {
|
||||||
|
email: String!
|
||||||
|
username: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session {
|
||||||
|
accessToken: String!
|
||||||
|
refreshToken: RefreshToken!
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshToken {
|
||||||
|
token: String!
|
||||||
|
maxAge: Int!
|
||||||
|
secure: Boolean!
|
||||||
|
httpOnly: Boolean!
|
||||||
|
sameSite: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Book {
|
type Book {
|
||||||
|
|
@ -8,7 +39,3 @@ type Book {
|
||||||
title: String!
|
title: String!
|
||||||
author: String!
|
author: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input BookInput {
|
|
||||||
id: ID!
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,31 @@
|
||||||
import "dotenv/config";
|
import { ApolloServer } from '@apollo/server';
|
||||||
import express from "express";
|
import { expressMiddleware } from '@apollo/server/express4';
|
||||||
import cors from "cors";
|
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
|
||||||
import ViteExpress from "vite-express";
|
import cors from 'cors';
|
||||||
import { ApolloServer } from "@apollo/server";
|
import 'dotenv/config';
|
||||||
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
|
import express, { Request, Response } from 'express';
|
||||||
import { expressMiddleware } from "@apollo/server/express4";
|
import fs from 'fs';
|
||||||
import http from "http";
|
import http from 'http';
|
||||||
import fs from "fs";
|
import jwt from 'jsonwebtoken';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
|
import ViteExpress from 'vite-express';
|
||||||
|
|
||||||
import { Query, Mutation, Subscription } from "./graphql/graphql";
|
import { Mutation, Query } from './graphql/graphql';
|
||||||
|
import { prisma } from './prisma/client';
|
||||||
|
|
||||||
const __dirname = import.meta.dirname;
|
const __dirname = import.meta.dirname;
|
||||||
const HOST = process.env.VITE_HOST || "localhost";
|
const HOST = process.env.VITE_HOST || 'localhost';
|
||||||
const PORT = parseInt(process.env.VITE_PORT || "3000");
|
const PORT = parseInt(process.env.VITE_PORT || '3000');
|
||||||
const MOTD = `\n\n\n\x1b[35m-server is listening at...-\x1b[0m\n\nhttp://${HOST}:${PORT}\nhttp://localhost:${PORT}\n\n\n`;
|
const MOTD = `\n\n\n\x1b[35m-server is listening at...-\x1b[0m\n\nhttp://${HOST}:${PORT}\nhttp://localhost:${PORT}\n\n\n`;
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
const typeDefs = fs.readFileSync(
|
const typeDefs = fs.readFileSync(path.join(__dirname, 'graphql', 'schema.graphql'), 'utf8');
|
||||||
path.join(__dirname, "graphql", "schema.graphql"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query,
|
Query,
|
||||||
// Mutation,
|
Mutation
|
||||||
// Subscription,
|
// Subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -37,31 +36,35 @@ interface IdentityContext {
|
||||||
const apollo = new ApolloServer<IdentityContext>({
|
const apollo = new ApolloServer<IdentityContext>({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
plugins: [ApolloServerPluginDrainHttpServer({ httpServer: server })],
|
plugins: [ApolloServerPluginDrainHttpServer({ httpServer: server })]
|
||||||
});
|
});
|
||||||
|
|
||||||
await apollo.start();
|
await apollo.start();
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
"/graphql",
|
'/graphql',
|
||||||
cors<cors.CorsRequest>(),
|
cors<cors.CorsRequest>(),
|
||||||
express.json(),
|
express.json(),
|
||||||
expressMiddleware(apollo, {
|
expressMiddleware(apollo, {
|
||||||
context: async ({ req, res }) => {
|
context: getUser
|
||||||
const token = req.headers.authorization || "";
|
})
|
||||||
// const user = await getUser(token);
|
|
||||||
|
|
||||||
const user = "user";
|
|
||||||
|
|
||||||
return { user };
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await new Promise<void>((resolve) =>
|
await new Promise<void>(resolve => server.listen({ hostname: HOST, port: PORT }, resolve));
|
||||||
server.listen({ hostname: HOST, port: PORT }, resolve),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(MOTD);
|
process.stdout.write(MOTD);
|
||||||
|
|
||||||
ViteExpress.bind(app, server);
|
ViteExpress.bind(app, server);
|
||||||
|
|
||||||
|
async function getUser({ req, res }: { req: Request; res: Response }) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader) return { identity: null };
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
|
||||||
|
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET;
|
||||||
|
if (accessTokenSecret == null) throw Error('No `accessToken`');
|
||||||
|
|
||||||
|
const decoded = jwt.verify(token, accessTokenSecret) as { userId: string };
|
||||||
|
const identity = await prisma.user.findUnique({ where: { id: decoded.userId } });
|
||||||
|
return { identity };
|
||||||
|
}
|
||||||
|
|
|
||||||
8
app/server/prisma/client.ts
Normal file
8
app/server/prisma/client.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export function uuid() {
|
||||||
|
return v4();
|
||||||
|
}
|
||||||
21
app/server/prisma/schema.prisma
Normal file
21
app/server/prisma/schema.prisma
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "mysql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id
|
||||||
|
|
||||||
|
email String @unique @db.VarChar(255)
|
||||||
|
username String @unique @db.VarChar(255)
|
||||||
|
password String
|
||||||
|
|
||||||
|
refreshToken String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
14
codegen.ts
14
codegen.ts
|
|
@ -2,20 +2,26 @@ import type { CodegenConfig } from '@graphql-codegen/cli';
|
||||||
|
|
||||||
const config: CodegenConfig = {
|
const config: CodegenConfig = {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
|
|
||||||
schema: './app/server/graphql/schema.graphql',
|
schema: './app/server/graphql/schema.graphql',
|
||||||
documents: ['app/client/fragments', 'app/client/**/*.tsx'],
|
documents: ['app/client/fragments', 'app/client/**/*.tsx'],
|
||||||
generates: {
|
generates: {
|
||||||
'./__generated__/': {
|
'./__generated__/': {
|
||||||
preset: 'client',
|
|
||||||
presetConfig: {
|
presetConfig: {
|
||||||
|
preset: 'client',
|
||||||
gqlTagName: 'gql'
|
gqlTagName: 'gql'
|
||||||
},
|
},
|
||||||
plugins: ['typescript', 'typescript-resolvers']
|
plugins: ['typescript', 'typescript-resolvers', 'typescript-resolvers']
|
||||||
},
|
},
|
||||||
'./__generated__/types.ts': {
|
'./__generated__/types.ts': {
|
||||||
plugins: ['typescript', 'typescript-operations']
|
plugins: ['typescript', 'typescript-operations', 'typescript-resolvers'],
|
||||||
|
config: {
|
||||||
|
scalars: {
|
||||||
|
Void: 'void'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'./graphql.schema.json': {
|
'./__generated__/graphql.schema.json': {
|
||||||
plugins: ['introspection']
|
plugins: ['introspection']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
1246
graphql.schema.json
1246
graphql.schema.json
File diff suppressed because it is too large
Load diff
689
package-lock.json
generated
689
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
|
@ -8,7 +8,8 @@
|
||||||
"start": "NODE_ENV=production tsx app/server/main.ts",
|
"start": "NODE_ENV=production tsx app/server/main.ts",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"gql-types": "graphql-codegen --config codegen.ts",
|
"gql-types": "graphql-codegen --config codegen.ts",
|
||||||
"checkup": "opinionated checkup"
|
"checkup": "opinionated checkup",
|
||||||
|
"prisma": "prisma"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.12.2",
|
"@apollo/client": "^3.12.2",
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
"@deftomat/opinionated": "^1.1.3",
|
"@deftomat/opinionated": "^1.1.3",
|
||||||
"@emotion/react": "^11.13.5",
|
"@emotion/react": "^11.13.5",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
|
"@prisma/client": "^6.4.1",
|
||||||
"@tanstack/react-router": "^1.85.0",
|
"@tanstack/react-router": "^1.85.0",
|
||||||
"@tanstack/router-vite-plugin": "^1.84.4",
|
"@tanstack/router-vite-plugin": "^1.84.4",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
|
|
@ -34,6 +36,7 @@
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"tsx": "^4.3.0",
|
"tsx": "^4.3.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"valibot": "^1.0.0-beta.10",
|
"valibot": "^1.0.0-beta.10",
|
||||||
"vite-express": "^0.20.0"
|
"vite-express": "^0.20.0"
|
||||||
},
|
},
|
||||||
|
|
@ -47,14 +50,18 @@
|
||||||
"@tanstack/router-plugin": "^1.84.4",
|
"@tanstack/router-plugin": "^1.84.4",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/node": "^20.9.3",
|
"@types/node": "^20.9.3",
|
||||||
"@types/react": "^18.0.38",
|
"@types/react": "^18.0.38",
|
||||||
"@types/react-dom": "^18.2.16",
|
"@types/react-dom": "^18.2.16",
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
|
"prisma": "^6.4.1",
|
||||||
"vite": "^5.0.2",
|
"vite": "^5.0.2",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"prettier": "@deftomat/opinionated/configs/prettier.config.mjs"
|
"prettier": "@deftomat/opinionated/configs/prettier.config.mjs",
|
||||||
|
"prisma": {
|
||||||
|
"schema": "app/server/prisma/schema.prisma"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
|
"jsx": "react-jsx",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|
@ -21,6 +22,6 @@
|
||||||
"@generated/*": ["./__generated__/*"],
|
"@generated/*": ["./__generated__/*"],
|
||||||
"@root/*": ["./*"]
|
"@root/*": ["./*"]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"exclude": ["./app/client/components/ui", "./app/client/assets", "./__generated__/graphql.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// vite.config.ts
|
// vite.config.ts
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from 'vite';
|
||||||
import viteReact from "@vitejs/plugin-react";
|
import viteReact from '@vitejs/plugin-react';
|
||||||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
|
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
|
||||||
import tsConfigPaths from "vite-tsconfig-paths";
|
import tsConfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [TanStackRouterVite(), viteReact(), tsConfigPaths()],
|
plugins: [TanStackRouterVite(), viteReact(), tsConfigPaths()]
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue