Skip to content

Commit 8e513c5

Browse files
同一アカウントでのマルチログインに対応する #34
1 parent dbcf09d commit 8e513c5

File tree

8 files changed

+197
-93
lines changed

8 files changed

+197
-93
lines changed

prisma/schema.prisma

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ datasource db {
1010
url = env("DATABASE_URL")
1111
}
1212

13+
model AppSetting {
14+
id Int @id @default(autoincrement())
15+
created_at DateTime @default(now())
16+
updated_at DateTime @updatedAt
17+
key String @unique
18+
value String
19+
}
20+
1321
model Role {
1422
id Int @id @default(autoincrement())
1523
created_at DateTime @default(now())
@@ -33,9 +41,11 @@ model AuthToken {
3341
id Int @id @default(autoincrement())
3442
created_at DateTime @default(now())
3543
updated_at DateTime @updatedAt
36-
user_id Int @unique
44+
user_id Int
3745
token String @unique
3846
user User @relation(fields: [user_id], references: [id])
47+
48+
@@index(updated_at)
3949
}
4050

4151
model AuthPin {

src/hooks.server.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,24 @@
1+
import { Auth } from '$lib/auth'
12
import { CookiesManager } from '$lib/cookies_manager'
2-
import { db } from '$lib/database'
33
import type { Handle } from '@sveltejs/kit'
44

55
// export const handle: Handle = async ({ event, resolve }) => resolve(event)
66

77
export const handle: Handle = async ({ event, resolve }) => {
8-
const session_id = event.cookies.get('session_id')
9-
8+
const cookiesManager = new CookiesManager(event.cookies)
9+
const session_id = cookiesManager.session_id
1010
if (!session_id) return await resolve(event)
1111

12-
const auth_token = await db.authToken.findUnique({
13-
where: { token: session_id },
14-
include: {
15-
user: {
16-
include: {
17-
role: true,
18-
},
19-
},
20-
},
21-
})
22-
12+
const auth_token = await Auth.findAuthToken(session_id)
2313
if (!auth_token) return await resolve(event)
2414

15+
await Auth.accessValid(auth_token.id, event.cookies)
16+
2517
event.locals.user = {
2618
email: auth_token.user.email,
2719
role: auth_token.user.role.name,
2820
}
2921

30-
new CookiesManager(event.cookies).setSessionId(auth_token.token);
31-
3222
return await resolve(event)
3323

3424
// console.log('🍌')

src/lib/auth.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type { AuthPin, AuthToken, Role, User } from '@prisma/client'
2+
import type { Cookies } from '@sveltejs/kit'
3+
import { CookiesManager } from './cookies_manager'
4+
import { Database, db } from './database'
5+
6+
export class Auth {
7+
public static async getSessionLifetimeSec(): Promise<number> {
8+
return await Database.getAppSettingInt('session_lifetime_sec')
9+
}
10+
11+
public static async getPinCodeLifetimeSec(): Promise<number> {
12+
return await Database.getAppSettingInt('pin_code_lifetime_sec')
13+
}
14+
15+
public static getLimit(lifetime_sec: number): Date {
16+
const limit = new Date()
17+
18+
if (lifetime_sec == 0) console.warn('lifetime_sec is 0')
19+
20+
limit.setSeconds(limit.getSeconds() - lifetime_sec)
21+
22+
return limit
23+
}
24+
25+
public static async createAuthToken(
26+
user_id: number,
27+
session_lifetime_sec: number
28+
): Promise<AuthToken> {
29+
const session_limit = await Auth.getLimit(session_lifetime_sec)
30+
31+
const [auth_token] = await db.$transaction([
32+
db.authToken.create({
33+
data: { user_id, token: crypto.randomUUID() },
34+
}),
35+
db.authToken.deleteMany({
36+
where: { updated_at: { lt: session_limit } },
37+
}),
38+
])
39+
40+
return auth_token
41+
}
42+
43+
public static async updateAuthToken(auth_token_id: number): Promise<AuthToken> {
44+
const auth_token = await db.authToken.update({
45+
where: { id: auth_token_id },
46+
data: { updated_at: new Date() },
47+
})
48+
49+
return auth_token
50+
}
51+
52+
public static async findAuthPin(email: string, pin_code: string): Promise<AuthPin | null> {
53+
const pin_lifetime_sec = await Auth.getPinCodeLifetimeSec()
54+
const pin_limit = Auth.getLimit(pin_lifetime_sec)
55+
56+
const auth_pin = await db.authPin.findFirst({
57+
where: {
58+
updated_at: { gte: pin_limit },
59+
pin_code,
60+
user: {
61+
email,
62+
},
63+
},
64+
})
65+
66+
return auth_pin
67+
}
68+
69+
public static async signIn(
70+
user_id: number,
71+
cookies: Cookies,
72+
pin_code_id?: number
73+
): Promise<void> {
74+
const session_lifetime_sec = await Auth.getSessionLifetimeSec()
75+
76+
const auth_token = await Auth.createAuthToken(user_id, session_lifetime_sec)
77+
78+
new CookiesManager(cookies).setSessionId(auth_token.token, session_lifetime_sec)
79+
80+
if (pin_code_id) {
81+
await db.authPin.delete({ where: { id: pin_code_id } })
82+
}
83+
}
84+
85+
public static async signOut(cookies: Cookies): Promise<void> {
86+
const cookiesManager = new CookiesManager(cookies)
87+
88+
await db.authToken.delete({ where: { token: cookiesManager.session_id } })
89+
cookiesManager.deleteSessionId()
90+
}
91+
92+
public static async accessValid(auth_token_id: number, cookies: Cookies): Promise<void> {
93+
const auth_token = await Auth.updateAuthToken(auth_token_id)
94+
const session_lifetime_sec = await Auth.getSessionLifetimeSec()
95+
96+
new CookiesManager(cookies).setSessionId(auth_token.token, session_lifetime_sec)
97+
}
98+
99+
public static async findAuthToken(session_id: string): Promise<
100+
| (AuthToken & {
101+
user: User & {
102+
role: Role
103+
}
104+
})
105+
| null
106+
> {
107+
const session_lifetime_sec = await Auth.getSessionLifetimeSec()
108+
const session_limit = Auth.getLimit(session_lifetime_sec)
109+
110+
const auth_token = await db.authToken.findFirst({
111+
where: {
112+
updated_at: { gte: session_limit },
113+
token: session_id,
114+
},
115+
include: {
116+
user: {
117+
include: {
118+
role: true,
119+
},
120+
},
121+
},
122+
})
123+
124+
return auth_token
125+
}
126+
}

src/lib/cookies_manager.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
import type { Cookies } from "@sveltejs/kit";
22

33
export class CookiesManager {
4+
private static readonly _session_id_key = 'session_id'
5+
46
public constructor(private readonly _cookies: Cookies) {}
57

6-
public setSessionId(session_id: string): void {
7-
this._cookies.set('session_id', session_id, {
8+
public get session_id(): string {
9+
return this._cookies.get(CookiesManager._session_id_key) ?? ''
10+
}
11+
12+
public setSessionId(session_id: string, max_age_sec: number): void {
13+
this._cookies.set(CookiesManager._session_id_key, session_id, {
814
path: '/',
9-
maxAge: 60 * 60 * 24 * 30,
15+
maxAge: max_age_sec,
1016
sameSite: 'lax',
1117
secure: true,
1218
// secure: process.env.NODE_ENV === 'production',
1319
httpOnly: true,
1420
})
1521
}
22+
23+
public deleteSessionId(): void {
24+
this._cookies.delete(CookiesManager._session_id_key)
25+
}
1626
}

src/lib/database.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,31 @@ enum Roles {
77

88
export const db = new prisma.PrismaClient()
99

10-
export async function findUser(email: string, can_register = true): Promise<User | undefined> {
11-
const user = await db.user.findUnique({ where: { email } })
10+
export class Database {
11+
public static async findUser(email: string, can_register = true): Promise<User | undefined> {
12+
const user = await db.user.findUnique({ where: { email } })
1213

13-
if (user) return user
14-
if (!can_register) return undefined
14+
if (user) return user
15+
if (!can_register) return undefined
1516

16-
try {
17-
return await db.user.create({
18-
data: {
19-
role: { connect: { name: Roles.user } },
20-
email,
21-
},
22-
})
23-
} catch (error) {
24-
console.error(error)
25-
return undefined
17+
try {
18+
return await db.user.create({
19+
data: {
20+
role: { connect: { name: Roles.user } },
21+
email,
22+
},
23+
})
24+
} catch (error) {
25+
console.error(error)
26+
return undefined
27+
}
28+
}
29+
30+
public static async getAppSettingInt(key: string): Promise<number> {
31+
const appSetting = await db.appSetting.findUnique({ where: { key } })
32+
const number_value = Number(appSetting?.value)
33+
const number_value_not_nan = Number.isNaN(number_value) ? 0 : number_value
34+
35+
return number_value_not_nan
2636
}
2737
}

src/routes/logout/+page.server.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { redirect, type Actions } from "@sveltejs/kit";
2-
import type { PageServerLoad } from "./$types";
1+
import { Auth } from '$lib/auth'
2+
import { redirect, type Actions } from '@sveltejs/kit'
3+
import type { PageServerLoad } from './$types'
34

45
export const load: PageServerLoad = async () => {
5-
throw redirect(302, "/")
6+
throw redirect(302, '/')
67
}
78

89
export const actions: Actions = {
910
default: async ({ cookies }) => {
10-
cookies.delete('session_id')
11+
Auth.signOut(cookies)
1112
throw redirect(302, '/')
12-
}
13+
},
1314
}

src/routes/pin_code/+page.server.ts

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { CookiesManager } from '$lib/cookies_manager'
2-
import { db, findUser } from '$lib/database'
1+
import { Auth } from '$lib/auth'
2+
import { Database, db } from '$lib/database'
33
import { NodemailerManager as NodeMailerManager } from '$lib/nodemailer_manager'
44
import type { PageServerLoad } from '.svelte-kit/types/src/routes/$types'
55
import type { User } from '@prisma/client'
@@ -70,7 +70,7 @@ export const actions: Actions = {
7070

7171
if (!email) throw redirect(302, '/')
7272

73-
const user = await findUser(email, true)
73+
const user = await Database.findUser(email, true)
7474

7575
if (!user) return { credentials: true, email, missing: false, success: false }
7676

@@ -94,38 +94,11 @@ export const actions: Actions = {
9494

9595
if (!email || !pin_code) return invalid(400, { missing: true, email })
9696

97-
const limit_date = new Date()
98-
99-
limit_date.setMinutes(limit_date.getMinutes() - 5)
100-
101-
const auth_pin = await db.authPin.findFirst({
102-
where: {
103-
pin_code,
104-
updated_at: { gt: limit_date },
105-
user: {
106-
email,
107-
},
108-
},
109-
})
97+
const auth_pin = await Auth.findAuthPin(email, pin_code)
11098

11199
if (!auth_pin) return invalid(400, { credentials: true, email })
112100

113-
const user_id = auth_pin.user_id
114-
115-
const [auth_token] = await db.$transaction([
116-
db.authToken.upsert({
117-
where: { user_id },
118-
update: { token: crypto.randomUUID() },
119-
create: { user_id, token: crypto.randomUUID() },
120-
}),
121-
db.authPin.delete({
122-
where: {
123-
id: auth_pin.id,
124-
},
125-
}),
126-
])
127-
128-
new CookiesManager(cookies).setSessionId(auth_token.token)
101+
await Auth.signIn(auth_pin.user_id, cookies, auth_pin.id)
129102

130103
return { success: true, email }
131104
},
@@ -147,19 +120,11 @@ export const actions: Actions = {
147120
console.log('Email: ' + payload.email)
148121

149122
const email = payload.email as string
150-
const user = await findUser(email, true)
123+
const user = await Database.findUser(email, true)
151124

152125
if (!user) return { credentials: true, email, missing: false }
153126

154-
const user_id = user.id
155-
156-
const auth_token = await db.authToken.upsert({
157-
where: { user_id },
158-
update: { token: crypto.randomUUID() },
159-
create: { user_id, token: crypto.randomUUID() },
160-
})
161-
162-
new CookiesManager(cookies).setSessionId(auth_token.token)
127+
await Auth.signIn(user.id, cookies)
163128

164129
throw redirect(302, '/login')
165130
},

0 commit comments

Comments
 (0)