-
Notifications
You must be signed in to change notification settings - Fork 653
Expand file tree
/
Copy pathprisma.server.ts
More file actions
243 lines (223 loc) · 6.7 KB
/
prisma.server.ts
File metadata and controls
243 lines (223 loc) · 6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import { remember } from '@epic-web/remember'
import { PrismaBetterSQLite3 } from '@prisma/adapter-better-sqlite3'
import { PrismaClient } from '@prisma/client'
import chalk from 'chalk'
import pProps from 'p-props'
import { type Session } from '#app/types.ts'
import { ensurePrimary } from '#app/utils/litefs-js.server.ts'
import { decrypt, encrypt } from './encryption.server.ts'
import { time, type Timings } from './timing.server.ts'
const logThreshold = 500
const prisma = remember('prisma', getClient)
function getClient(): PrismaClient {
// NOTE: during development if you change anything in this function, remember
// that this only runs once per server restart and won't automatically be
// re-run per request like everything else is.
const client = new PrismaClient({
adapter: new PrismaBetterSQLite3({
url: process.env.DATABASE_URL!.replace('file:', ''),
}),
log: [
{ level: 'query', emit: 'event' },
{ level: 'error', emit: 'stdout' },
{ level: 'info', emit: 'stdout' },
{ level: 'warn', emit: 'stdout' },
],
})
client.$on('query', async (e) => {
if (e.duration < logThreshold) return
const color =
e.duration < logThreshold * 1.1
? 'green'
: e.duration < logThreshold * 1.2
? 'blue'
: e.duration < logThreshold * 1.3
? 'yellow'
: e.duration < logThreshold * 1.4
? 'redBright'
: 'red'
const dur = chalk[color](`${e.duration}ms`)
console.info(`prisma:query - ${dur} - ${e.query}`)
})
// make the connection eagerly so the first request doesn't have to wait
void client.$connect()
return client
}
const linkExpirationTime = 1000 * 60 * 30
const sessionExpirationTime = 1000 * 60 * 60 * 24 * 365
const magicLinkSearchParam = 'kodyKey'
type MagicLinkPayload = {
emailAddress: string
creationDate: string
validateSessionMagicLink: boolean
}
function getMagicLink({
emailAddress,
validateSessionMagicLink,
domainUrl,
}: {
emailAddress: string
validateSessionMagicLink: boolean
domainUrl: string
}) {
const payload: MagicLinkPayload = {
emailAddress,
validateSessionMagicLink,
creationDate: new Date().toISOString(),
}
const stringToEncrypt = JSON.stringify(payload)
const encryptedString = encrypt(stringToEncrypt)
const url = new URL(domainUrl)
url.pathname = 'magic'
url.searchParams.set(magicLinkSearchParam, encryptedString)
return url.toString()
}
function getMagicLinkCode(link: string) {
try {
const url = new URL(link)
return url.searchParams.get(magicLinkSearchParam) ?? ''
} catch {
return ''
}
}
async function validateMagicLink(link: string, sessionMagicLink?: string) {
const linkCode = getMagicLinkCode(link)
const sessionLinkCode = sessionMagicLink
? getMagicLinkCode(sessionMagicLink)
: null
let emailAddress, linkCreationDateString, validateSessionMagicLink
try {
const decryptedString = decrypt(linkCode)
const payload = JSON.parse(decryptedString) as MagicLinkPayload
emailAddress = payload.emailAddress
linkCreationDateString = payload.creationDate
validateSessionMagicLink = payload.validateSessionMagicLink
} catch (error: unknown) {
console.error(error)
throw new Error(
'Sign in link invalid (link payload is invalid). Please request a new one.',
)
}
if (typeof emailAddress !== 'string') {
console.error(`Email is not a string. Maybe wasn't set in the session?`)
throw new Error(
'Sign in link invalid (email is not a string). Please request a new one.',
)
}
if (validateSessionMagicLink) {
if (!sessionLinkCode) {
console.error(
'Must validate session magic link but no session link provided',
)
throw new Error(
'Sign in link invalid. No link validation cookie was found (does your browser block cookies or did you open the link in a different browser?). Please request a new link.',
)
}
if (linkCode !== sessionLinkCode) {
console.error(`Magic link does not match sessionMagicLink`)
throw new Error(
`You must open the magic link on the same device it was created from for security reasons. Please request a new link.`,
)
}
}
if (typeof linkCreationDateString !== 'string') {
console.error('Link expiration is not a string.')
throw new Error(
'Sign in link invalid (link expiration is not a string). Please request a new one.',
)
}
const linkCreationDate = new Date(linkCreationDateString)
const expirationTime = linkCreationDate.getTime() + linkExpirationTime
if (Date.now() > expirationTime) {
throw new Error('Magic link expired. Please request a new one.')
}
return emailAddress
}
async function createSession(
sessionData: Omit<Session, 'id' | 'expirationDate' | 'createdAt'>,
) {
await ensurePrimary()
return prisma.session.create({
data: {
...sessionData,
expirationDate: new Date(Date.now() + sessionExpirationTime),
},
})
}
async function getUserFromSessionId(
sessionId: string,
{ timings }: { timings?: Timings } = {},
) {
const session = await time(
prisma.session.findUnique({
where: { id: sessionId },
include: { user: true },
}),
{ timings, type: 'getUserFromSessionId' },
)
if (!session) {
throw new Error('No user found')
}
if (Date.now() > session.expirationDate.getTime()) {
await ensurePrimary()
await prisma.session.delete({ where: { id: sessionId } })
throw new Error('Session expired. Please request a new magic link.')
}
// if there's less than ~six months left, extend the session
const twoWeeks = 1000 * 60 * 60 * 24 * 30 * 6
if (Date.now() + twoWeeks > session.expirationDate.getTime()) {
await ensurePrimary()
const newExpirationDate = new Date(Date.now() + sessionExpirationTime)
await prisma.session.update({
data: { expirationDate: newExpirationDate },
where: { id: sessionId },
})
}
return session.user
}
async function getAllUserData(userId: string) {
return pProps({
user: prisma.user.findUnique({ where: { id: userId } }),
calls: prisma.call.findMany({ where: { userId } }),
postReads: prisma.postRead.findMany({ where: { userId } }),
sessions: prisma.session.findMany({ where: { userId } }),
})
}
async function addPostRead({
slug,
userId,
clientId,
}: { slug: string } & (
| { userId: string; clientId?: undefined }
| { userId?: undefined; clientId: string }
)) {
const id = userId ? { userId } : { clientId }
const readInLastWeek = await prisma.postRead.findFirst({
select: { id: true },
where: {
...id,
postSlug: slug,
createdAt: { gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7) },
},
})
if (readInLastWeek) {
return null
} else {
const postRead = await prisma.postRead.create({
data: { postSlug: slug, ...id },
select: { id: true },
})
return postRead
}
}
export {
addPostRead,
createSession,
getAllUserData,
getMagicLink,
getUserFromSessionId,
linkExpirationTime,
prisma,
sessionExpirationTime,
validateMagicLink,
}