Implement register form

This commit is contained in:
Jorge Vargas 2024-11-15 14:41:21 -06:00
parent adeb3fd3bf
commit cdcd71cf2a
17 changed files with 526 additions and 99 deletions

View file

@ -7,8 +7,8 @@ export default function Button(props: PropsWithChildren<{ className?: string; lo
return (
<button
className={clsx(
loading ? 'bg-blue-400 cursor-progress' : 'bg-blue-600 hover:bg-blue-700',
'py-2 px-3.5 rounded-lg',
{ 'cursor-progress': loading },
'py-2 px-3.5 rounded-lg bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400',
className
)}
{...restProps}

33
src/components/Modal.tsx Normal file
View file

@ -0,0 +1,33 @@
import { useEffect, type KeyboardEvent, type PropsWithChildren } from 'react'
import type { SetState } from 'types'
export default function Modal(props: PropsWithChildren<{ setOpen: SetState<boolean> }>) {
const { children, setOpen } = props
useEffect(() => {
const handleEsc = (ev: KeyboardEvent) => {
if (ev.code === 'Escape') setOpen(false)
}
window.addEventListener('keydown', handleEsc)
return () => {
window.removeEventListener('keydown', handleEsc)
}
}, [])
return (
<div
className='fixed size-full flex bg-black bg-opacity-50 left-0 top-0 z-50 justify-center items-center'
onClick={() => setOpen(false)}
>
<div
className='bg-white rounded-lg overflow-hidden shadow-xl transform transition-all m-8'
role='dialog'
aria-modal='true'
onClick={(ev) => ev.stopPropagation()}
>
{children}
</div>
</div>
)
}

View file

@ -0,0 +1,86 @@
import type { Session } from '@auth/core/types'
import { useState } from 'react'
import { gql } from '@/graphql/__generated__/client'
import { useMutation } from '@apollo/client/react/hooks'
import Button from 'components/Button'
import * as m from 'paraglide/messages.js'
import Modal from 'components/Modal'
import apolloClient from '@/graphql/apolloClient'
import toast from 'react-hot-toast'
const registerMutation = gql(`
mutation Register($username: String!, $email: String!, $pfp: File) {
registerUser(username: $username, email: $email, pfp: $pfp)
}
`)
export default function RegisterBtn(props: { session: Session }) {
const { session } = props
const [modalOpen, setModalOpen] = useState(false)
const [mutate, { loading }] = useMutation(registerMutation, { client: apolloClient, ignoreResults: true })
if (session) return null
const handleSubmit = (ev) => {
ev.preventDefault()
const formData = new FormData(ev.target)
const variables = Object.fromEntries(formData)
mutate({ variables })
.then(() => {
toast.success(m.emailSuccess())
setModalOpen(false)
})
.catch((err) => {
toast.error(err.message)
})
}
return (
<>
<Button className='rounded-t-none' onClick={() => setModalOpen(true)}>
{m.register()}
</Button>
{modalOpen ? (
<Modal setOpen={setModalOpen}>
<form onSubmit={handleSubmit}>
<div className='bg-white px-4 pt-5 pb-4 gap-x-4 flex flex-col'>
<div className='flex gap-x-4'>
<div className='flex flex-col'>
<label htmlFor='username' className='font-medium text-black'>
{m.username()}:
</label>
<input type='text' name='username' className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black' />
</div>
<div className='flex flex-col'>
<label htmlFor='email' className='font-medium text-black'>
{m.email()}:
</label>
<input type='text' name='email' className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black' />
</div>
</div>
<div className='flex flex-col'>
<label htmlFor='pfp' className='font-medium text-black'>
{m.profilePic()}:
</label>
<input
type='file'
name='pfp'
className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black'
accept='image/*'
/>
</div>
</div>
<div className='bg-zinc-200 px-4 py-3 text-right gap-x-2 flex justify-end'>
<Button className='bg-zinc-500 hover:bg-zinc-600'>{m.close()}</Button>
<Button loading={loading} disabled={loading}>
{m.register()}
</Button>
</div>
</form>
</Modal>
) : null}
</>
)
}

View file

@ -3,15 +3,11 @@ import { SchemaLink } from '@apollo/client/link/schema'
import { getSession } from 'auth-astro/server'
import type { Session } from '@auth/core/types'
import prismaClient, { type users } from 'prisma/client'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { typeDefs } from '@/graphql/__generated__/typeDefs.generated'
import resolvers from '@/graphql/resolvers'
import schema from './schema'
export type ResolverContext = { request?: Request; session?: Session; user?: users }
const schema = makeExecutableSchema({ typeDefs, resolvers })
export async function getApolloClient(request?: Request) {
const session = async () => (request ? getSession(request) : null)
const user = async () => (session ? prismaClient.users.findUnique({ where: { username: session.id } }) : null)

View file

@ -1,28 +1,38 @@
import { composeResolvers } from '@graphql-tools/resolvers-composition'
// import type { Resolvers } from '@/graphql/__generated__/types.generated'
import type { Resolvers } from '@/graphql/__generated__/types.generated'
import generator from 'generate-password-ts'
import bcrypt from 'bcrypt'
import sharp from 'sharp'
import fs from 'fs-extra'
import type { User } from '@auth/core/types'
import prismaClient from 'prisma/client'
import nodemailer from 'nodemailer'
const resolversComposition = {
//'Mutation.updateUser': [isAuthedApp]
import { UserInputError } from 'utils/graphQLErrors'
import forgorTemplate from 'utils/forgorTemplate'
async function processImage(imagePath) {
const sharpImg = sharp(imagePath)
const meta = await sharpImg.metadata()
const placeholderImgWidth = 20
const imgAspectRatio = meta.width / meta.height
const placeholderImgHeight = Math.round(placeholderImgWidth / imgAspectRatio)
const buffer = await sharpImg.resize(placeholderImgWidth, placeholderImgHeight).toBuffer()
return `data:image/${meta.format};base64,${buffer.toString('base64')}`
}
/* const streamToString = (stream) => {
const chunks = []
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
stream.on('error', (err) => reject(err))
stream.on('end', () => resolve(Buffer.concat(chunks)))
})
}
async function cropPFP(pfpFile: File, username: string, imgId: string) {
if (!imgId)
return 'data:image/webp;base64,UklGRlQAAABXRUJQVlA4IEgAAACwAQCdASoEAAQAAUAmJZgCdAEO9p5AAPa//NFYLcn+a7b+3z7ynq/qXv+iG0yH/y1D9eBf9pqWugq9G0RnxmxwsjaA2bW8AAA='
async function cropPFP(streamItem, username, imgId) {
const { createReadStream } = await streamItem
const pathString = '/var/www/soc_img/img/user'
const fullPath = path.join(pathString, `${username}_${imgId}.png`)
const fullPath = `${pathString}/${username}_${imgId}.png`
await fs.ensureDir(pathString)
const image = await streamToString(createReadStream())
let sharpImage = sharp(image)
let sharpImage = sharp(await pfpFile.arrayBuffer())
const metadata = await sharpImage.metadata()
const { width, height } = metadata
@ -30,36 +40,63 @@ async function cropPFP(streamItem, username, imgId) {
sharpImage = sharpImage.extract(
width > height
? {
left: Math.floor((width - height) / 2),
top: 0,
width: height,
height
}
left: Math.floor((width - height) / 2),
top: 0,
width: height,
height
}
: {
left: 0,
top: Math.floor((height - width) / 2),
width,
height: width
}
left: 0,
top: Math.floor((height - width) / 2),
width,
height: width
}
)
}
await sharpImage.resize({ width: 200, height: 200 }).png().toFile(fullPath)
return await processImage(fullPath)
}*/
}
const resolvers = {
export const mailConfig = JSON.parse(process.env.MAILSERVER || '{}')
export const transporter = nodemailer.createTransport(mailConfig)
async function createForgor(user: User) {
await prismaClient.forgors.deleteMany({ where: { username: user.username } })
const key = generator.generate({
length: 15,
numbers: true,
uppercase: false,
strict: true
})
const row = await prismaClient.forgors.create({ data: { key, username: user.username } })
const html = forgorTemplate.replaceAll('{{forgor_link}}', `https://sittingonclouds.net/forgor?key=${key}`)
const message = {
from: mailConfig.auth.user,
to: user.email,
subject: 'Password Reset',
html
}
await transporter.sendMail(message)
return row
}
const resolversComposition = {
//'Mutation.updateUser': [isAuthedApp]
}
const resolvers: Resolvers = {
Mutation: {
/*registerUser: async (_, { username, email, pfp }, { db }) => {
await Promise.all([
db.models.user.findByPk(username).then((result) => {
if (result) throw UserInputError('Username already in use')
}),
db.models.user.findOne({ where: { email } }).then((result) => {
if (result) throw UserInputError('Email already in use')
})
])
registerUser: async (_, args) => {
const { username, email } = args
const pfp: File = args.pfp
const checkUser = await prismaClient.users.findFirst({ where: { OR: [{ username }, { email }] } })
if (checkUser) throw UserInputError('Username/email already in use')
const password = generator.generate({
length: 30,
@ -68,26 +105,14 @@ const resolvers = {
strict: true
})
return db.transaction(async (transaction) => {
const user = await db.models.user.create(
{ username, email, password: await bcrypt.hash(password, 10) },
{ transaction }
)
if (pfp) {
const imgId = Date.now()
user.placeholder = await cropPFP(pfp, username, imgId)
user.imgId = imgId
} else {
user.placeholder =
'data:image/webp;base64,UklGRlQAAABXRUJQVlA4IEgAAACwAQCdASoEAAQAAUAmJZgCdAEO9p5AAPa//NFYLcn+a7b+3z7ynq/qXv+iG0yH/y1D9eBf9pqWugq9G0RnxmxwsjaA2bW8AAA='
}
const imgId = pfp.size > 0 ? Date.now().toString() : null
const [hash, placeholder] = await Promise.all([bcrypt.hash(password, 10), cropPFP(pfp, username, imgId)])
const user = await prismaClient.users.create({ data: { username, email, password: hash, placeholder, imgId } })
await createForgor(user)
await user.save({ transaction })
await createForgor(user, db, transaction)
return true
})
},
return true
}
/*
updateUserRoles: async (
parent,
{ username, roles },

6
src/graphql/schema.ts Normal file
View file

@ -0,0 +1,6 @@
import { makeExecutableSchema } from '@graphql-tools/schema'
import { typeDefs } from '@/graphql/__generated__/typeDefs.generated'
import resolvers from '@/graphql/resolvers'
const schema = makeExecutableSchema({ typeDefs, resolvers })
export default schema

View file

@ -185,8 +185,8 @@ type Mutation {
updatePublisher(id: ID!, name: String): Publisher!
deletePublisher(id: ID!): Int
createSeries(slug: String, name: String, cover: Upload!): Series!
updateSeries(slug: String, name: String, cover: Upload): Series!
createSeries(slug: String, name: String, cover: File!): Series!
updateSeries(slug: String, name: String, cover: File): Series!
deleteSeries(slug: String!): Int
createGame(
@ -196,7 +196,7 @@ type Mutation {
publishers: [ID]
series: [String]
platforms: [ID]
cover: Upload!
cover: File!
): Game!
updateGame(
releaseDate: String
@ -205,7 +205,7 @@ type Mutation {
publishers: [ID]
series: [String]
platforms: [ID]
cover: Upload
cover: File
): Game!
deleteGame(slug: String!): Int
@ -213,21 +213,21 @@ type Mutation {
updateStudio(slug: String, name: String): Studio!
deleteStudio(slug: String!): Int
createAnimation(title: String, subTitle: String, releaseDate: String, studios: [String], cover: Upload): Animation
createAnimation(title: String, subTitle: String, releaseDate: String, studios: [String], cover: File): Animation
updateAnimation(
id: ID!
title: String
subTitle: String
releaseDate: String
studios: [String]
cover: Upload
cover: File
): Animation
deleteAnimation(id: ID!): Int
createAlbum(
title: String
subTitle: String
cover: Upload
cover: File
releaseDate: String
label: String
vgmdb: String
@ -249,7 +249,7 @@ type Mutation {
id: ID!
title: String
subTitle: String
cover: Upload
cover: File
releaseDate: String
label: String
vgmdb: String

View file

@ -1,4 +1,4 @@
scalar Upload
scalar File
scalar JSON
scalar JSONObject
@ -13,7 +13,7 @@ type Query {
}
type Mutation {
uploadBanner(banner: Upload!): Int
uploadBanner(banner: File!): Int
selectBanner(name: String!): Int
config(name: String!, value: String!): Config!
}

View file

@ -44,12 +44,7 @@ type Mutation {
createForgorLink(key: String!): Boolean!
updatePass(key: String!, pass: String!): Boolean!
updateUser(
username: String
password: String
email: String
pfp: Upload
): Boolean!
updateUser(username: String, password: String, email: String, pfp: File): Boolean!
createRole(name: String!, permissions: [String]!): Role
updateRole(key: String!, name: String!, permissions: [String]!): Role

View file

@ -1,6 +1,7 @@
---
import { languageTag } from '../paraglide/runtime'
import { Toaster } from 'react-hot-toast'
import { languageTag } from '../paraglide/runtime'
import Header from 'components/Header.astro'
import 'styles/global.css'
@ -14,28 +15,18 @@ import 'styles/global.css'
<meta name='theme-color' content='#ffffff' />
<meta property='og:url' content='/' />
<meta
property='og:title'
content='Sitting on Clouds — High Quality soundtrack library'
/>
<meta
property='og:description'
content='Largest Video Game & Animation Soundtrack サウンドトラック Archive'
/>
<meta property='og:title' content='Sitting on Clouds — High Quality soundtrack library' />
<meta property='og:description' content='Largest Video Game & Animation Soundtrack サウンドトラック Archive' />
<meta property='og:image' content='/img/assets/clouds_thumb.png' />
<meta name='generator' content={Astro.generator} />
<meta charset='utf-8' />
<link
rel='alternate'
type='application/rss+xml'
title='Sitting on Clouds'
href={new URL('rss.xml', Astro.site)}
/>
<link rel='alternate' type='application/rss+xml' title='Sitting on Clouds' href={new URL('rss.xml', Astro.site)} />
</head>
<body class='flex flex-col min-h-full m-0 color-'>
<Header />
<main class='flex-1'>
<Toaster client:only='react' toastOptions={{ duration: 6000 }} />
<slot />
</main>
<footer>This is the footer</footer>

15
src/pages/api/graphql.ts Normal file
View file

@ -0,0 +1,15 @@
import type { APIRoute } from 'astro'
import { createYoga } from 'graphql-yoga'
import schema from '@/graphql/schema'
const { handleRequest } = createYoga({
schema,
graphqlEndpoint: '/api/graphql',
fetchAPI: { Request, Response }
})
export const POST: APIRoute = async (context) => {
const { request } = context
return handleRequest(request, context)
}

3
src/types/index.ts Normal file
View file

@ -0,0 +1,3 @@
import type { Dispatch, SetStateAction } from 'react'
export type SetState<T> = Dispatch<SetStateAction<T>>

241
src/utils/forgorTemplate.ts Normal file
View file

@ -0,0 +1,241 @@
const forgorTemplate = `<!DOCTYPE html>
<html lang='en' xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:v='urn:schemas-microsoft-com:vml'>
<head>
<title></title>
<meta charset='utf-8' />
<meta content='width=device-width, initial-scale=1.0' name='viewport' />
<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch><o:AllowPNG/></o:OfficeDocumentSettings></xml><![endif]-->
<!--[if !mso]><!-->
<link href='https://fonts.googleapis.com/css?family=Abril+Fatface' rel='stylesheet' type='text/css' />
<link href='https://fonts.googleapis.com/css?family=Alegreya' rel='stylesheet' type='text/css' />
<link href='https://fonts.googleapis.com/css?family=Arvo' rel='stylesheet' type='text/css' />
<link href='https://fonts.googleapis.com/css?family=Bitter' rel='stylesheet' type='text/css' />
<link href='https://fonts.googleapis.com/css?family=Cabin' rel='stylesheet' type='text/css' />
<link href='https://fonts.googleapis.com/css?family=Ubuntu' rel='stylesheet' type='text/css' />
<!--<![endif]-->
<style>
* {
box-sizing: border-box;
}
html {
height: 100%;
background-color: #f5f5f5;
}
body {
margin: 0;
padding: 0;
}
th.column {
padding: 0
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: inherit !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
}
p {
line-height: inherit
}
@media (max-width:520px) {
.icons-inner {
text-align: center;
}
.icons-inner td {
margin: 0 auto;
}
.row-content {
width: 100% !important;
}
.image_block img.big {
width: auto !important;
}
.stack .column {
width: 100%;
display: block;
}
}
</style>
</head>
<body style='background-color: #FFFFFF; margin: 0; padding: 0; -webkit-text-size-adjust: none; text-size-adjust: none;'>
<table border='0' cellpadding='0' cellspacing='0' class='nl-container' role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #FFFFFF;' width='100%'>
<tbody>
<tr>
<td>
<table align='center' border='0' cellpadding='0' cellspacing='0' class='row row-3'
role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f5f5f5;' width='100%'>
<tbody>
<tr>
<td>
<table align='center' border='0' cellpadding='0' cellspacing='0'
class='row-content stack' role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #ffffff; color: #000000;'
width='500'>
<tbody>
<tr>
<th class='column'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding-top: 0px; padding-bottom: 5px; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;'
width='100%'>
<table border='0' cellpadding='0' cellspacing='0'
class='image_block' role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt;'
width='100%'>
<tr>
<td
style='padding-bottom:5px;padding-left:5px;padding-right:5px;width:100%;'>
<div align='center' style='line-height:10px'><img
alt='reset-password' src='https://sittingonclouds.net/img/assets/clouds.png'
style='display: block; height: auto; border: 0; width: 175px; max-width: 100%;'
title='reset-password' width='175' /></div>
</td>
</tr>
</table>
<table border='0' cellpadding='0' cellspacing='0'
class='heading_block' role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt;'
width='100%'>
<tr>
<td style='text-align:center;width:100%;'>
<h1
style='margin: 0; color: #393d47; direction: ltr; font-family: Tahoma, Verdana, Segoe, sans-serif; font-size: 25px; font-weight: normal; letter-spacing: normal; line-height: 120%; text-align: center; margin-top: 0; margin-bottom: 0;'>
<strong>Forgot your password?</strong></h1>
</td>
</tr>
</table>
<table border='0' cellpadding='10' cellspacing='0'
class='text_block' role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;'
width='100%'>
<tr>
<td>
<div style='font-family: Tahoma, Verdana, sans-serif'>
<div
style='font-size: 12px; font-family: Tahoma, Verdana, Segoe, sans-serif; mso-line-height-alt: 18px; color: #393d47; line-height: 1.5;'>
<p
style='margin: 0; font-size: 14px; text-align: center; mso-line-height-alt: 21px;'>
<span style='font-size:14px;'><span
style=''>Not to worry, we got you!
</span><span style=''>Lets get you a
new password.</span></span></p>
</div>
</div>
</td>
</tr>
</table>
<table border='0' cellpadding='15' cellspacing='0'
class='button_block' role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt;'
width='100%'>
<tr>
<td>
<div align='center'>
<!--[if mso]><v:roundrect xmlns:v='urn:schemas-microsoft-com:vml' xmlns:w='urn:schemas-microsoft-com:office:word' href='{{forgor_link}}' style='height:58px;width:272px;v-text-anchor:middle;' arcsize='35%' strokeweight='0.75pt' strokecolor='#FFC727' fillcolor='#e38e36'><w:anchorlock/><v:textbox inset='0px,0px,0px,0px'><center style='color:#393d47; font-family:Tahoma, Verdana, sans-serif; font-size:18px'><![endif]--><a
href='{{forgor_link}}'
style='text-decoration:none;display:inline-block;color:#393d47;background-color:#ff7b24;border-radius:20px;width:auto;padding-top:10px;padding-bottom:10px;font-family:Tahoma, Verdana, Segoe, sans-serif;text-align:center;mso-border-alt:none;word-break:keep-all;'
target='_blank'><span
style='padding-left:50px;padding-right:50px;font-size:18px;display:inline-block;letter-spacing:normal;'><span
style='font-size: 16px; line-height: 2; word-break: break-word; mso-line-height-alt: 32px;'><span
data-mce-style='font-size: 18px; line-height: 36px;'
style='font-size: 18px; line-height: 36px;'><strong>RESET
PASSWORD</strong></span></span></span></a>
<!--[if mso]></center></v:textbox></v:roundrect><![endif]-->
</div>
</td>
</tr>
</table>
<table border='0' cellpadding='0' cellspacing='0' class='text_block'
role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;'
width='100%'>
<tr>
<td
style='padding-bottom:5px;padding-left:10px;padding-right:10px;padding-top:10px;'>
<div style='font-family: Tahoma, Verdana, sans-serif'>
<div
style='font-size: 12px; font-family: Tahoma, Verdana, Segoe, sans-serif; text-align: center; mso-line-height-alt: 18px; color: #393d47; line-height: 1.5;'>
<p
style='margin: 0; mso-line-height-alt: 19.5px;'>
<span style='font-size:13px;'>If you didnt
request to change your password, simply
ignore this email.</span></p>
</div>
</div>
</td>
</tr>
</table>
</th>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table align='center' border='0' cellpadding='0' cellspacing='0' class='row row-5'
role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f5f5f5;' width='100%'>
<tbody>
<tr>
<td>
<table align='center' border='0' cellpadding='0' cellspacing='0'
class='row-content stack' role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #000000;'
width='500'>
<tbody>
<tr>
<th class='column'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding-top: 5px; padding-bottom: 5px; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;'
width='100%'>
<table border='0' cellpadding='15' cellspacing='0'
class='text_block' role='presentation'
style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;'
width='100%'>
<tr>
<td>
<div style='font-family: Tahoma, Verdana, sans-serif'>
<div
style='font-size: 12px; font-family: Tahoma, Verdana, Segoe, sans-serif; mso-line-height-alt: 14.399999999999999px; color: #393d47; line-height: 1.2;'>
<p
style='margin: 0; font-size: 14px; text-align: center;'>
<span style='font-size:10px;'>This link will
expire in 24 hours</span></p>
</div>
</div>
</td>
</tr>
</table>
</th>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table><!-- End -->
</body>
</html>`
export default forgorTemplate

View file

@ -1,6 +1,5 @@
import User from "sequelize/models/user";
import type { Session } from "@auth/core";
// import User from "sequelize/models/user";
import type { Session } from '@auth/core'
export async function getUser(session: Session | null) {
if (!session) return null
@ -8,4 +7,26 @@ export async function getUser(session: Session | null) {
const { id } = session
const user = await User.findByPk(id)
return user
}
}
const isAuthed = (next) => async (root, args, context, info) => {
const session = await getSession()
const { username = null } = session
if (!username) throw AuthenticationError()
return next(root, args, context, info)
}
const hasPerm = (perm) => (next) => async (root, args, context, info) => {
const { db } = context
const user = await getUser(db)
const roles = await user.getRoles()
const permissions = roles.map((r) => r.permissions).flat()
if (!permissions.includes(perm)) throw ForbiddenError()
return next(root, args, context, info)
}
consoley.log()
export const hasRole = (role) => [isAuthedApp, hasPermApp(role)]