Skip to content

Commit

Permalink
feat: first pass at passwords
Browse files Browse the repository at this point in the history
  • Loading branch information
Arcath committed Mar 3, 2024
1 parent cc4a1c2 commit 3a7dd6d
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 1 deletion.
42 changes: 42 additions & 0 deletions app/lib/crypto.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {scrypt, randomBytes, createCipheriv, createDecipheriv} from 'crypto'

export const getCryptoSuite = async () => {
const algorithm = 'aes-192-cbc'
const key = await new Promise<Buffer>((resolve, reject) => {
scrypt(
process.env.PASSWORD_KEY!,
process.env.PASSWORD_SALT!,
24,
(error, derivedKey) => {
if (error) {
reject(error)
}

resolve(derivedKey)
}
)
})

const encrypt = (text: string) => {
console.dir(randomBytes(8).toString('hex'))

const cipher = createCipheriv(
algorithm,
key,
Buffer.from(process.env.PASSWORD_IV!, 'utf8')
)

return cipher.update(text, 'utf8', 'hex') + cipher.final('hex')
}

const decrypt = (hash: string) => {
const decipher = createDecipheriv(
algorithm,
key,
Buffer.from(process.env.PASSWORD_IV!, 'utf8')
)
return decipher.update(hash, 'hex', 'utf8') + decipher.final('utf8')
}

return {encrypt, decrypt}
}
3 changes: 2 additions & 1 deletion app/lib/rbac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export const {can} = canCant<'guest' | 'user' | 'manager' | 'admin'>({
'dashboard',
'search',
'logout',
'document:*'
'document:*',
'password:*'
]
}
}) as {
Expand Down
63 changes: 63 additions & 0 deletions app/routes/app.passwords.$password._index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {type LoaderFunctionArgs, json} from '@remix-run/node'
import {useLoaderData} from '@remix-run/react'
import {useState} from 'react'

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

export const loader = async ({request, params}: LoaderFunctionArgs) => {
const user = await ensureUser(request, 'password:view', {
passwordId: params.password
})

const prisma = getPrisma()

const password = await prisma.password.findFirstOrThrow({
select: {id: true, title: true, username: true, notes: true},
where: {id: params.password}
})

const code = await buildMDXBundle(password.notes)

return json({user, password, code})
}

const AssetManagerAsset = () => {
const {password, code} = useLoaderData<typeof loader>()
const [passwordOpen, setPasswordOpen] = useState(false)

return (
<div>
<h4 className="text-xl">{password.title}</h4>
<AButton href={`/app/passwords/${password.id}/edit`} className="bg-info">
Edit
</AButton>
<p>
<b>Username</b>
<br />
{password.username}
</p>
<p>
<b>Password</b>
<br />
{passwordOpen ? (
'OPEN'
) : (
<button
onClick={() => {
setPasswordOpen(true)
}}
>
🔐
</button>
)}
</p>
<MDXComponent code={code} />
</div>
)
}

export default AssetManagerAsset
48 changes: 48 additions & 0 deletions app/routes/app.passwords._index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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, 'password:list', {})

const prisma = getPrisma()

const passwords = await prisma.password.findMany({orderBy: {title: 'asc'}})

return json({user, passwords})
}

const DocumentsList = () => {
const {passwords} = useLoaderData<typeof loader>()

return (
<div>
<AButton className="bg-success" href="/app/passwords/add">
Add Password
</AButton>
<table>
<thead>
<tr>
<th>Password</th>
</tr>
</thead>
<tbody>
{passwords.map(({id, title}) => {
return (
<tr key={id}>
<td>
<a href={`/app/passwords/${id}`}>{title}</a>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}

export default DocumentsList
81 changes: 81 additions & 0 deletions app/routes/app.passwords.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 {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, HelperText, TextArea} from '~/lib/components/input'

import {getCryptoSuite} from '~/lib/crypto.server'

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

return json({user})
}

export const action = async ({request}: ActionFunctionArgs) => {
await ensureUser(request, 'password:add', {})

const formData = await request.formData()

const prisma = getPrisma()
const {encrypt} = await getCryptoSuite()

const title = formData.get('title') as string | undefined
const username = formData.get('username') as string | undefined
const password = formData.get('password') as string | undefined
const notes = formData.get('notes') as string | undefined

invariant(title)
invariant(password)

const newPassword = await prisma.password.create({
data: {
title,
username: username ? username : '',
password: encrypt(password),
notes: notes ? notes : ''
}
})

return redirect(`/app/passwords/${newPassword.id}`)
}

const PasswordAdd = () => {
return (
<div>
<h2>Add Password</h2>
<form method="POST">
<Label>
Title
<Input name="title" />
<HelperText>The title of the Password.</HelperText>
</Label>
<Label>
Username
<Input name="username" />
<HelperText>The username (can be blank).</HelperText>
</Label>
<Label>
Password
<Input name="password" />
<HelperText>The password.</HelperText>
</Label>
<Label>
Notes
<TextArea name="notes" className="min-h-[25vh]" />
<HelperText>Any notes for the password.</HelperText>
</Label>
<Button className="bg-success">Add Password</Button>
</form>
</div>
)
}

export default PasswordAdd
14 changes: 14 additions & 0 deletions app/routes/app.passwords.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 Passwords = () => {
return (
<div>
<Header title="Passwords" />
<Outlet />
</div>
)
}

export default Passwords
1 change: 1 addition & 0 deletions app/routes/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const Dashboard = () => {
<a href="/app">📜 Dashboard</a>
<a href="/app/search">🔎 Search</a>
<a href="/app/documents">📰 Documents</a>
<a href="/app/passwords">🔐 Passwords</a>
</div>
<h2 className="text-xl ml-4">Assets</h2>
<div className="pl-8 mb-2 flex flex-col gap-2 mt-2">
Expand Down
23 changes: 23 additions & 0 deletions prisma/migrations/20240303172355_add_passwords/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "Password" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"username" TEXT NOT NULL,
"password" TEXT NOT NULL,
"notes" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);

-- CreateTable
CREATE TABLE "PasswordHistory" (
"id" TEXT NOT NULL PRIMARY KEY,
"previousTitle" TEXT NOT NULL,
"previousBody" TEXT NOT NULL,
"editedById" TEXT NOT NULL,
"passwordId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "PasswordHistory_editedById_fkey" FOREIGN KEY ("editedById") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "PasswordHistory_passwordId_fkey" FOREIGN KEY ("passwordId") REFERENCES "Password" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
28 changes: 28 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ model User {
valueEdits Value[]
pastValueEdits ValueHistory[]
documentChanges DocumentHistory[]
passwordChanges PasswordHistory[]
}

model Session {
Expand Down Expand Up @@ -128,3 +129,30 @@ model DocumentHistory {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Password {
id String @id @default(uuid())
title String
username String
password String
notes String
history PasswordHistory[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model PasswordHistory {
id String @id @default(uuid())
previousTitle String
previousBody String
editedBy User @relation(fields: [editedById], references: [id])
editedById String
password Password @relation(fields: [passwordId], references: [id], onDelete: Cascade)
passwordId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

0 comments on commit 3a7dd6d

Please sign in to comment.