Skip to content

Commit

Permalink
feat: basic user manager
Browse files Browse the repository at this point in the history
  • Loading branch information
Arcath committed Feb 29, 2024
1 parent 0f40d3c commit 0b51579
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 5 deletions.
4 changes: 3 additions & 1 deletion app/lib/rbac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ export const {can} = canCant<'guest' | 'user' | 'manager' | 'admin'>({
'entry:*',
'asset-manager:*',
'field-manager:*',
'user-manager:*',
'dashboard',
'search'
'search',
'logout'
]
}
}) as {
Expand Down
4 changes: 2 additions & 2 deletions app/lib/utils/ensure-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const ensureUser = async (

const cookieSession = await prisma.session.findFirst({
where: {id: cookie},
include: {user: true}
include: {user: {select: {id: true, name: true, role: true}}}
})

if (!cookieSession) {
Expand All @@ -39,5 +39,5 @@ export const ensureUser = async (
})
}

return cookieSession.user
return {...cookieSession.user, sessionId: cookieSession.id}
}
11 changes: 11 additions & 0 deletions app/routes/api.me.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {type LoaderFunctionArgs, json} from '@remix-run/node'

import {ensureUser} from '~/lib/utils/ensure-user'

export const loader = async ({request}: LoaderFunctionArgs) => {
const user = await ensureUser(request, 'dashboard', {})

console.log('HIYA')

return json({user})
}
21 changes: 21 additions & 0 deletions app/routes/app.logout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {redirect, type LoaderFunctionArgs} from '@remix-run/node'

import {ensureUser} from '~/lib/utils/ensure-user'
import {session} from '~/lib/cookies'
import {getPrisma} from '~/lib/prisma.server'

export const loader = async ({request}: LoaderFunctionArgs) => {
const {sessionId} = await ensureUser(request, 'logout', {})

const prisma = getPrisma()

await prisma.session.delete({where: {id: sessionId}})

return redirect(`/app/login`, {
headers: {
'Set-Cookie': await session.serialize('', {
maxAge: 1
})
}
})
}
11 changes: 9 additions & 2 deletions app/routes/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const loader = async ({request}: LoaderFunctionArgs) => {
return json({user, assets})
}

export type AppLoader = {user: {name: string; id: string}}

const Dashboard = () => {
const {assets} = useLoaderData<typeof loader>()

Expand All @@ -45,8 +47,13 @@ const Dashboard = () => {
</div>
<h2 className="text-xl ml-4">System</h2>
<div className="pl-8 mb-2 flex flex-col gap-2 mt-2">
<a href="/app/asset-manager">Asset Manager</a>
<a href="/app/field-manager">Field Manager</a>
<a href="/app/asset-manager">📦 Asset Manager</a>
<a href="/app/field-manager">🚜 Field Manager</a>
<a href="/app/user-manager">👤 User Manager</a>
</div>
<h2 className="text-xl ml-4">User</h2>
<div className="pl-8 mb-2 flex flex-col gap-2 mt-2">
<a href="/app/logout">👋 Logout</a>
</div>
</nav>
<div className="pt-8">
Expand Down
93 changes: 93 additions & 0 deletions app/routes/app.user-manager.$user.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
type LoaderFunctionArgs,
type ActionFunctionArgs,
json,
redirect
} from '@remix-run/node'
import {useLoaderData} from '@remix-run/react'
import {invariant} from '@arcath/utils'

import {ensureUser} from '~/lib/utils/ensure-user'
import {getPrisma} from '~/lib/prisma.server'
import {Button} from '~/lib/components/button'
import {Label, Input, Select} from '~/lib/components/input'
import {hashPassword} from '~/lib/user.server'

export const loader = async ({request, params}: LoaderFunctionArgs) => {
const currentUser = await ensureUser(request, 'user-manager:edit', {
userId: params.user
})

const prisma = getPrisma()

const user = await prisma.user.findFirstOrThrow({where: {id: params.user}})

return json({currentUser, user})
}

export const action = async ({request, params}: ActionFunctionArgs) => {
const currentUser = await ensureUser(request, 'user-manager:edit', {

Check failure on line 29 in app/routes/app.user-manager.$user.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'currentUser' is assigned a value but never used
userId: params.user
})

const prisma = getPrisma()

const formData = await request.formData()

const name = formData.get('name') as string | undefined
const email = formData.get('email') as string | undefined
const password = formData.get('password') as string | undefined
const role = formData.get('role') as string | undefined

invariant(name)
invariant(email)
invariant(role)

let newPassword: string | undefined = undefined

if (password) {
newPassword = await hashPassword(password)
}

const user = await prisma.user.update({
where: {id: params.user},
data: {name, email, passwordHash: newPassword, role}
})

return redirect(`/app/user-manager/${user.id}`)
}

const UserManagerUser = () => {
const {user} = useLoaderData<typeof loader>()

return (
<div>
<h4 className="text-xl">{user.name}</h4>
<form method="POST">
<Label>
Name
<Input name="name" defaultValue={user.name} />
</Label>
<Label>
Email
<Input name="email" type="email" defaultValue={user.email} />
</Label>
<Label>
Password
<Input name="password" type="password" />
</Label>
<Label>
Role
<Select name="role" defaultValue={user.role}>
<option value="user">User</option>
<option value="manager">Manager</option>
<option value="admin">Admin</option>
</Select>
</Label>
<Button className="bg-success">Update User</Button>
</form>
</div>
)
}

export default UserManagerUser
53 changes: 53 additions & 0 deletions app/routes/app.user-manager._index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {type LoaderFunctionArgs, json} from '@remix-run/node'
import {useLoaderData} from '@remix-run/react'

import {ensureUser} from '~/lib/utils/ensure-user'
import {getPrisma} from '~/lib/prisma.server'
import {AButton} from '~/lib/components/button'

export const loader = async ({request}: LoaderFunctionArgs) => {
const user = await ensureUser(request, 'user-manager:list', {})

const prisma = getPrisma()

const users = await prisma.user.findMany({
select: {id: true, name: true, email: true},
orderBy: {name: 'asc'}
})

return json({user, users})
}

const UserManagerList = () => {
const {users} = useLoaderData<typeof loader>()

return (
<div>
<AButton className="bg-success" href="/app/user-manager/add">
Add User
</AButton>
<table className="">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{users.map(({id, name, email}) => {
return (
<tr key={id}>
<td>
<a href={`/app/user-manager/${id}`}>{name}</a>
</td>
<td>{email}</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}

export default UserManagerList
81 changes: 81 additions & 0 deletions app/routes/app.user-manager.add.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
type LoaderFunctionArgs,
type ActionFunctionArgs,
json,
redirect
} from '@remix-run/node'
import {useLoaderData} from '@remix-run/react'

Check failure on line 7 in app/routes/app.user-manager.add.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'useLoaderData' is defined but never used
import {invariant} from '@arcath/utils'

import {ensureUser} from '~/lib/utils/ensure-user'
import {getPrisma} from '~/lib/prisma.server'
import {Button} from '~/lib/components/button'
import {Label, Input, Select} from '~/lib/components/input'
import {hashPassword} from '~/lib/user.server'

export const loader = async ({request, params}: LoaderFunctionArgs) => {
const currentUser = await ensureUser(request, 'user-manager:edit', {
userId: params.user
})

return json({currentUser})
}

export const action = async ({request, params}: ActionFunctionArgs) => {
await ensureUser(request, 'user-manager:add', {
userId: params.user
})

const prisma = getPrisma()

const formData = await request.formData()

const name = formData.get('name') as string | undefined
const email = formData.get('email') as string | undefined
const password = formData.get('password') as string | undefined
const role = formData.get('role') as string | undefined

invariant(name)
invariant(email)
invariant(password)
invariant(role)

const user = await prisma.user.create({
data: {name, email, passwordHash: await hashPassword(password), role}
})

return redirect(`/app/user-manager/${user.id}`)
}

const UserManagerAdd = () => {
return (
<div>
<h4 className="text-xl">Add User</h4>
<form method="POST">
<Label>
Name
<Input name="name" />
</Label>
<Label>
Email
<Input name="email" type="email" />
</Label>
<Label>
Password
<Input name="password" type="password" />
</Label>
<Label>
Role
<Select name="role">
<option value="user">User</option>
<option value="manager">Manager</option>
<option value="admin">Admin</option>
</Select>
</Label>
<Button className="bg-success">Add User</Button>
</form>
</div>
)
}

export default UserManagerAdd
14 changes: 14 additions & 0 deletions app/routes/app.user-manager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Outlet} from '@remix-run/react'

import {Header} from '~/lib/components/header'

const UserManager = () => {
return (
<div>
<Header title="User Manager" />
<Outlet />
</div>
)
}

export default UserManager

0 comments on commit 0b51579

Please sign in to comment.