diff --git a/app/lib/crypto.server.ts b/app/lib/crypto.server.ts new file mode 100644 index 0000000..ac8c8f0 --- /dev/null +++ b/app/lib/crypto.server.ts @@ -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((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} +} diff --git a/app/lib/rbac.ts b/app/lib/rbac.ts index 95bc4dc..f74fffb 100644 --- a/app/lib/rbac.ts +++ b/app/lib/rbac.ts @@ -33,7 +33,8 @@ export const {can} = canCant<'guest' | 'user' | 'manager' | 'admin'>({ 'dashboard', 'search', 'logout', - 'document:*' + 'document:*', + 'password:*' ] } }) as { diff --git a/app/routes/app.passwords.$password._index.tsx b/app/routes/app.passwords.$password._index.tsx new file mode 100644 index 0000000..fdb646e --- /dev/null +++ b/app/routes/app.passwords.$password._index.tsx @@ -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() + const [passwordOpen, setPasswordOpen] = useState(false) + + return ( +
+

{password.title}

+ + Edit + +

+ Username +
+ {password.username} +

+

+ Password +
+ {passwordOpen ? ( + 'OPEN' + ) : ( + + )} +

+ +
+ ) +} + +export default AssetManagerAsset diff --git a/app/routes/app.passwords._index.tsx b/app/routes/app.passwords._index.tsx new file mode 100644 index 0000000..ea46ade --- /dev/null +++ b/app/routes/app.passwords._index.tsx @@ -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() + + return ( +
+ + Add Password + + + + + + + + + {passwords.map(({id, title}) => { + return ( + + + + ) + })} + +
Password
+ {title} +
+
+ ) +} + +export default DocumentsList diff --git a/app/routes/app.passwords.add.tsx b/app/routes/app.passwords.add.tsx new file mode 100644 index 0000000..8a2b902 --- /dev/null +++ b/app/routes/app.passwords.add.tsx @@ -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 ( +
+

Add Password

+
+ + + +