-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsession.ts
More file actions
137 lines (124 loc) · 4.62 KB
/
session.ts
File metadata and controls
137 lines (124 loc) · 4.62 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
import Debug from 'debug'
import { RequestHandler } from 'express'
import { remove } from 'fs-extra'
import http from 'http'
import { join } from 'path'
import { Server } from 'socket.io'
import { ApiLockedError, ApiNotReadyError } from 'src/error'
import { OpenApiRequestExt } from 'src/otomi-models'
import { default as OtomiStack, rootPath } from 'src/otomi-stack'
import { API_NAMESPACE, cleanEnv, EDITOR_INACTIVITY_TIMEOUT } from 'src/validators'
import { v4 as uuidv4 } from 'uuid'
import { setApiStatusInConfigMap } from '../k8s-operations'
import { getSanitizedErrorMessage } from '../utils'
const debug = Debug('otomi:session')
const env = cleanEnv({
EDITOR_INACTIVITY_TIMEOUT,
API_NAMESPACE,
})
export type DbMessage = {
state: 'clean' | 'corrupt'
editor: string
reason: 'deploy' | 'revert' | 'conflict' | 'restored'
sha?: string
}
// instantiate read-only version of the stack
let readOnlyStack: OtomiStack
let sessions: Record<string, OtomiStack> = {}
// handler to get the correct stack for the user: if never touched any data give the main otomiStack
export const getSessionStack = async (sessionId?: string): Promise<OtomiStack> => {
if (!readOnlyStack) {
readOnlyStack = new OtomiStack()
await readOnlyStack.init()
}
if (!sessionId || !sessions[sessionId]) return readOnlyStack
return sessions[sessionId]
}
export const setSessionStack = async (editor: string, sessionId: string): Promise<OtomiStack> => {
if (env.isTest) return readOnlyStack
if (!sessions[sessionId]) {
debug(`Creating session ${sessionId} for user ${editor}`)
sessions[sessionId] = new OtomiStack(editor, sessionId)
await sessions[sessionId].initGitWorktree(readOnlyStack.git)
sessions[sessionId].fileStore.copyFrom(readOnlyStack.fileStore)
} else sessions[sessionId].sessionId = sessionId
return sessions[sessionId]
}
export const getEditors = () => Object.keys(sessions)
export const lockApi = async (): Promise<void> => {
if (!readOnlyStack) {
debug('readOnlyStack is not set')
}
readOnlyStack.setLocked(true)
if (process.env.NODE_ENV !== 'test') {
await setApiStatusInConfigMap(env.API_NAMESPACE, true)
}
}
export const cleanAllSessions = (): void => {
debug(`Cleaning all editor sessions`)
sessions = {}
// @ts-ignore
readOnlyStack = undefined
}
export const cleanSession = async (sessionId: string): Promise<void> => {
debug(`Cleaning session ${sessionId}`)
const session = sessions[sessionId]
const worktreePath = join(rootPath, sessionId)
if (session?.git) {
try {
await readOnlyStack.git.removeWorktree(worktreePath)
} catch (error) {
const errorMessage = getSanitizedErrorMessage(error)
debug(`Error removing worktree for session ${sessionId}: ${errorMessage}`)
await remove(worktreePath)
}
} else {
await remove(worktreePath)
}
delete sessions[sessionId]
}
let io: Server
export const getIo = () => io
// we use session middleware so we can give each user their own otomiStack
// with a snapshot of the db, the moment they start touching data
export function sessionMiddleware(server: http.Server): RequestHandler {
// socket setup - only create Socket.IO if we have a server and not in tests
if (!env.isTest && server) {
io = new Server(server, { path: '/ws' })
io.on('connection', (socket: any) => {
socket.on('error', console.error)
const users: any[] = []
for (const [id, { email }] of io.of('/').sockets as Map<string, any>) {
users.push({
id,
email,
})
}
socket.emit('users', users)
// notify existing users
socket.broadcast.emit('user connected', {
userID: socket.id,
email: socket.email,
})
})
}
return async function nextHandler(req: OpenApiRequestExt, res, next): Promise<any> {
if (!env.isTest && (!readOnlyStack || !readOnlyStack.isLoaded)) throw new ApiNotReadyError()
const { email } = req.user || {}
const roStack = await getSessionStack()
// eslint-disable-next-line no-param-reassign
req.otomi = roStack
if (['post', 'put', 'delete'].includes(req.method.toLowerCase())) {
// in the workloadCatalog endpoint(s), don't need to create a session
if (req.path === '/v1/workloadCatalog') return next()
// Block all write operations when the API is locked (git migration completed)
if (readOnlyStack?.locked) throw new ApiLockedError()
// bootstrap session stack with unique sessionId to manipulate data
const sessionId = uuidv4() as string
// eslint-disable-next-line no-param-reassign
req.otomi = await setSessionStack(email, sessionId)
return next()
}
return next()
}
}