From 74b1799d21b513062c0b72cb69403d7f47a58bb1 Mon Sep 17 00:00:00 2001 From: diced Date: Mon, 24 Oct 2022 11:28:06 -0700 Subject: [PATCH] feat: user registration without oauth --- src/lib/config/Config.ts | 1 + src/lib/config/readConfig.ts | 1 + src/lib/config/validateConfig.ts | 5 +- src/lib/middleware/getServerSideProps.ts | 1 + src/pages/api/auth/create.ts | 35 ++-- src/pages/auth/login.tsx | 12 +- src/pages/auth/register.tsx | 225 +++++++++++++++++++---- src/pages/invite/[code].tsx | 183 ------------------ 8 files changed, 223 insertions(+), 240 deletions(-) delete mode 100644 src/pages/invite/[code].tsx diff --git a/src/lib/config/Config.ts b/src/lib/config/Config.ts index c77dffe..ce21898 100644 --- a/src/lib/config/Config.ts +++ b/src/lib/config/Config.ts @@ -102,6 +102,7 @@ export interface ConfigDiscordEmbed { export interface ConfigFeatures { invites: boolean; oauth_registration: boolean; + user_registration: boolean; } export interface ConfigOAuth { diff --git a/src/lib/config/readConfig.ts b/src/lib/config/readConfig.ts index 6ce8e63..e39bca4 100644 --- a/src/lib/config/readConfig.ts +++ b/src/lib/config/readConfig.ts @@ -121,6 +121,7 @@ export default function readConfig() { map('FEATURES_INVITES', 'boolean', 'features.invites'), map('FEATURES_OAUTH_REGISTRATION', 'boolean', 'features.oauth_registration'), + map('FEATURES_USER_REGISTRATION', 'boolean', 'features.user_registration'), ]; const config = {}; diff --git a/src/lib/config/validateConfig.ts b/src/lib/config/validateConfig.ts index 00df718..cc2e6d7 100644 --- a/src/lib/config/validateConfig.ts +++ b/src/lib/config/validateConfig.ts @@ -163,10 +163,11 @@ const validator = s.object({ .nullish.default(null), features: s .object({ - invites: s.boolean.default(true), + invites: s.boolean.default(false), oauth_registration: s.boolean.default(false), + user_registration: s.boolean.default(false), }) - .default({ invites: true, oauth_registration: false }), + .default({ invites: false, oauth_registration: false, user_registration: false }), }); export default function validate(config): Config { diff --git a/src/lib/middleware/getServerSideProps.ts b/src/lib/middleware/getServerSideProps.ts index 4413887..f45a963 100644 --- a/src/lib/middleware/getServerSideProps.ts +++ b/src/lib/middleware/getServerSideProps.ts @@ -27,6 +27,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { external_links: JSON.stringify(config.website.external_links), disable_media_preview: config.website.disable_media_preview, invites: config.features.invites, + user_registration: config.features.user_registration, oauth_registration: config.features.oauth_registration, oauth_providers: JSON.stringify(oauth_providers), }, diff --git a/src/pages/api/auth/create.ts b/src/pages/api/auth/create.ts index 870bacd..2a097fa 100644 --- a/src/pages/api/auth/create.ts +++ b/src/pages/api/auth/create.ts @@ -5,18 +5,19 @@ import Logger from 'lib/logger'; import config from 'lib/config'; async function handler(req: NextApiReq, res: NextApiRes) { - if (req.method === 'POST' && req.body && req.body.code) { - if (!config.features.invites) return res.forbid('invites are disabled'); + if (req.method === 'POST' && req.body) { + if (!config.features.invites && req.body.code) return res.forbid('invites are disabled'); + if (!config.features.user_registration) return res.forbid('user registration is disabled'); const { code, username, password } = req.body as { - code: string; + code?: string; username: string; password: string; }; const invite = await prisma.invite.findUnique({ - where: { code }, + where: { code: code ?? '' }, }); - if (!invite) return res.bad('invalid invite code'); + if (!invite && code) return res.bad('invalid invite code'); const user = await prisma.user.findFirst({ where: { username }, @@ -33,16 +34,22 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, }); - await prisma.invite.update({ - where: { - code, - }, - data: { - used: true, - }, - }); + if (code) { + await prisma.invite.update({ + where: { + code, + }, + data: { + used: true, + }, + }); + } - Logger.get('user').info(`Created user ${newUser.username} (${newUser.id}) from invite code ${code}`); + Logger.get('user').info( + `Created user ${newUser.username} (${newUser.id}) ${ + code ? `from invite code ${code}` : 'via registration' + }` + ); return res.json({ success: true }); } diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index 43954c7..c95b1fa 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -8,7 +8,7 @@ import Head from 'next/head'; import { GitHubIcon, DiscordIcon } from 'components/icons'; export { getServerSideProps } from 'middleware/getServerSideProps'; -export default function Login({ title, oauth_registration, oauth_providers: unparsed }) { +export default function Login({ title, user_registration, oauth_registration, oauth_providers: unparsed }) { const router = useRouter(); const oauth_providers = JSON.parse(unparsed); @@ -75,6 +75,16 @@ export default function Login({ title, oauth_registration, oauth_providers: unpa Login + {user_registration && ( + <> + + + + + + )} {oauth_registration && ( <> diff --git a/src/pages/auth/register.tsx b/src/pages/auth/register.tsx index 945e402..2fe828a 100644 --- a/src/pages/auth/register.tsx +++ b/src/pages/auth/register.tsx @@ -1,58 +1,203 @@ -import { Button, Center } from '@mantine/core'; -import Link from 'next/link'; +import { GetServerSideProps } from 'next'; +import prisma from 'lib/prisma'; +import { useState } from 'react'; +import { Box, Button, Card, Center, Group, PasswordInput, Stepper, TextInput } from '@mantine/core'; +import useFetch from 'hooks/useFetch'; +import PasswordStrength from 'components/PasswordStrength'; +import { showNotification } from '@mantine/notifications'; +import { CrossIcon, UserIcon } from 'components/icons'; import { useRouter } from 'next/router'; -import { useEffect } from 'react'; -import GitHubIcon from 'components/icons/GitHubIcon'; -import DiscordIcon from 'components/icons/DiscordIcon'; import Head from 'next/head'; -export { getServerSideProps } from 'middleware/getServerSideProps'; +import config from 'lib/config'; +import { useSetRecoilState } from 'recoil'; +import { userSelector } from 'lib/recoil/user'; +import { randomChars } from 'lib/util'; -export default function Login({ title, oauth_registration, oauth_providers: unparsed }) { - const oauth_providers = JSON.parse(unparsed); +export default function Register({ code, title, user_registration }) { + const [active, setActive] = useState(0); + const [username, setUsername] = useState(''); + const [usernameError, setUsernameError] = useState(''); + const [password, setPassword] = useState(''); + const [verifyPassword, setVerifyPassword] = useState(''); + const [verifyPasswordError, setVerifyPasswordError] = useState(''); + const [strength, setStrength] = useState(0); - const icons = { - GitHub: GitHubIcon, - Discord: DiscordIcon, + const setUser = useSetRecoilState(userSelector); + const router = useRouter(); + + const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current)); + const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); + + const checkUsername = async () => { + setUsername(username.trim()); + + setUsernameError(''); + + const res = await useFetch('/api/users', 'POST', { code, username }); + if (res.error) { + setUsernameError('A user with that username already exists'); + } else { + setUsernameError(''); + } }; - for (const provider of oauth_providers) { - provider.Icon = icons[provider.name]; - } + const checkPassword = () => { + setVerifyPasswordError(''); + setPassword(password.trim()); + setVerifyPassword(verifyPassword.trim()); - const router = useRouter(); - if (!oauth_registration) { - router.push('/auth/login'); - return null; - } + if (password.trim() !== verifyPassword.trim()) { + setVerifyPasswordError('Passwords do not match'); + } + }; - useEffect(() => { - (async () => { - const a = await fetch('/api/user'); - if (a.ok) await router.push('/dashboard'); - })(); - }, []); + const createUser = async () => { + const res = await useFetch('/api/auth/create', 'POST', { + code: user_registration ? null : code, + username, + password, + }); + if (res.error) { + showNotification({ + title: 'Error while creating user', + message: res.error, + color: 'red', + icon: , + }); + } else { + showNotification({ + title: 'User created', + message: 'You will be logged in shortly...', + color: 'green', + icon: , + }); + + setUser(null); + await useFetch('/api/auth/logout'); + await useFetch('/api/auth/login', 'POST', { + username, + password, + }); + + router.push('/dashboard'); + } + }; return ( <> - {title} - Login + + {title} - Invite ({code}) +
-
- - - - {oauth_providers.map(({ url, name, Icon }, i) => ( - - - - ))} -
+ ({ + backgroundColor: t.colors.dark[6], + borderRadius: t.radius.sm, + })} + p='md' + > + + 0}> + setUsername(e.target.value)} + error={usernameError} + onBlur={() => checkUsername()} + /> + + + + + 1 && usernameError === ''}> + + + + + + + 2}> + setVerifyPassword(e.target.value)} + error={verifyPasswordError} + onBlur={() => checkPassword()} + /> + + + + + + + + + + + + +
); } + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { code } = context.query as { code: string }; + + if (!config.features.invites && code) + return { + notFound: true, + }; + + if (!config.features.user_registration) return { notFound: true }; + + if (code) { + const invite = await prisma.invite.findUnique({ + where: { + code, + }, + }); + + if (!invite) return { notFound: true }; + if (invite.used) return { notFound: true }; + + if (invite.expires_at && invite.expires_at < new Date()) return { notFound: true }; + + return { + props: { + title: config.website.title, + code: invite.code, + }, + }; + } else { + const code = randomChars(4); + await prisma.invite.create({ + data: { + code, + createdById: 1, + }, + }); + return { + props: { + title: config.website.title, + code, + user_registration: true, + }, + }; + } +}; diff --git a/src/pages/invite/[code].tsx b/src/pages/invite/[code].tsx deleted file mode 100644 index 768298f..0000000 --- a/src/pages/invite/[code].tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { GetServerSideProps } from 'next'; -import prisma from 'lib/prisma'; -import { useState } from 'react'; -import { Box, Button, Card, Center, Group, PasswordInput, Stepper, TextInput } from '@mantine/core'; -import useFetch from 'hooks/useFetch'; -import PasswordStrength from 'components/PasswordStrength'; -import { showNotification } from '@mantine/notifications'; -import { CrossIcon, UserIcon } from 'components/icons'; -import { useRouter } from 'next/router'; -import Head from 'next/head'; -import config from 'lib/config'; -import { useSetRecoilState } from 'recoil'; -import { userSelector } from 'lib/recoil/user'; - -export default function Invite({ code, title }) { - const [active, setActive] = useState(0); - const [username, setUsername] = useState(''); - const [usernameError, setUsernameError] = useState(''); - const [password, setPassword] = useState(''); - const [verifyPassword, setVerifyPassword] = useState(''); - const [verifyPasswordError, setVerifyPasswordError] = useState(''); - const [strength, setStrength] = useState(0); - - const setUser = useSetRecoilState(userSelector); - const router = useRouter(); - - const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current)); - const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); - - const checkUsername = async () => { - setUsername(username.trim()); - - setUsernameError(''); - - const res = await useFetch('/api/users', 'POST', { code, username }); - if (res.error) { - setUsernameError('A user with that username already exists'); - } else { - setUsernameError(''); - } - }; - - const checkPassword = () => { - setVerifyPasswordError(''); - setPassword(password.trim()); - setVerifyPassword(verifyPassword.trim()); - - if (password.trim() !== verifyPassword.trim()) { - setVerifyPasswordError('Passwords do not match'); - } - }; - - const createUser = async () => { - const res = await useFetch('/api/auth/create', 'POST', { - code, - username, - password, - }); - if (res.error) { - showNotification({ - title: 'Error while creating user', - message: res.error, - color: 'red', - icon: , - }); - } else { - showNotification({ - title: 'User created', - message: 'You will be logged in shortly...', - color: 'green', - icon: , - }); - - setUser(null); - await useFetch('/api/auth/logout'); - await useFetch('/api/auth/login', 'POST', { - username, - password, - }); - - router.push('/dashboard'); - } - }; - - return ( - <> - - - {title} - Invite ({code}) - - -
- ({ - backgroundColor: t.colors.dark[6], - borderRadius: t.radius.sm, - })} - p='md' - > - - 0}> - setUsername(e.target.value)} - error={usernameError} - onBlur={() => checkUsername()} - /> - - - - - 1 && usernameError === ''}> - - - - - - - 2}> - setVerifyPassword(e.target.value)} - error={verifyPasswordError} - onBlur={() => checkPassword()} - /> - - - - - - - - - - - - - -
- - ); -} - -export const getServerSideProps: GetServerSideProps = async (context) => { - if (!config.features.invites) - return { - notFound: true, - }; - - const { code } = context.query as { code: string }; - - const invite = await prisma.invite.findUnique({ - where: { - code, - }, - }); - - if (!invite) return { notFound: true }; - if (invite.used) return { notFound: true }; - - if (invite.expires_at && invite.expires_at < new Date()) return { notFound: true }; - - return { - props: { - title: config.website.title, - code: invite.code, - }, - }; -};