mirror of
https://github.com/dsoaress/link-free.git
synced 2026-08-16 22:46:04 +01:00
refactor: api routes
This commit is contained in:
@@ -30,9 +30,7 @@
|
||||
"next-connect": "0.11.0",
|
||||
"nookies": "2.5.2",
|
||||
"polished": "4.1.3",
|
||||
"rc-slider": "9.7.5",
|
||||
"react": "17.0.2",
|
||||
"react-collapsible": "2.8.4",
|
||||
"react-color": "2.19.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-hot-toast": "^2.1.1",
|
||||
|
||||
@@ -87,6 +87,7 @@ async function main() {
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: 1,
|
||||
username: USERNAME,
|
||||
password: hashSync(PASSWORD, 10),
|
||||
lang: 'en',
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { compare } from 'bcryptjs'
|
||||
|
||||
import { prisma } from 'services/prisma'
|
||||
import { createSession } from 'utils/createSession'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { username, password } = req.body
|
||||
|
||||
if (!username || !password) {
|
||||
throw new ExceptionError('Username and password are required')
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new ExceptionError('Credentials are invalid', 401)
|
||||
}
|
||||
|
||||
const isValidPassword = await compare(password, user.password)
|
||||
|
||||
if (!isValidPassword) {
|
||||
throw new ExceptionError('Credentials are invalid', 401)
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken } = await createSession(user)
|
||||
|
||||
res.status(200).json({ accessToken, refreshToken })
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { prisma } from 'services/prisma'
|
||||
import { createSession } from 'utils/createSession'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function refreshToken(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { refreshToken } = req.body
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new ExceptionError('Refresh Token required')
|
||||
}
|
||||
|
||||
const session = await prisma.session.findUnique({
|
||||
where: { id: refreshToken }
|
||||
})
|
||||
|
||||
if (!session) {
|
||||
throw new ExceptionError('Refresh Token are invalid', 401)
|
||||
}
|
||||
|
||||
const isSessionExpired = new Date(session.expiresAt) < new Date()
|
||||
|
||||
if (isSessionExpired) {
|
||||
await prisma.session.delete({
|
||||
where: { id: refreshToken }
|
||||
})
|
||||
|
||||
throw new ExceptionError('Refresh Token are expired', 401)
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.userId }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new ExceptionError('User not found', 401)
|
||||
}
|
||||
|
||||
const tokens = await createSession(user)
|
||||
|
||||
await prisma.session.delete({
|
||||
where: { id: refreshToken }
|
||||
})
|
||||
|
||||
res.status(200).json({ accessToken: tokens.accessToken, refreshToken: tokens.refreshToken })
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function getData(_req: NextApiRequest, res: NextApiResponse) {
|
||||
const response = await prisma.data.findUnique({
|
||||
where: { id: 1 }
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new ExceptionError('No data found')
|
||||
}
|
||||
|
||||
res.status(200).json(response?.data)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function updateData(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { data } = req.body
|
||||
|
||||
if (!data) {
|
||||
throw new ExceptionError('No data provided')
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await prisma.data.update({
|
||||
where: { id: 1 },
|
||||
data: { data: JSON.stringify(req.body.data) }
|
||||
})
|
||||
|
||||
res.status(200).json(response)
|
||||
} catch (err: any) {
|
||||
throw new ExceptionError(err)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export { auth } from './auth'
|
||||
export { refreshToken } from './auth/refreshToken'
|
||||
|
||||
export { getData } from './data/getData'
|
||||
export { updateData } from './data/updateData'
|
||||
|
||||
export { checkUsername } from './users/checkUsername'
|
||||
export { createUser } from './users/createUser'
|
||||
export { deleteUser } from './users/deleteUser'
|
||||
export { getUserById } from './users/getUserById'
|
||||
export { getUsers } from './users/getUsers'
|
||||
export { me } from './users/me'
|
||||
export { updateUser } from './users/updateUser'
|
||||
@@ -1,13 +0,0 @@
|
||||
import { prisma } from 'services/prisma'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function checkUsername(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { username } = req.body
|
||||
|
||||
const isTaken = await prisma.user.findUnique({
|
||||
where: { username: username.toLowerCase().trim() }
|
||||
})
|
||||
|
||||
res.status(200).json({ isTaken: !!isTaken })
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { hashSync } from 'bcryptjs'
|
||||
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function createUser(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { username, password, role } = req.body
|
||||
const normalizedUsername = username.toLowerCase().trim()
|
||||
|
||||
if (!username || !password) {
|
||||
throw new ExceptionError('Username and password are required')
|
||||
}
|
||||
|
||||
if (!['ADMIN', 'EDITOR'].includes(role)) {
|
||||
throw new ExceptionError('Role must be either ADMIN or EDITOR')
|
||||
}
|
||||
|
||||
if (req.userRole !== 'ADMIN') {
|
||||
throw new ExceptionError('Only admins can create users')
|
||||
}
|
||||
|
||||
const usernameExists = await prisma.user.findUnique({
|
||||
where: { username: normalizedUsername }
|
||||
})
|
||||
|
||||
if (usernameExists) {
|
||||
throw new ExceptionError('Username already exists')
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await prisma.user.create({
|
||||
data: {
|
||||
username: normalizedUsername,
|
||||
password: hashSync(password, 10),
|
||||
role: role ? role : 'EDITOR'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
res.status(200).json(response)
|
||||
} catch (err: any) {
|
||||
throw new ExceptionError(err)
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function deleteUser(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { id } = req.params
|
||||
|
||||
if (id === '1') {
|
||||
throw new ExceptionError('Cannot delete admin', 403)
|
||||
}
|
||||
|
||||
if (req.userRole !== 'ADMIN') {
|
||||
throw new ExceptionError('Only admins can delete other users')
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.user.delete({
|
||||
where: { id: +id }
|
||||
})
|
||||
|
||||
res.status(204).end()
|
||||
} catch (err: any) {
|
||||
throw new ExceptionError(err)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function getUserById(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { id } = req.params
|
||||
|
||||
const response = await prisma.user.findUnique({
|
||||
where: { id: +id },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new ExceptionError('No data found', 404)
|
||||
}
|
||||
|
||||
res.status(200).json(response)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function getUsers(_req: NextApiRequest, res: NextApiResponse) {
|
||||
const response = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new ExceptionError('No data found')
|
||||
}
|
||||
|
||||
res.status(200).json(response)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function me(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { userId } = req
|
||||
const response = await prisma.user.findUnique({
|
||||
where: { id: +userId },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
lang: true,
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new ExceptionError('User not found', 404)
|
||||
}
|
||||
|
||||
res.status(200).json(response)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { compare, hashSync } from 'bcryptjs'
|
||||
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function updateUser(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { id } = req.params
|
||||
const { oldPassword, password, role } = req.body
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: +id }
|
||||
})
|
||||
|
||||
let updatedUser = {}
|
||||
|
||||
if (!user) {
|
||||
throw new ExceptionError('No user found', 404)
|
||||
}
|
||||
|
||||
// if (req.userRole !== 'ADMIN' || req.userId !== id) {
|
||||
// throw new ExceptionError('Only admins can update other users')
|
||||
// }
|
||||
|
||||
if (oldPassword && password) {
|
||||
const isValidPassword = await compare(oldPassword, user.password)
|
||||
|
||||
if (!isValidPassword) {
|
||||
throw new ExceptionError('Password does not match')
|
||||
}
|
||||
|
||||
updatedUser = { password: hashSync(password, 10) }
|
||||
}
|
||||
|
||||
if (role) {
|
||||
if (!['ADMIN', 'EDITOR'].includes(role)) {
|
||||
throw new ExceptionError('Role must be either ADMIN or EDITOR')
|
||||
}
|
||||
|
||||
if (user.role === 'ADMIN' && role === 'EDITOR') {
|
||||
throw new ExceptionError('Cannot downgrade admin role', 403)
|
||||
}
|
||||
|
||||
if (role !== 'EDITOR' && user.role === 'EDITOR') {
|
||||
throw new ExceptionError('Cannot change role', 403)
|
||||
}
|
||||
|
||||
updatedUser = { ...updatedUser, role }
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await prisma.user.update({
|
||||
where: { id: +id },
|
||||
data: updatedUser,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
lang: true,
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
res.status(200).json(response)
|
||||
} catch (err: any) {
|
||||
throw new ExceptionError(err)
|
||||
}
|
||||
}
|
||||
@@ -10,27 +10,28 @@ type ButtonProps = {
|
||||
}
|
||||
|
||||
export const Button = styled(BaseButton)<ButtonProps>`
|
||||
background: #0284c7;
|
||||
border-color: #0284c7;
|
||||
background: #000;
|
||||
border-color: #000;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(0);
|
||||
background: ${({ outlined }) => (outlined ? '#0284c7' : darken(0.1, '#0284c7'))};
|
||||
border-color: ${({ outlined }) => (outlined ? '#0284c7' : darken(0.1, '#0284c7'))};
|
||||
background: ${({ outlined }) => (outlined ? '#000' : lighten(0.3, '#000'))};
|
||||
border-color: ${({ outlined }) => (outlined ? '#000' : lighten(0.3, '#000'))};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${({ outlined }) => (outlined ? '#0284c7' : darken(0.2, '#0284c7'))};
|
||||
border-color: ${({ outlined }) => (outlined ? '#0284c7' : darken(0.2, '#0284c7'))};
|
||||
background: ${({ outlined }) => (outlined ? '#000' : lighten(0.4, '#000'))};
|
||||
border-color: ${({ outlined }) => (outlined ? '#000' : lighten(0.4, '#000'))};
|
||||
}
|
||||
|
||||
${({ outlined }) =>
|
||||
outlined &&
|
||||
`
|
||||
color: #0284c7;
|
||||
color: #000;
|
||||
background: transparent;
|
||||
border-color: #0284c7;
|
||||
border-color: #000;
|
||||
|
||||
&:hover {
|
||||
color: #f8fafc;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Textarea } from 'components/Textarea'
|
||||
import { useData } from 'hooks/useData'
|
||||
|
||||
export function DescriptionInput() {
|
||||
const { data, setData } = useData()
|
||||
|
||||
return (
|
||||
<textarea
|
||||
style={{ height: '200px !important', width: '100% !important' }}
|
||||
<Textarea
|
||||
label="Description"
|
||||
value={data.settings.description}
|
||||
onChange={e =>
|
||||
setData({
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import { useData } from 'hooks/useData'
|
||||
import { FaFont } from 'react-icons/fa'
|
||||
|
||||
import type { Fonts } from 'types/Fonts'
|
||||
import { useData } from 'hooks/useData'
|
||||
import { fonts } from 'constants/fonts'
|
||||
import { Select } from 'components/Select'
|
||||
|
||||
import type { Fonts as FontsType } from 'types/Fonts'
|
||||
|
||||
export function FontsSelect() {
|
||||
const { data, setData } = useData()
|
||||
|
||||
return (
|
||||
<select
|
||||
<Select
|
||||
label="Font select"
|
||||
icon={FaFont}
|
||||
value={data.settings.font}
|
||||
onChange={e =>
|
||||
setData({
|
||||
...data,
|
||||
settings: {
|
||||
...data.settings,
|
||||
font: e.target.value as Fonts
|
||||
font: e.target.value as FontsType
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="inconsolata">Inconsolata</option>
|
||||
<option value="oswald">Oswald</option>
|
||||
<option value="poppins">Poppins</option>
|
||||
<option value="quicksand">Quicksand</option>
|
||||
<option value="roboto">Roboto</option>
|
||||
<option value="robotoSlab">Roboto Slab</option>
|
||||
<option value="ubuntu">Ubuntu</option>
|
||||
</select>
|
||||
options={Object.entries(fonts).map(([value, { name: label }]) => ({ label, value }))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'rc-slider/assets/index.css'
|
||||
|
||||
import Slider from 'rc-slider'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useData } from 'hooks/useData'
|
||||
import { DescriptionInput } from 'components/Dash/DescriptionInput'
|
||||
import { FontsSelect } from 'components/Dash/FontsSelect'
|
||||
import { NameInput } from 'components/Dash/NameInput'
|
||||
import { Input } from 'components/Input'
|
||||
|
||||
export function DataSettings() {
|
||||
const { data, setData } = useData()
|
||||
const [number, setNumber] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -18,23 +18,22 @@ export function DataSettings() {
|
||||
<FontsSelect />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Slider
|
||||
value={+data.settings.buttonBorderRadius}
|
||||
<Input
|
||||
label="Border radius"
|
||||
type="number"
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
onChange={value =>
|
||||
value={+data.settings.buttonBorderRadius}
|
||||
onChange={e =>
|
||||
setData({
|
||||
...data,
|
||||
settings: {
|
||||
...data.settings,
|
||||
buttonBorderRadius: String(value)
|
||||
buttonBorderRadius: e.target.value
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Input } from 'components/Input'
|
||||
import { useData } from 'hooks/useData'
|
||||
|
||||
export function NameInput() {
|
||||
const { data, setData } = useData()
|
||||
|
||||
return (
|
||||
<input
|
||||
<Input
|
||||
label="Name"
|
||||
value={data.settings.name}
|
||||
onChange={e =>
|
||||
setData({
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useData } from 'hooks/useData'
|
||||
import { api } from 'services/api'
|
||||
import { removeLocalStorage } from 'utils/localStorage'
|
||||
import { Button } from 'components/Button'
|
||||
import { useI18n } from 'hooks/useI18n'
|
||||
|
||||
import { Button } from '../Button'
|
||||
import { AlertModal } from '../AlertModal'
|
||||
|
||||
import { ButtonsGroup, Wrapper } from './styles'
|
||||
|
||||
@@ -23,17 +28,25 @@ export function SaveChangesAlert({
|
||||
}: SaveChangesAlertProps) {
|
||||
const { push } = useRouter()
|
||||
const { data, setData } = useData()
|
||||
const { t } = useI18n()
|
||||
const [isDeleteUnsavedDataModalOpen, setIsDeleteUnsavedDataModalOpen] = useState(false)
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await api.put('data', { data })
|
||||
const handleSave = () => {
|
||||
const saving = toast.loading(t.common.saving)
|
||||
|
||||
api
|
||||
.put('data', { data })
|
||||
.then(() => {
|
||||
removeLocalStorage('data')
|
||||
setHasUnsavedChanges(false)
|
||||
toast.success(t.userSection.editUserModal.success, { id: saving })
|
||||
setData(data)
|
||||
push('/')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
toast.error(t.userSection.editUserModal.error, { id: saving })
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -43,10 +56,16 @@ export function SaveChangesAlert({
|
||||
}
|
||||
|
||||
return hasUnsavedChanges ? (
|
||||
<>
|
||||
<Wrapper>
|
||||
You have unsaved changes.
|
||||
<ButtonsGroup>
|
||||
<Button size="small" backgroundColor="transparent" onClick={() => handleCancel()}>
|
||||
<Button
|
||||
size="small"
|
||||
outlined
|
||||
danger
|
||||
onClick={() => setIsDeleteUnsavedDataModalOpen(true)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="small" onClick={() => handleSave()}>
|
||||
@@ -54,5 +73,14 @@ export function SaveChangesAlert({
|
||||
</Button>
|
||||
</ButtonsGroup>
|
||||
</Wrapper>
|
||||
|
||||
<AlertModal
|
||||
title={t.userSection.deleteUserModal.title}
|
||||
description={'teste'}
|
||||
isOpen={isDeleteUnsavedDataModalOpen}
|
||||
onClose={() => setIsDeleteUnsavedDataModalOpen(false)}
|
||||
callback={() => handleCancel()}
|
||||
/>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ export const Wrapper = styled.div`
|
||||
padding: 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1;
|
||||
background-color: #ddd;
|
||||
z-index: 10;
|
||||
`
|
||||
|
||||
export const ButtonsGroup = styled.div`
|
||||
|
||||
@@ -15,7 +15,7 @@ import { InputsGroup, Wrapper } from './styles'
|
||||
|
||||
import type { User } from 'types/User'
|
||||
import type { Role } from '@prisma/client'
|
||||
import type { FormEvent, Dispatch, SetStateAction } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
|
||||
type EditUserModalProps = {
|
||||
isOpen: boolean
|
||||
@@ -138,7 +138,7 @@ export function EditUserModal({ isOpen, onClose, user }: EditUserModalProps) {
|
||||
autoComplete="new-password"
|
||||
errorMessage={errors.password}
|
||||
/>
|
||||
{user.id !== 1 && (
|
||||
{user?.id !== 1 && (
|
||||
<Can roles={['ADMIN']}>
|
||||
<Select
|
||||
label={t.userSection.common.role}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isEqual } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Collapsible from 'react-collapsible'
|
||||
|
||||
import { useData } from 'hooks/useData'
|
||||
import { getLocalStorage, setLocalStorage } from 'utils/localStorage'
|
||||
@@ -8,10 +7,12 @@ import { Home } from 'components/Home'
|
||||
import { ColorsSettings } from 'components/Dash/ColorsSettings'
|
||||
import { DataSettings } from 'components/Dash/NameInput/DataSettings'
|
||||
import { SaveChangesAlert } from 'components/Dash/SaveChangesAlert'
|
||||
import { useAuth } from 'hooks/useAuth'
|
||||
|
||||
import { Content, Preview, Wrapper } from './styles'
|
||||
import { UserSettings } from './UserSettings'
|
||||
import { SocialLinksSettings } from './SocialLinksSettings'
|
||||
import { Button } from './Button'
|
||||
|
||||
import type { Data } from 'types/Data'
|
||||
|
||||
@@ -21,6 +22,7 @@ type DashProps = {
|
||||
|
||||
export function Dash({ initialData }: DashProps) {
|
||||
const { data, setData } = useData()
|
||||
const { signOut } = useAuth()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
|
||||
|
||||
@@ -28,8 +30,6 @@ export function Dash({ initialData }: DashProps) {
|
||||
if (isLoading) {
|
||||
const storageData = getLocalStorage<Data>('data')
|
||||
if (storageData) setData(storageData)
|
||||
else setData(initialData)
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
@@ -50,16 +50,14 @@ export function Dash({ initialData }: DashProps) {
|
||||
initialData={initialData}
|
||||
/>
|
||||
<h1>Dash</h1>
|
||||
<Button outlined size="small" onClick={() => signOut()}>
|
||||
Logout
|
||||
</Button>
|
||||
|
||||
<UserSettings />
|
||||
{/* <SocialLinksSettings /> */}
|
||||
{/* <DataSettings /> */}
|
||||
{/* <ColorsSettings />
|
||||
|
||||
<Collapsible trigger="Start here">
|
||||
<p>This is the collapsible content. It can be any element or React component you like.</p>
|
||||
<p>It can even be another Collapsible component. Check out the next section!</p>
|
||||
</Collapsible> */}
|
||||
<DataSettings />
|
||||
<SocialLinksSettings />
|
||||
<ColorsSettings />
|
||||
</Content>
|
||||
<Preview>
|
||||
<Home />
|
||||
|
||||
@@ -14,6 +14,7 @@ export const Content = styled.div`
|
||||
padding: 5rem 1rem 1rem;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
overflow-y: scroll;
|
||||
background: #fff;
|
||||
`
|
||||
|
||||
export const Preview = styled.div`
|
||||
|
||||
@@ -11,7 +11,7 @@ export const Home = () => {
|
||||
const { colors, buttonBorderRadius, font, name, description } = data.settings
|
||||
|
||||
return (
|
||||
<Wrapper color={colors.texts} font={fonts[font].name}>
|
||||
<Wrapper color={colors.texts} font={fonts[font].value}>
|
||||
<Content>
|
||||
<Avatar />
|
||||
<Name>{name}</Name>
|
||||
|
||||
16
src/components/Textarea/index.tsx
Normal file
16
src/components/Textarea/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TextareaWrapper, Label, Wrapper } from './styles'
|
||||
|
||||
import type { TextareaHTMLAttributes } from 'react'
|
||||
|
||||
type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
label?: string
|
||||
}
|
||||
|
||||
export function Textarea({ label, ...rest }: TextareaProps) {
|
||||
return (
|
||||
<Wrapper>
|
||||
{!!label && <Label>{label}</Label>}
|
||||
<TextareaWrapper {...rest} />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
32
src/components/Textarea/styles.ts
Normal file
32
src/components/Textarea/styles.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 226px;
|
||||
`
|
||||
|
||||
export const Label = styled.label`
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-wight: 500;
|
||||
`
|
||||
|
||||
export const TextareaWrapper = styled.textarea`
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: transparent;
|
||||
font-size: 1rem;
|
||||
resize: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 5px 0 #0284c7;
|
||||
}
|
||||
`
|
||||
@@ -1,14 +1,11 @@
|
||||
const JWT_EXPIRES_IN = 15000000 // minutes
|
||||
const SESSION_EXPIRES_IN = 30 // days
|
||||
|
||||
const { JWT_SECRET } = process.env
|
||||
const SESSION_EXPIRES_IN_S = SESSION_EXPIRES_IN * 24 * 60 * 60
|
||||
const SESSION_EXPIRES_IN_MS = SESSION_EXPIRES_IN_S * 1000
|
||||
|
||||
if (!JWT_SECRET && !process.browser) throw new Error('JWT_SECRET is not defined')
|
||||
|
||||
export const authConstants = {
|
||||
JWT_SECRET,
|
||||
JWT_SECRET: process.env.JWT_SECRET || 'JWT_SECRET',
|
||||
JWT_EXPIRES_IN,
|
||||
SESSION_EXPIRES_IN_S,
|
||||
SESSION_EXPIRES_IN_MS
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
export const fonts = {
|
||||
roboto: {
|
||||
name: "'Roboto', sans-serif",
|
||||
name: 'Roboto',
|
||||
value: "'Roboto', sans-serif",
|
||||
url: 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap'
|
||||
},
|
||||
robotoSlab: {
|
||||
name: "'Roboto Slab', serif",
|
||||
name: 'Roboto Slab',
|
||||
value: "'Roboto Slab', serif",
|
||||
url: 'https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;500&display=swap'
|
||||
},
|
||||
oswald: {
|
||||
name: "'Oswald', sans-serif",
|
||||
name: 'Oswald',
|
||||
value: "'Oswald', sans-serif",
|
||||
url: 'https://fonts.googleapis.com/css2?family=Oswald:wght@400;500&display=swap'
|
||||
},
|
||||
poppins: {
|
||||
name: "'Poppins', sans-serif",
|
||||
name: 'Poppins',
|
||||
value: "'Poppins', sans-serif",
|
||||
url: 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap'
|
||||
},
|
||||
ubuntu: {
|
||||
name: "'Ubuntu', sans-serif",
|
||||
name: 'Ubuntu',
|
||||
value: "'Ubuntu', sans-serif",
|
||||
url: 'https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500&display=swap'
|
||||
},
|
||||
quicksand: {
|
||||
name: "'Quicksand', sans-serif",
|
||||
name: 'Quicksand',
|
||||
value: "'Quicksand', sans-serif",
|
||||
url: 'https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500&display=swap'
|
||||
},
|
||||
inconsolata: {
|
||||
name: "'Inconsolata', monospace",
|
||||
name: 'Inconsolata',
|
||||
value: "'Inconsolata', monospace",
|
||||
url: 'https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;500&display=swap'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import { authConstants } from 'constants/auth'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { Role } from '@prisma/client'
|
||||
|
||||
type Token = {
|
||||
sub: string
|
||||
role: string
|
||||
role: Role
|
||||
}
|
||||
|
||||
export async function authMiddleware(req: NextApiRequest, _res: NextApiResponse, next: () => void) {
|
||||
21
src/modules/auth/auth.controller.ts
Normal file
21
src/modules/auth/auth.controller.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AuthService } from './auth.service'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
class AuthController {
|
||||
async auth(req: NextApiRequest, res: NextApiResponse) {
|
||||
const authService = new AuthService()
|
||||
const { username, password } = req.body
|
||||
const result = await authService.auth(username, password)
|
||||
res.status(200).json(result)
|
||||
}
|
||||
|
||||
async refreshToken(req: NextApiRequest, res: NextApiResponse) {
|
||||
const authService = new AuthService()
|
||||
const { refreshToken } = req.body
|
||||
const result = await authService.refreshToken(refreshToken)
|
||||
res.status(200).json(result)
|
||||
}
|
||||
}
|
||||
|
||||
export const authController = new AuthController()
|
||||
96
src/modules/auth/auth.service.ts
Normal file
96
src/modules/auth/auth.service.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { compare } from 'bcryptjs'
|
||||
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
import { authConstants } from 'constants/auth'
|
||||
|
||||
import type { User } from '@prisma/client'
|
||||
|
||||
export class AuthService {
|
||||
async auth(username: string, password: string) {
|
||||
if (!username || !password) {
|
||||
throw new ExceptionError('Username and password are required')
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new ExceptionError('Credentials are invalid', 401)
|
||||
}
|
||||
|
||||
const isValidPassword = await compare(password, user.password)
|
||||
|
||||
if (!isValidPassword) {
|
||||
throw new ExceptionError('Credentials are invalid', 401)
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken } = await this.createSession(user)
|
||||
|
||||
return { accessToken, refreshToken }
|
||||
}
|
||||
|
||||
async refreshToken(token: string) {
|
||||
if (!token) {
|
||||
throw new ExceptionError('Refresh Token required')
|
||||
}
|
||||
|
||||
const session = await prisma.session.findUnique({
|
||||
where: { id: token }
|
||||
})
|
||||
|
||||
if (!session) {
|
||||
throw new ExceptionError('Refresh Token are invalid', 401)
|
||||
}
|
||||
|
||||
const isSessionExpired = new Date(session.expiresAt) < new Date()
|
||||
|
||||
if (isSessionExpired) {
|
||||
await prisma.session.delete({
|
||||
where: { id: token }
|
||||
})
|
||||
|
||||
throw new ExceptionError('Refresh Token are expired', 401)
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.userId }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new ExceptionError('User not found', 401)
|
||||
}
|
||||
|
||||
const tokens = await this.createSession(user)
|
||||
|
||||
await prisma.session.delete({
|
||||
where: { id: token }
|
||||
})
|
||||
|
||||
return { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken }
|
||||
}
|
||||
|
||||
private async createSession(user: User) {
|
||||
const { SESSION_EXPIRES_IN_MS, JWT_EXPIRES_IN, JWT_SECRET } = authConstants
|
||||
|
||||
try {
|
||||
const session = await prisma.session.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
expiresAt: new Date(Date.now() + SESSION_EXPIRES_IN_MS)
|
||||
}
|
||||
})
|
||||
|
||||
const payload = { role: user.role, sub: user.id }
|
||||
const accessToken = jwt.sign(payload, JWT_SECRET!, {
|
||||
expiresIn: `${JWT_EXPIRES_IN}m`
|
||||
})
|
||||
|
||||
return { accessToken, refreshToken: session.id }
|
||||
} catch (err: any) {
|
||||
throw new ExceptionError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/modules/data/data.controller.ts
Normal file
19
src/modules/data/data.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { DataService } from './data.service'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
class DataController {
|
||||
async getData(_req: NextApiRequest, res: NextApiResponse) {
|
||||
const dataService = new DataService()
|
||||
const result = await dataService.getData()
|
||||
res.status(200).json(result)
|
||||
}
|
||||
|
||||
async updateData(req: NextApiRequest, res: NextApiResponse) {
|
||||
const dataService = new DataService()
|
||||
const result = await dataService.updateData(req.body.data)
|
||||
res.status(200).json(result)
|
||||
}
|
||||
}
|
||||
|
||||
export const dataController = new DataController()
|
||||
29
src/modules/data/data.service.ts
Normal file
29
src/modules/data/data.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
export class DataService {
|
||||
async getData() {
|
||||
const response = await prisma.data.findUnique({
|
||||
where: { id: 1 }
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new ExceptionError('No data found', 404)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
async updateData(data = {}) {
|
||||
const response = await prisma.data.update({
|
||||
where: { id: 1 },
|
||||
data: { data: JSON.stringify(data) }
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new ExceptionError('No data found', 404)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
59
src/modules/users/users.controller.ts
Normal file
59
src/modules/users/users.controller.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { UsersService } from './users.service'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
class UsersController {
|
||||
async createUser(req: NextApiRequest, res: NextApiResponse) {
|
||||
const usersService = new UsersService()
|
||||
const { username, password, role } = req.body
|
||||
const { userRole } = req
|
||||
const result = await usersService.createUser(username, password, role, userRole)
|
||||
res.status(200).json(result)
|
||||
}
|
||||
|
||||
async getUser(req: NextApiRequest, res: NextApiResponse) {
|
||||
const usersService = new UsersService()
|
||||
const { id: userId } = req.params
|
||||
const result = await usersService.getUser(+userId)
|
||||
res.status(200).json(result)
|
||||
}
|
||||
|
||||
async getUsers(_req: NextApiRequest, res: NextApiResponse) {
|
||||
const usersService = new UsersService()
|
||||
const result = await usersService.getUsers()
|
||||
res.status(200).json(result)
|
||||
}
|
||||
|
||||
async getMe(req: NextApiRequest, res: NextApiResponse) {
|
||||
const usersService = new UsersService()
|
||||
const { userId } = req
|
||||
const result = await usersService.getUser(+userId)
|
||||
res.status(200).json(result)
|
||||
}
|
||||
|
||||
async checkUserExists(req: NextApiRequest, res: NextApiResponse) {
|
||||
const usersService = new UsersService()
|
||||
const { username } = req.body
|
||||
const result = await usersService.checkUserExists(username)
|
||||
res.status(200).json({ isTaken: result })
|
||||
}
|
||||
|
||||
async updateUser(req: NextApiRequest, res: NextApiResponse) {
|
||||
const usersService = new UsersService()
|
||||
const { id: userId } = req.params
|
||||
const { oldPassword, password, role } = req.body
|
||||
const { userRole } = req
|
||||
const result = await usersService.updateUser(+userId, userRole, oldPassword, password, role)
|
||||
res.status(200).json(result)
|
||||
}
|
||||
|
||||
async deleteUser(req: NextApiRequest, res: NextApiResponse) {
|
||||
const usersService = new UsersService()
|
||||
const { id: userId } = req.params
|
||||
const { userRole } = req
|
||||
await usersService.deleteUser(+userId, userRole)
|
||||
res.status(204).end()
|
||||
}
|
||||
}
|
||||
|
||||
export const usersController = new UsersController()
|
||||
171
src/modules/users/users.service.ts
Normal file
171
src/modules/users/users.service.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { compare, hashSync } from 'bcryptjs'
|
||||
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { Role } from '@prisma/client'
|
||||
|
||||
export class UsersService {
|
||||
async createUser(username: string, password: string, role: Role = 'EDITOR', userRole: Role) {
|
||||
if (!username || !password) {
|
||||
throw new ExceptionError('Username and password are required')
|
||||
}
|
||||
|
||||
if (!['ADMIN', 'EDITOR'].includes(role)) {
|
||||
throw new ExceptionError('Role must be either ADMIN or EDITOR')
|
||||
}
|
||||
|
||||
if (userRole !== 'ADMIN') {
|
||||
throw new ExceptionError('Only admins can create users', 403)
|
||||
}
|
||||
|
||||
const usernameExists = await this.checkUserExists(username)
|
||||
|
||||
if (usernameExists) {
|
||||
throw new ExceptionError('Username already exists')
|
||||
}
|
||||
|
||||
const response = await prisma.user.create({
|
||||
data: {
|
||||
username: username,
|
||||
password: hashSync(password, 10),
|
||||
role: role ? role : 'EDITOR'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new ExceptionError('User not created')
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
async getUser(userId: number, withPassword = false) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
password: withPassword,
|
||||
lang: true,
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new ExceptionError('User not found', 404)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
async getUsers() {
|
||||
console.log('getUsers')
|
||||
const response = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
lang: true,
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
console.log(response)
|
||||
|
||||
if (!response) {
|
||||
throw new ExceptionError('No data found', 404)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
async updateUser(
|
||||
userId: number,
|
||||
userRole: Role,
|
||||
oldPassword?: string,
|
||||
password?: string,
|
||||
role?: Role
|
||||
) {
|
||||
const user = await this.getUser(userId, true)
|
||||
|
||||
let updatedUser = {}
|
||||
|
||||
if (oldPassword && password) {
|
||||
const isValidPassword = await compare(oldPassword, user.password)
|
||||
|
||||
if (!isValidPassword) {
|
||||
throw new ExceptionError('Password does not match')
|
||||
}
|
||||
|
||||
updatedUser = { ...updatedUser, password: hashSync(password, 10) }
|
||||
}
|
||||
|
||||
if (role) {
|
||||
if (!['ADMIN', 'EDITOR'].includes(role)) {
|
||||
throw new ExceptionError('Role must be either ADMIN or EDITOR')
|
||||
}
|
||||
|
||||
if (userRole === 'ADMIN' && role === 'EDITOR') {
|
||||
throw new ExceptionError('Cannot downgrade admin role', 403)
|
||||
}
|
||||
|
||||
if (role !== 'EDITOR' && userRole === 'EDITOR') {
|
||||
throw new ExceptionError('Cannot change role', 403)
|
||||
}
|
||||
|
||||
updatedUser = { ...updatedUser, role }
|
||||
}
|
||||
|
||||
if (Object.keys(updatedUser).length === 0) {
|
||||
throw new ExceptionError('No data to update')
|
||||
}
|
||||
|
||||
const response = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: updatedUser,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
lang: true,
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new ExceptionError('User not updated')
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
async deleteUser(userId: number, userRole: Role) {
|
||||
if (userId === 1) {
|
||||
throw new ExceptionError('Cannot delete default admin', 403)
|
||||
}
|
||||
|
||||
if (userRole !== 'ADMIN') {
|
||||
throw new ExceptionError('Only admins can delete other users', 403)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.user.delete({
|
||||
where: { id: userId }
|
||||
})
|
||||
} catch (err: any) {
|
||||
throw new ExceptionError(err)
|
||||
}
|
||||
}
|
||||
|
||||
async checkUserExists(username: string) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
})
|
||||
|
||||
return !!user
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import Modal from 'react-modal'
|
||||
|
||||
import { Layout } from 'components/Layout'
|
||||
import { AuthProvider } from 'contexts/AuthContext'
|
||||
@@ -19,7 +20,8 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
</I18nProvider>
|
||||
</AuthProvider>
|
||||
</DataProvider>
|
||||
<Toaster toastOptions={{ duration: 5000, position: 'top-left' }} />
|
||||
<Toaster toastOptions={{ duration: 5000, position: 'top-right' }} />
|
||||
{Modal.setAppElement('#__next')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
import NextDocument, { Head, Html, Main, NextScript } from 'next/document'
|
||||
import NextDocument, { DocumentContext, Head, Html, Main, NextScript } from 'next/document'
|
||||
import { ServerStyleSheet } from 'styled-components'
|
||||
|
||||
export default class Document extends NextDocument {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const sheet = new ServerStyleSheet()
|
||||
const originalRenderPage = ctx.renderPage
|
||||
|
||||
try {
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: App => props => sheet.collectStyles(<App {...props} />)
|
||||
})
|
||||
|
||||
const initialProps = await NextDocument.getInitialProps(ctx)
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
{sheet.getStyleElement()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
sheet.seal()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
import { nc } from 'utils/nc'
|
||||
import { authMiddleware } from 'utils/authMiddleware'
|
||||
import {
|
||||
auth,
|
||||
checkUsername,
|
||||
createUser,
|
||||
deleteUser,
|
||||
getData,
|
||||
getUserById,
|
||||
getUsers,
|
||||
me,
|
||||
refreshToken,
|
||||
updateData,
|
||||
updateUser
|
||||
} from 'api'
|
||||
import { authMiddleware } from 'middlewares/auth.middleware'
|
||||
import { dataController } from 'modules/data/data.controller'
|
||||
import { authController } from 'modules/auth/auth.controller'
|
||||
import { usersController } from 'modules/users/users.controller'
|
||||
|
||||
const handler = nc
|
||||
.post('/api/auth', auth)
|
||||
.post('/api/auth/refresh-token', refreshToken)
|
||||
.post('/api/auth', authController.auth)
|
||||
.post('/api/auth/refresh-token', authController.refreshToken)
|
||||
|
||||
.use(authMiddleware)
|
||||
|
||||
.get('/api/data', getData)
|
||||
.put('/api/data', updateData)
|
||||
.get('/api/data', dataController.getData)
|
||||
.put('/api/data', dataController.updateData)
|
||||
|
||||
.get('/api/users', getUsers)
|
||||
.get('/api/me', me)
|
||||
.get('/api/users/:id', getUserById)
|
||||
.post('/api/users/check-username', checkUsername)
|
||||
.post('/api/users', createUser)
|
||||
.patch('/api/users/:id', updateUser)
|
||||
.delete('/api/users/:id', deleteUser)
|
||||
.get('/api/users', usersController.getUsers)
|
||||
.get('/api/me', usersController.getMe)
|
||||
.get('/api/users/:id', usersController.getUser)
|
||||
.post('/api/users/check-username', usersController.checkUserExists)
|
||||
.post('/api/users', usersController.createUser)
|
||||
.patch('/api/users/:id', usersController.updateUser)
|
||||
.delete('/api/users/:id', usersController.deleteUser)
|
||||
|
||||
export default handler
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Global = createGlobalStyle<GlobalStyleProps>`
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
color: #0f172a;
|
||||
background: ${({ data }) => data.settings.colors.background};
|
||||
}
|
||||
|
||||
4
src/types/next.d.ts
vendored
4
src/types/next.d.ts
vendored
@@ -1,9 +1,11 @@
|
||||
import { IncomingMessage } from 'http'
|
||||
|
||||
import type { Role } from '@prisma/client'
|
||||
|
||||
declare module 'next' {
|
||||
export interface NextApiRequest extends IncomingMessage {
|
||||
userId: string
|
||||
userRole: string
|
||||
userRole: Role
|
||||
params: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
import { authConstants } from 'constants/auth'
|
||||
import { prisma } from 'services/prisma'
|
||||
import { ExceptionError } from 'utils/error'
|
||||
|
||||
import type { User } from '@prisma/client'
|
||||
|
||||
export async function createSession(user: User) {
|
||||
const { SESSION_EXPIRES_IN_MS, JWT_EXPIRES_IN, JWT_SECRET } = authConstants
|
||||
|
||||
try {
|
||||
const session = await prisma.session.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
expiresAt: new Date(Date.now() + SESSION_EXPIRES_IN_MS)
|
||||
}
|
||||
})
|
||||
|
||||
const payload = { role: user.role, sub: user.id }
|
||||
const accessToken = jwt.sign(payload, JWT_SECRET!, {
|
||||
expiresIn: `${JWT_EXPIRES_IN}m`
|
||||
})
|
||||
|
||||
return { accessToken, refreshToken: session.id }
|
||||
} catch (err: any) {
|
||||
throw new ExceptionError(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user