mirror of
https://github.com/jorgev259/soc_site-astro.git
synced 2025-06-29 07:57:41 +00:00
Implement register form
This commit is contained in:
parent
adeb3fd3bf
commit
cdcd71cf2a
17 changed files with 526 additions and 99 deletions
|
|
@ -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
33
src/components/Modal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
86
src/components/header/RegisterButton.tsx
Normal file
86
src/components/header/RegisterButton.tsx
Normal 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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
6
src/graphql/schema.ts
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
15
src/pages/api/graphql.ts
Normal 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
3
src/types/index.ts
Normal 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
241
src/utils/forgorTemplate.ts
Normal 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=''>Let’s 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 didn’t
|
||||
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
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue