Skip to content

Commit

Permalink
epic v2.3: NEW .env.example; csrf security
Browse files Browse the repository at this point in the history
  • Loading branch information
thadk committed Dec 1, 2023
1 parent d85adb9 commit 4494a75
Show file tree
Hide file tree
Showing 49 changed files with 772 additions and 449 deletions.
13 changes: 10 additions & 3 deletions heat-stack/app/routes/settings+/profile.password.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { conform, useForm } from '@conform-to/react'
import { getFieldsetConstraint, parse } from '@conform-to/zod'
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import { json, redirect, type DataFunctionArgs } from '@remix-run/node'
import { Form, Link, useActionData } from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { z } from 'zod'
import { ErrorList, Field } from '#app/components/forms.tsx'
import { Button } from '#app/components/ui/button.tsx'
Expand All @@ -12,13 +14,16 @@ import {
requireUserId,
verifyUserPassword,
} from '#app/utils/auth.server.ts'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { useIsPending } from '#app/utils/misc.tsx'
import { redirectWithToast } from '#app/utils/toast.server.ts'
import { PasswordSchema } from '#app/utils/user-validation.ts'
import { type BreadcrumbHandle } from './profile.tsx'

export const handle = {
export const handle: BreadcrumbHandle & SEOHandle = {
breadcrumb: <Icon name="dots-horizontal">Password</Icon>,
getSitemapEntries: () => null,
}

const ChangePasswordForm = z
Expand All @@ -31,7 +36,7 @@ const ChangePasswordForm = z
if (confirmNewPassword !== newPassword) {
ctx.addIssue({
path: ['confirmNewPassword'],
code: 'custom',
code: z.ZodIssueCode.custom,
message: 'The passwords must match',
})
}
Expand All @@ -57,6 +62,7 @@ export async function action({ request }: DataFunctionArgs) {
const userId = await requireUserId(request)
await requirePassword(userId)
const formData = await request.formData()
await validateCSRF(formData, request.headers)
const submission = await parse(formData, {
async: true,
schema: ChangePasswordForm.superRefine(
Expand All @@ -66,7 +72,7 @@ export async function action({ request }: DataFunctionArgs) {
if (!user) {
ctx.addIssue({
path: ['currentPassword'],
code: 'custom',
code: z.ZodIssueCode.custom,
message: 'Incorrect password.',
})
}
Expand Down Expand Up @@ -126,6 +132,7 @@ export default function ChangePasswordRoute() {

return (
<Form method="POST" {...form.props} className="mx-auto max-w-md">
<AuthenticityTokenInput />
<Field
labelProps={{ children: 'Current Password' }}
inputProps={conform.input(fields.currentPassword, { type: 'password' })}
Expand Down
35 changes: 12 additions & 23 deletions heat-stack/app/routes/settings+/profile.password_.create.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
import { conform, useForm } from '@conform-to/react'
import { getFieldsetConstraint, parse } from '@conform-to/zod'
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import { json, redirect, type DataFunctionArgs } from '@remix-run/node'
import { Form, Link, useActionData } from '@remix-run/react'
import { z } from 'zod'
import { ErrorList, Field } from '#app/components/forms.tsx'
import { Button } from '#app/components/ui/button.tsx'
import { Icon } from '#app/components/ui/icon.tsx'
import { StatusButton } from '#app/components/ui/status-button.tsx'
import { getPasswordHash, requireUserId } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { useIsPending } from '#app/utils/misc.tsx'
import { PasswordSchema } from '#app/utils/user-validation.ts'
import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts'
import { type BreadcrumbHandle } from './profile.tsx'

export const handle = {
export const handle: BreadcrumbHandle & SEOHandle = {
breadcrumb: <Icon name="dots-horizontal">Password</Icon>,
getSitemapEntries: () => null,
}

const CreatePasswordForm = z
.object({
newPassword: PasswordSchema,
confirmNewPassword: PasswordSchema,
})
.superRefine(({ confirmNewPassword, newPassword }, ctx) => {
if (confirmNewPassword !== newPassword) {
ctx.addIssue({
path: ['confirmNewPassword'],
code: 'custom',
message: 'The passwords must match',
})
}
})
const CreatePasswordForm = PasswordAndConfirmPasswordSchema

async function requireNoPassword(userId: string) {
const password = await prisma.password.findUnique({
Expand Down Expand Up @@ -66,15 +55,15 @@ export async function action({ request }: DataFunctionArgs) {
return json({ status: 'error', submission } as const, { status: 400 })
}

const { newPassword } = submission.value
const { password } = submission.value

await prisma.user.update({
select: { username: true },
where: { id: userId },
data: {
password: {
create: {
hash: await getPasswordHash(newPassword),
hash: await getPasswordHash(password),
},
},
},
Expand All @@ -101,15 +90,15 @@ export default function CreatePasswordRoute() {
<Form method="POST" {...form.props} className="mx-auto max-w-md">
<Field
labelProps={{ children: 'New Password' }}
inputProps={conform.input(fields.newPassword, { type: 'password' })}
errors={fields.newPassword.errors}
inputProps={conform.input(fields.password, { type: 'password' })}
errors={fields.password.errors}
/>
<Field
labelProps={{ children: 'Confirm New Password' }}
inputProps={conform.input(fields.confirmNewPassword, {
inputProps={conform.input(fields.confirmPassword, {
type: 'password',
})}
errors={fields.confirmNewPassword.errors}
errors={fields.confirmPassword.errors}
/>
<ErrorList id={form.errorId} errors={form.errors} />
<div className="grid w-full grid-cols-2 gap-6">
Expand Down
Loading

0 comments on commit 4494a75

Please sign in to comment.