updated codegen, fixed tsconfig, added prisma

added login, registration and session check
This commit is contained in:
lnn0q 2025-03-23 21:06:15 +02:00
parent 9518787206
commit 04b606b7fa
24 changed files with 740 additions and 1747 deletions

View file

@ -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"

View file

@ -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 (

View file

@ -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';

View file

@ -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>
), )
}); });

View file

@ -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
} // }
} // }
`; // `;

View file

@ -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';

View file

@ -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')({

View file

@ -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}>

View file

@ -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
{ {

View file

@ -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 = {};

View file

@ -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 };

View file

@ -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 });
}
};

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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!
}

View file

@ -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 };
}

View file

@ -0,0 +1,8 @@
import { PrismaClient } from '@prisma/client';
import { v4 } from 'uuid';
export const prisma = new PrismaClient();
export function uuid() {
return v4();
}

View 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
}

View file

@ -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']
} }
}, },

File diff suppressed because it is too large Load diff

689
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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"
}
} }

View file

@ -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"]
} }

View file

@ -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()]
}); });