opensoma 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +6 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +4 -0
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +6 -0
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/index.d.ts +1 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +1 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/toz.d.ts +16 -0
- package/dist/src/commands/toz.d.ts.map +1 -0
- package/dist/src/commands/toz.js +488 -0
- package/dist/src/commands/toz.js.map +1 -0
- package/dist/src/credential-manager.d.ts +8 -2
- package/dist/src/credential-manager.d.ts.map +1 -1
- package/dist/src/credential-manager.js +27 -5
- package/dist/src/credential-manager.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/session-recovery.js +2 -0
- package/dist/src/session-recovery.js.map +1 -1
- package/dist/src/shared/utils/config-dir.d.ts +11 -0
- package/dist/src/shared/utils/config-dir.d.ts.map +1 -0
- package/dist/src/shared/utils/config-dir.js +19 -0
- package/dist/src/shared/utils/config-dir.js.map +1 -0
- package/dist/src/toz-client.d.ts +89 -0
- package/dist/src/toz-client.d.ts.map +1 -0
- package/dist/src/toz-client.js +204 -0
- package/dist/src/toz-client.js.map +1 -0
- package/dist/src/toz-pending-store.d.ts +30 -0
- package/dist/src/toz-pending-store.d.ts.map +1 -0
- package/dist/src/toz-pending-store.js +36 -0
- package/dist/src/toz-pending-store.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +6 -3
- package/src/client.ts +10 -0
- package/src/commands/helpers.test.ts +4 -0
- package/src/commands/index.ts +1 -0
- package/src/commands/toz.test.ts +51 -0
- package/src/commands/toz.ts +607 -0
- package/src/credential-manager.test.ts +54 -0
- package/src/credential-manager.ts +28 -5
- package/src/index.ts +13 -0
- package/src/session-recovery.ts +2 -0
- package/src/shared/utils/config-dir.test.ts +41 -0
- package/src/shared/utils/config-dir.ts +20 -0
- package/src/toz-client.test.ts +243 -0
- package/src/toz-client.ts +311 -0
- package/src/toz-pending-store.test.ts +91 -0
- package/src/toz-pending-store.ts +63 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertDurationInRange,
|
|
3
|
+
buildBranchIdsParam,
|
|
4
|
+
buildStartTimeParam,
|
|
5
|
+
formatDuration,
|
|
6
|
+
formatPhone,
|
|
7
|
+
parseEmail,
|
|
8
|
+
parsePhone,
|
|
9
|
+
} from './shared/utils/toz'
|
|
10
|
+
import {
|
|
11
|
+
isReservationConfirmSuccess,
|
|
12
|
+
parseBookingPageBranches,
|
|
13
|
+
parseBookingPageMeetings,
|
|
14
|
+
parseMypageReservations,
|
|
15
|
+
parseTozBoothes,
|
|
16
|
+
parseTozDurations,
|
|
17
|
+
parseTozReserved,
|
|
18
|
+
} from './toz-formatters'
|
|
19
|
+
import { TozHttp, type TozHttpOptions } from './toz-http'
|
|
20
|
+
import type { TozBooth, TozBranch, TozDuration, TozMeeting, TozReservation, TozReserved } from './types'
|
|
21
|
+
|
|
22
|
+
export interface TozAvailabilityQuery {
|
|
23
|
+
date: string
|
|
24
|
+
startTime: string
|
|
25
|
+
durationMinutes: number
|
|
26
|
+
userCount: number
|
|
27
|
+
branchIds: readonly number[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TozCheckQuery {
|
|
31
|
+
date: string
|
|
32
|
+
startTimes: readonly string[]
|
|
33
|
+
durationMinutes: number
|
|
34
|
+
userCount: number
|
|
35
|
+
branchIds: readonly number[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TozCheckResult {
|
|
39
|
+
startTime: string
|
|
40
|
+
booths: TozBooth[]
|
|
41
|
+
error?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TozReserveBoothArgs {
|
|
45
|
+
date: string
|
|
46
|
+
startTime: string
|
|
47
|
+
durationMinutes: number
|
|
48
|
+
userCount: number
|
|
49
|
+
boothId: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TozConfirmArgs {
|
|
53
|
+
reservationId: string
|
|
54
|
+
date: string
|
|
55
|
+
startTime: string
|
|
56
|
+
durationMinutes: number
|
|
57
|
+
name: string
|
|
58
|
+
phone: string
|
|
59
|
+
email: string
|
|
60
|
+
pinNum: string
|
|
61
|
+
meetingId?: number
|
|
62
|
+
newMeetingName?: string
|
|
63
|
+
memo?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TozSearchArgs {
|
|
67
|
+
name?: string
|
|
68
|
+
phone?: string
|
|
69
|
+
startDate?: string
|
|
70
|
+
endDate?: string
|
|
71
|
+
meetingName?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TozCancelArgs {
|
|
75
|
+
reservationId: number
|
|
76
|
+
name?: string
|
|
77
|
+
phone?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface TozClientOptions extends TozHttpOptions {
|
|
81
|
+
name?: string
|
|
82
|
+
phone?: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class TozClient {
|
|
86
|
+
readonly http: TozHttp
|
|
87
|
+
readonly identity: { name?: string; phone?: string }
|
|
88
|
+
|
|
89
|
+
constructor(options: TozClientOptions = {}) {
|
|
90
|
+
const { name, phone, ...httpOptions } = options
|
|
91
|
+
this.http = new TozHttp(httpOptions)
|
|
92
|
+
this.identity = { name, phone }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async branches(): Promise<TozBranch[]> {
|
|
96
|
+
await this.http.bootstrap()
|
|
97
|
+
return parseBookingPageBranches(await this.http.get('/booking.htm'))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async meetings(): Promise<TozMeeting[]> {
|
|
101
|
+
await this.http.bootstrap()
|
|
102
|
+
return parseBookingPageMeetings(await this.http.get('/booking.htm'))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async durations(date: string, startTime: string): Promise<TozDuration[]> {
|
|
106
|
+
await this.http.bootstrap()
|
|
107
|
+
const param = buildStartTimeParam(startTime)
|
|
108
|
+
const json = await this.http.postJson<unknown>('/ajaxCheckDurationTime.htm', {
|
|
109
|
+
basedate: date,
|
|
110
|
+
hour: param.slice(0, 2),
|
|
111
|
+
minute: param.slice(2, 4),
|
|
112
|
+
})
|
|
113
|
+
return parseTozDurations(json)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async available(query: TozAvailabilityQuery): Promise<TozBooth[]> {
|
|
117
|
+
assertDurationInRange(query.durationMinutes)
|
|
118
|
+
await this.http.bootstrap()
|
|
119
|
+
const json = await this.http.postJson<unknown>('/ajaxGetEnableBoothes.htm', {
|
|
120
|
+
basedate: query.date,
|
|
121
|
+
starttime: buildStartTimeParam(query.startTime),
|
|
122
|
+
durationTime: formatDuration(query.durationMinutes),
|
|
123
|
+
userCount: String(query.userCount),
|
|
124
|
+
branchIds: buildBranchIdsParam(query.branchIds),
|
|
125
|
+
})
|
|
126
|
+
return parseTozBoothes(json)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async check(query: TozCheckQuery): Promise<TozCheckResult[]> {
|
|
130
|
+
if (query.startTimes.length === 0) {
|
|
131
|
+
throw new Error('toz check requires at least one --time. For a single slot, use `toz available` instead.')
|
|
132
|
+
}
|
|
133
|
+
if (query.startTimes.length > 6) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
'Too many times (max 6 per invocation). Consider `toz available` for a single slot, or split into multiple invocations.',
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
assertDurationInRange(query.durationMinutes)
|
|
139
|
+
await this.http.bootstrap()
|
|
140
|
+
|
|
141
|
+
const results: TozCheckResult[] = []
|
|
142
|
+
for (let i = 0; i < query.startTimes.length; i += 1) {
|
|
143
|
+
const startTime = query.startTimes[i]!
|
|
144
|
+
try {
|
|
145
|
+
const booths = await this.available({
|
|
146
|
+
date: query.date,
|
|
147
|
+
startTime,
|
|
148
|
+
durationMinutes: query.durationMinutes,
|
|
149
|
+
userCount: query.userCount,
|
|
150
|
+
branchIds: query.branchIds,
|
|
151
|
+
})
|
|
152
|
+
results.push({ startTime, booths })
|
|
153
|
+
} catch (error) {
|
|
154
|
+
results.push({
|
|
155
|
+
startTime,
|
|
156
|
+
booths: [],
|
|
157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
if (i < query.startTimes.length - 1) await sleep(500)
|
|
161
|
+
}
|
|
162
|
+
return results
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async reserveBooth(args: TozReserveBoothArgs): Promise<TozReserved> {
|
|
166
|
+
assertDurationInRange(args.durationMinutes)
|
|
167
|
+
await this.http.bootstrap()
|
|
168
|
+
const json = await this.http.postJson<unknown>('/ajaxReservationBooth.htm', {
|
|
169
|
+
basedate: args.date,
|
|
170
|
+
starttime: buildStartTimeParam(args.startTime),
|
|
171
|
+
durationTime: formatDuration(args.durationMinutes),
|
|
172
|
+
userCount: String(args.userCount),
|
|
173
|
+
booth_id: String(args.boothId),
|
|
174
|
+
})
|
|
175
|
+
const reserved = parseTozReserved(json)
|
|
176
|
+
if (reserved.isLargeBooth) {
|
|
177
|
+
throw new Error('대형부스 예약은 선입금 약관 동의가 필요합니다. v1에서는 지원하지 않습니다 (small booths only).')
|
|
178
|
+
}
|
|
179
|
+
return reserved
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async skipEquipment(args: {
|
|
183
|
+
reservationId: string
|
|
184
|
+
date: string
|
|
185
|
+
startTime: string
|
|
186
|
+
durationMinutes: number
|
|
187
|
+
}): Promise<void> {
|
|
188
|
+
await this.http.postText('/ajaxReservationEquipment.htm', {
|
|
189
|
+
reservationId: args.reservationId,
|
|
190
|
+
basedate: args.date,
|
|
191
|
+
starttime: buildStartTimeParam(args.startTime),
|
|
192
|
+
durationTime: formatDuration(args.durationMinutes),
|
|
193
|
+
equipmentNotebookChecked: 'false',
|
|
194
|
+
equipmentProjectorChecked: 'false',
|
|
195
|
+
equipmentMonitorChecked: 'false',
|
|
196
|
+
equipmentPlayerChecked: 'false',
|
|
197
|
+
equipmentSpeakerChecked: 'false',
|
|
198
|
+
equipmentNotebookCnt: '0',
|
|
199
|
+
equipmentProjectorCnt: '0',
|
|
200
|
+
equipmentMonitorCnt: '0',
|
|
201
|
+
equipmentPlayerCnt: '0',
|
|
202
|
+
equipmentSpeakerCnt: '0',
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async sendOtp(phone?: string): Promise<void> {
|
|
207
|
+
const parts = parsePhone(this.requirePhone(phone))
|
|
208
|
+
const result = await this.http.postText('/ajaxHpVerify.htm', { ...parts })
|
|
209
|
+
if (result !== 'SUCCESS') {
|
|
210
|
+
throw new Error(`OTP 발송 실패: ${result}`)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async confirm(args: TozConfirmArgs): Promise<{ resultMsg: string; isLargeBooth: boolean }> {
|
|
215
|
+
if (!args.meetingId && !args.newMeetingName) {
|
|
216
|
+
throw new Error('meetingId 또는 newMeetingName 중 하나는 필수입니다.')
|
|
217
|
+
}
|
|
218
|
+
const phoneParts = parsePhone(args.phone)
|
|
219
|
+
const emailParts = parseEmail(args.email)
|
|
220
|
+
|
|
221
|
+
const json = await this.http.postJson<{ resultMsg?: string; message3?: string | null }>(
|
|
222
|
+
'/ajaxReservationConfirm.htm',
|
|
223
|
+
{
|
|
224
|
+
reservationId: args.reservationId,
|
|
225
|
+
phone: formatPhone(phoneParts),
|
|
226
|
+
...phoneParts,
|
|
227
|
+
name: args.name,
|
|
228
|
+
pinNum: args.pinNum,
|
|
229
|
+
...emailParts,
|
|
230
|
+
meeting_id: args.meetingId ? String(args.meetingId) : '새모임',
|
|
231
|
+
newMeetingName: args.newMeetingName ?? '',
|
|
232
|
+
prepareMemo: args.memo ?? '',
|
|
233
|
+
projectSeq: '',
|
|
234
|
+
tozApplyType: '',
|
|
235
|
+
addedInfo: '',
|
|
236
|
+
attendType: '',
|
|
237
|
+
isMobile: 'false',
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const resultMsg = json.resultMsg ?? ''
|
|
242
|
+
if (!isReservationConfirmSuccess(resultMsg)) {
|
|
243
|
+
throw new Error(resultMsg || '예약 확정에 실패했습니다.')
|
|
244
|
+
}
|
|
245
|
+
return { resultMsg, isLargeBooth: resultMsg.includes('대형부스') }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async destroyHold(): Promise<void> {
|
|
249
|
+
await this.http.postText('/ajaxDestroyReservation.htm', {})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async myReservations(args: TozSearchArgs = {}): Promise<TozReservation[]> {
|
|
253
|
+
await this.http.bootstrap()
|
|
254
|
+
await this.mypageLogin(this.requireName(args.name), this.requirePhone(args.phone))
|
|
255
|
+
|
|
256
|
+
const html = await this.http.post('/mypage.htm', {
|
|
257
|
+
opage: '1',
|
|
258
|
+
rpage: '1',
|
|
259
|
+
key: '',
|
|
260
|
+
projectSeq: '',
|
|
261
|
+
tozApplyType: '',
|
|
262
|
+
addedInfo: '',
|
|
263
|
+
startdate: args.startDate ?? '',
|
|
264
|
+
enddate: args.endDate ?? '',
|
|
265
|
+
meetingName: args.meetingName ?? '',
|
|
266
|
+
})
|
|
267
|
+
return parseMypageReservations(html)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async cancel(args: TozCancelArgs): Promise<void> {
|
|
271
|
+
await this.http.bootstrap()
|
|
272
|
+
await this.mypageLogin(this.requireName(args.name), this.requirePhone(args.phone))
|
|
273
|
+
|
|
274
|
+
const result = await this.http.postText('/ajaxCancelReservation.htm', {
|
|
275
|
+
reservation_id: String(args.reservationId),
|
|
276
|
+
})
|
|
277
|
+
if (result !== 'SUCCESS') {
|
|
278
|
+
if (result === 'FAILED') throw new Error('취소하지 못했습니다.')
|
|
279
|
+
throw new Error(result || '취소하지 못했습니다.')
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private requireName(name: string | undefined): string {
|
|
284
|
+
const resolved = name ?? this.identity.name
|
|
285
|
+
if (!resolved) throw new Error('Toz name is required (pass via args or TozClient({ name }))')
|
|
286
|
+
return resolved
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private requirePhone(phone: string | undefined): string {
|
|
290
|
+
const resolved = phone ?? this.identity.phone
|
|
291
|
+
if (!resolved) throw new Error('Toz phone is required (pass via args or TozClient({ phone }))')
|
|
292
|
+
return resolved
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async mypageLogin(name: string, phone: string): Promise<void> {
|
|
296
|
+
const parts = parsePhone(phone)
|
|
297
|
+
await this.http.post('/mypage_login_.htm', {
|
|
298
|
+
key: '',
|
|
299
|
+
projectSeq: '',
|
|
300
|
+
tozApplyType: '',
|
|
301
|
+
addedInfo: '',
|
|
302
|
+
email: '',
|
|
303
|
+
name,
|
|
304
|
+
...parts,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function sleep(ms: number): Promise<void> {
|
|
310
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
311
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtempSync } from 'node:fs'
|
|
3
|
+
import { readdir, stat } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { TozPendingStore, type TozPendingReservation } from './toz-pending-store'
|
|
8
|
+
|
|
9
|
+
let configDir: string
|
|
10
|
+
const cleanups: (() => void)[] = []
|
|
11
|
+
|
|
12
|
+
function freshStore(): TozPendingStore {
|
|
13
|
+
configDir = mkdtempSync(join(tmpdir(), 'toz-pending-'))
|
|
14
|
+
cleanups.push(() => {
|
|
15
|
+
require('node:fs').rmSync(configDir, { recursive: true, force: true })
|
|
16
|
+
})
|
|
17
|
+
return new TozPendingStore(configDir)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
while (cleanups.length > 0) cleanups.pop()?.()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const sample: TozPendingReservation = {
|
|
25
|
+
reservationId: 'abc-123',
|
|
26
|
+
cookies: { JSESSIONID: 'XYZ' },
|
|
27
|
+
branchName: '토즈타워점',
|
|
28
|
+
branchTel: '02-3454-0116',
|
|
29
|
+
boothGroupName: '2인부스 A타입',
|
|
30
|
+
isLargeBooth: false,
|
|
31
|
+
date: '2026-04-21',
|
|
32
|
+
startTime: '14:00',
|
|
33
|
+
endTime: '16:00',
|
|
34
|
+
durationMinutes: 120,
|
|
35
|
+
userCount: 2,
|
|
36
|
+
boothId: 740,
|
|
37
|
+
meetingId: 2305094,
|
|
38
|
+
email: 'me@gmail.com',
|
|
39
|
+
name: '홍길동',
|
|
40
|
+
phone: '010-1234-5678',
|
|
41
|
+
createdAt: '2026-04-17T18:42:13.291Z',
|
|
42
|
+
expiresAt: '2026-04-17T18:47:13.291Z',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('TozPendingStore', () => {
|
|
46
|
+
test('returns null when no pending file exists', async () => {
|
|
47
|
+
const store = freshStore()
|
|
48
|
+
expect(await store.get()).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('round-trips a reservation through set/get', async () => {
|
|
52
|
+
const store = freshStore()
|
|
53
|
+
await store.set(sample)
|
|
54
|
+
expect(await store.get()).toEqual(sample)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('clear removes the file', async () => {
|
|
58
|
+
const store = freshStore()
|
|
59
|
+
await store.set(sample)
|
|
60
|
+
await store.clear()
|
|
61
|
+
expect(await store.get()).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('clear is no-op when file does not exist', async () => {
|
|
65
|
+
const store = freshStore()
|
|
66
|
+
await store.clear()
|
|
67
|
+
expect(await store.get()).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('overwrites an existing reservation', async () => {
|
|
71
|
+
const store = freshStore()
|
|
72
|
+
await store.set(sample)
|
|
73
|
+
const updated = { ...sample, reservationId: 'new-id' }
|
|
74
|
+
await store.set(updated)
|
|
75
|
+
expect(await store.get()).toEqual(updated)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('writes file with 0o600 permissions at creation (no widened window)', async () => {
|
|
79
|
+
const store = freshStore()
|
|
80
|
+
await store.set(sample)
|
|
81
|
+
const fileStat = await stat(join(configDir, 'toz-pending.json'))
|
|
82
|
+
expect(fileStat.mode & 0o777).toBe(0o600)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('leaves no temp file behind after set', async () => {
|
|
86
|
+
const store = freshStore()
|
|
87
|
+
await store.set(sample)
|
|
88
|
+
const entries = await readdir(configDir)
|
|
89
|
+
expect(entries).toEqual(['toz-pending.json'])
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { getConfigDir } from './shared/utils/config-dir'
|
|
6
|
+
|
|
7
|
+
export interface TozPendingReservation {
|
|
8
|
+
reservationId: string
|
|
9
|
+
cookies: Record<string, string>
|
|
10
|
+
branchName: string
|
|
11
|
+
branchTel: string
|
|
12
|
+
boothGroupName: string
|
|
13
|
+
isLargeBooth: boolean
|
|
14
|
+
date: string
|
|
15
|
+
startTime: string
|
|
16
|
+
endTime: string
|
|
17
|
+
durationMinutes: number
|
|
18
|
+
userCount: number
|
|
19
|
+
boothId: number
|
|
20
|
+
meetingId?: number
|
|
21
|
+
newMeetingName?: string
|
|
22
|
+
email: string
|
|
23
|
+
memo?: string
|
|
24
|
+
name: string
|
|
25
|
+
phone: string
|
|
26
|
+
createdAt: string
|
|
27
|
+
expiresAt: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class TozPendingStore {
|
|
31
|
+
private readonly path: string
|
|
32
|
+
|
|
33
|
+
constructor(configDir?: string) {
|
|
34
|
+
const dir = configDir ?? getConfigDir()
|
|
35
|
+
this.path = join(dir, 'toz-pending.json')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async get(): Promise<TozPendingReservation | null> {
|
|
39
|
+
if (!existsSync(this.path)) return null
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const raw = await readFile(this.path, 'utf8')
|
|
43
|
+
return JSON.parse(raw) as TozPendingReservation
|
|
44
|
+
} catch {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async set(reservation: TozPendingReservation): Promise<void> {
|
|
50
|
+
const dir = dirname(this.path)
|
|
51
|
+
await mkdir(dir, { recursive: true })
|
|
52
|
+
// Write to a temp file with 0o600 from creation, then rename atomically.
|
|
53
|
+
// Avoids the default 0o644 window between writeFile() and chmod() that
|
|
54
|
+
// would briefly expose the cookie + phone number to other users on the system.
|
|
55
|
+
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}`
|
|
56
|
+
await writeFile(tmp, JSON.stringify(reservation, null, 2), { mode: 0o600 })
|
|
57
|
+
await rename(tmp, this.path)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async clear(): Promise<void> {
|
|
61
|
+
await rm(this.path, { force: true })
|
|
62
|
+
}
|
|
63
|
+
}
|