Skip to content

Commit a69e963

Browse files
committed
add decision document for sessions
1 parent c660820 commit a69e963

File tree

20 files changed

+174
-54
lines changed

20 files changed

+174
-54
lines changed

app/root.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
import { clsx } from 'clsx'
2424
import { useState } from 'react'
2525
import tailwindStylesheetUrl from './styles/tailwind.css'
26-
import { authenticator } from './utils/auth.server.ts'
26+
import { authenticator, getUserId } from './utils/auth.server.ts'
2727
import { prisma } from './utils/db.server.ts'
2828
import { getEnv } from './utils/env.server.ts'
2929
import { ButtonLink } from './utils/forms.tsx'
@@ -65,7 +65,7 @@ export const meta: V2_MetaFunction = () => {
6565
}
6666

6767
export async function loader({ request }: DataFunctionArgs) {
68-
const userId = await authenticator.isAuthenticated(request)
68+
const userId = await getUserId(request)
6969

7070
const user = userId
7171
? await prisma.user.findUnique({

app/routes/_auth+/login.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ import {
66
import { useLoaderData, useSearchParams } from '@remix-run/react'
77
import { GeneralErrorBoundary } from '~/components/error-boundary.tsx'
88
import { Spacer } from '~/components/spacer.tsx'
9-
import { authenticator } from '~/utils/auth.server.ts'
9+
import { authenticator, requireAnonymous } from '~/utils/auth.server.ts'
1010
import { commitSession, getSession } from '~/utils/session.server.ts'
1111
import { InlineLogin } from '../resources+/login.tsx'
1212

1313
export async function loader({ request }: DataFunctionArgs) {
14-
await authenticator.isAuthenticated(request, {
15-
successRedirect: '/',
16-
})
14+
await requireAnonymous(request)
1715
const session = await getSession(request.headers.get('cookie'))
1816
const error = session.get(authenticator.sessionErrorKey)
1917
let errorMessage: string | null = null

app/routes/_auth+/onboarding.tsx

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '@remix-run/react'
1717
import { z } from 'zod'
1818
import { Spacer } from '~/components/spacer.tsx'
19-
import { authenticator, createUser } from '~/utils/auth.server.ts'
19+
import { authenticator, requireAnonymous, signup } from '~/utils/auth.server.ts'
2020
import { Button, CheckboxField, ErrorList, Field } from '~/utils/forms.tsx'
2121
import { safeRedirect } from '~/utils/misc.ts'
2222
import { commitSession, getSession } from '~/utils/session.server.ts'
@@ -25,8 +25,8 @@ import {
2525
passwordSchema,
2626
usernameSchema,
2727
} from '~/utils/user-validation.ts'
28-
import { onboardingEmailSessionKey } from './signup.tsx'
2928
import { checkboxSchema } from '~/utils/zod-extensions.ts'
29+
import { onboardingEmailSessionKey } from './signup.tsx'
3030

3131
const OnboardingFormSchema = z
3232
.object({
@@ -52,9 +52,7 @@ const OnboardingFormSchema = z
5252
})
5353

5454
export async function loader({ request }: DataFunctionArgs) {
55-
await authenticator.isAuthenticated(request, {
56-
successRedirect: '/',
57-
})
55+
await requireAnonymous(request)
5856
const session = await getSession(request.headers.get('cookie'))
5957
const error = session.get(authenticator.sessionErrorKey)
6058
const onboardingEmail = session.get(onboardingEmailSessionKey)
@@ -72,8 +70,8 @@ export async function loader({ request }: DataFunctionArgs) {
7270
}
7371

7472
export async function action({ request }: DataFunctionArgs) {
75-
const session = await getSession(request.headers.get('cookie'))
76-
const email = session.get(onboardingEmailSessionKey)
73+
const cookieSession = await getSession(request.headers.get('cookie'))
74+
const email = cookieSession.get(onboardingEmailSessionKey)
7775
if (typeof email !== 'string' || !email) {
7876
return redirect('/signup')
7977
}
@@ -105,14 +103,13 @@ export async function action({ request }: DataFunctionArgs) {
105103
redirectTo,
106104
} = submission.value
107105

108-
const user = await createUser({ email, username, password, name })
109-
session.set(authenticator.sessionKey, user.id)
110-
session.unset(onboardingEmailSessionKey)
106+
const session = await signup({ email, username, password, name })
107+
108+
cookieSession.set(authenticator.sessionKey, session.id)
109+
cookieSession.unset(onboardingEmailSessionKey)
111110

112-
const newCookie = await commitSession(session, {
113-
maxAge: remember
114-
? 60 * 60 * 24 * 7 // 7 days
115-
: undefined,
111+
const newCookie = await commitSession(cookieSession, {
112+
expires: remember ? session.expirationDate : undefined,
116113
})
117114
return redirect(safeRedirect(redirectTo, '/'), {
118115
headers: { 'Set-Cookie': newCookie },
@@ -162,7 +159,9 @@ export default function OnboardingPage() {
162159
inputProps={{
163160
...conform.input(fields.username),
164161
autoComplete: 'username',
165-
autoFocus: typeof actionData === 'undefined' || typeof fields.username.initialError !== 'undefined',
162+
autoFocus:
163+
typeof actionData === 'undefined' ||
164+
typeof fields.username.initialError !== 'undefined',
166165
}}
167166
errors={fields.username.errors}
168167
/>
@@ -201,7 +200,10 @@ export default function OnboardingPage() {
201200
children:
202201
'Do you agree to our Terms of Service and Privacy Policy?',
203202
}}
204-
buttonProps={conform.input(fields.agreeToTermsOfServiceAndPrivacyPolicy, { type: 'checkbox' })}
203+
buttonProps={conform.input(
204+
fields.agreeToTermsOfServiceAndPrivacyPolicy,
205+
{ type: 'checkbox' },
206+
)}
205207
errors={fields.agreeToTermsOfServiceAndPrivacyPolicy.errors}
206208
/>
207209

@@ -211,7 +213,9 @@ export default function OnboardingPage() {
211213
children:
212214
'Would you like to receive special discounts and offers?',
213215
}}
214-
buttonProps={conform.input(fields.agreeToMailingList, { type: 'checkbox' })}
216+
buttonProps={conform.input(fields.agreeToMailingList, {
217+
type: 'checkbox',
218+
})}
215219
errors={fields.agreeToMailingList.errors}
216220
/>
217221

@@ -224,7 +228,11 @@ export default function OnboardingPage() {
224228
errors={fields.remember.errors}
225229
/>
226230

227-
<input name={fields.redirectTo.name} type="hidden" value={redirectTo} />
231+
<input
232+
name={fields.redirectTo.name}
233+
type="hidden"
234+
value={redirectTo}
235+
/>
228236

229237
<ErrorList errors={data.formError ? [data.formError] : []} />
230238
<ErrorList errors={form.errors} id={form.errorId} />

app/routes/_auth+/reset-password.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import {
1313
} from '@remix-run/react'
1414
import { z } from 'zod'
1515
import { GeneralErrorBoundary } from '~/components/error-boundary.tsx'
16-
import { authenticator, resetUserPassword } from '~/utils/auth.server.ts'
16+
import {
17+
authenticator,
18+
requireAnonymous,
19+
resetUserPassword,
20+
} from '~/utils/auth.server.ts'
1721
import { Button, ErrorList, Field } from '~/utils/forms.tsx'
1822
import { conform, useForm } from '@conform-to/react'
1923
import { getFieldsetConstraint, parse } from '@conform-to/zod'
@@ -32,9 +36,7 @@ const ResetPasswordSchema = z
3236
})
3337

3438
export async function loader({ request }: DataFunctionArgs) {
35-
await authenticator.isAuthenticated(request, {
36-
successRedirect: '/',
37-
})
39+
await requireAnonymous(request)
3840
const session = await getSession(request.headers.get('cookie'))
3941
const error = session.get(authenticator.sessionErrorKey)
4042
const resetPasswordUsername = session.get(resetPasswordSessionKey)

app/routes/resources+/login.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ export async function action({ request }: DataFunctionArgs) {
3535
)
3636
}
3737

38-
let userId: string | null = null
38+
let sessionId: string | null = null
3939
try {
40-
userId = await authenticator.authenticate(FormStrategy.name, request, {
40+
sessionId = await authenticator.authenticate(FormStrategy.name, request, {
4141
throwOnError: true,
4242
})
4343
} catch (error) {
@@ -60,7 +60,7 @@ export async function action({ request }: DataFunctionArgs) {
6060
}
6161

6262
const session = await getSession(request.headers.get('cookie'))
63-
session.set(authenticator.sessionKey, userId)
63+
session.set(authenticator.sessionKey, sessionId)
6464
const { remember, redirectTo } = submission.value
6565
const newCookie = await commitSession(session, {
6666
maxAge: remember
@@ -143,7 +143,11 @@ export function InlineLogin({
143143
</div>
144144
</div>
145145

146-
<input value={redirectTo} {...fields.redirectTo} type="hidden" />
146+
<input
147+
value={redirectTo}
148+
{...conform.input(fields.redirectTo)}
149+
type="hidden"
150+
/>
147151
<ErrorList errors={formError ? [formError] : []} />
148152
<ErrorList errors={form.errors} id={form.errorId} />
149153

app/routes/settings+/profile.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export default function EditUserProfile() {
147147
return parse(formData, { schema: ProfileFormSchema })
148148
},
149149
defaultValue: {
150+
username: data.user.username,
150151
name: data.user.name ?? '',
151152
email: data.user.email,
152153
},
@@ -188,10 +189,7 @@ export default function EditUserProfile() {
188189
htmlFor: fields.username.id,
189190
children: 'Username',
190191
}}
191-
inputProps={{
192-
...fields.username,
193-
defaultValue: data.user.username,
194-
}}
192+
inputProps={conform.input(fields.username)}
195193
errors={fields.username.errors}
196194
/>
197195
<Field
@@ -224,7 +222,9 @@ export default function EditUserProfile() {
224222
children: 'Current Password',
225223
}}
226224
inputProps={{
227-
...conform.input(fields.currentPassword, { type: 'password' }),
225+
...conform.input(fields.currentPassword, {
226+
type: 'password',
227+
}),
228228
autoComplete: 'current-password',
229229
}}
230230
errors={fields.currentPassword.errors}

app/utils/auth.server.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import { FormStrategy } from 'remix-auth-form'
55
import invariant from 'tiny-invariant'
66
import { prisma } from '~/utils/db.server.ts'
77
import { sessionStorage } from './session.server.ts'
8+
import { redirect } from '@remix-run/node'
89

910
export type { User }
1011

1112
export const authenticator = new Authenticator<string>(sessionStorage, {
12-
sessionKey: 'userId',
13+
sessionKey: 'sessionId',
1314
})
1415

16+
const SESSION_EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 30
17+
1518
authenticator.use(
1619
new FormStrategy(async ({ form }) => {
1720
const username = form.get('username')
@@ -27,8 +30,15 @@ authenticator.use(
2730
if (!user) {
2831
throw new Error('Invalid username or password')
2932
}
33+
const session = await prisma.session.create({
34+
data: {
35+
expirationDate: new Date(Date.now() + SESSION_EXPIRATION_TIME),
36+
userId: user.id,
37+
},
38+
select: { id: true },
39+
})
3040

31-
return user.id
41+
return session.id
3242
}),
3343
FormStrategy.name,
3444
)
@@ -48,14 +58,35 @@ export async function requireUserId(
4858
const failureRedirect = ['/login', loginParams?.toString()]
4959
.filter(Boolean)
5060
.join('?')
51-
const userId = await authenticator.isAuthenticated(request, {
61+
const sessionId = await authenticator.isAuthenticated(request, {
5262
failureRedirect,
5363
})
54-
return userId
64+
const session = await prisma.session.findFirst({
65+
where: { id: sessionId },
66+
select: { userId: true, expirationDate: true },
67+
})
68+
if (!session) {
69+
throw redirect(failureRedirect)
70+
}
71+
console.log(session.expirationDate, session.expirationDate > new Date())
72+
return session.userId
5573
}
5674

5775
export async function getUserId(request: Request) {
58-
return authenticator.isAuthenticated(request)
76+
const sessionId = await authenticator.isAuthenticated(request)
77+
if (!sessionId) return null
78+
const session = await prisma.session.findUnique({
79+
where: { id: sessionId },
80+
select: { userId: true },
81+
})
82+
if (!session) return null
83+
return session.userId
84+
}
85+
86+
export async function requireAnonymous(request: Request) {
87+
await authenticator.isAuthenticated(request, {
88+
successRedirect: '/',
89+
})
5990
}
6091

6192
export async function resetUserPassword({
@@ -78,7 +109,7 @@ export async function resetUserPassword({
78109
})
79110
}
80111

81-
export async function createUser({
112+
export async function signup({
82113
email,
83114
username,
84115
password,
@@ -91,18 +122,25 @@ export async function createUser({
91122
}) {
92123
const hashedPassword = await getPasswordHash(password)
93124

94-
return prisma.user.create({
125+
const session = await prisma.session.create({
95126
data: {
96-
email,
97-
username,
98-
name,
99-
password: {
127+
expirationDate: new Date(Date.now() + SESSION_EXPIRATION_TIME),
128+
user: {
100129
create: {
101-
hash: hashedPassword,
130+
email,
131+
username,
132+
name,
133+
password: {
134+
create: {
135+
hash: hashedPassword,
136+
},
137+
},
102138
},
103139
},
104140
},
141+
select: { id: true, expirationDate: true },
105142
})
143+
return session
106144
}
107145

108146
export async function getPasswordHash(password: string) {
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)