blob: 450f33c7cf4ffe12083b3a632628bea2a67ac918 [file] [log] [blame]
Adrien Béraude74741b2021-04-19 13:22:54 -04001'use strict'
Larbi Gharibe9af9732021-03-31 15:08:01 +01002
Adrien Béraude74741b2021-04-19 13:22:54 -04003import dotenv from 'dotenv'
Adrien Béraudab519ff2022-05-03 15:34:48 -04004const env = dotenv.config()
Adrien Béraude74741b2021-04-19 13:22:54 -04005
6import { promises as fs } from 'fs'
7import http from 'http'
simon7a7b4d52022-09-23 02:09:42 -04008import express, {NextFunction, Response, Request} from 'express'
Adrien Béraude74741b2021-04-19 13:22:54 -04009import session from 'express-session'
Adrien Béraud4e287b92021-04-24 16:15:56 -040010import cookieParser from'cookie-parser'
simon7a7b4d52022-09-23 02:09:42 -040011import {Server, Socket} from 'socket.io'
Adrien Béraude74741b2021-04-19 13:22:54 -040012import path from 'path'
13import passport from 'passport'
14import { Strategy as LocalStrategy } from 'passport-local'
15//import { createRequire } from 'module';
16//const require = createRequire(import.meta.url);
Larbi Gharibe9af9732021-03-31 15:08:01 +010017
Adrien Béraud947e8792021-04-15 18:32:44 -040018//const redis = require('redis-url').connect()
19//const RedisStore = require('connect-redis')(session)
Adrien Béraud6ecaa402021-04-06 17:37:25 -040020/*const passportSocketIo = require('passport.socketio')*/
Larbi Gharibe9af9732021-03-31 15:08:01 +010021
Adrien Béraude74741b2021-04-19 13:22:54 -040022import indexRouter from './routes/index.js'
Adrien Béraud6ecaa402021-04-06 17:37:25 -040023
Adrien Béraud4e287b92021-04-24 16:15:56 -040024import cors from 'cors'
Larbi Gharibe9af9732021-03-31 15:08:01 +010025
Adrien Béraude74741b2021-04-19 13:22:54 -040026import JamiRestApi from './routes/jami.js'
27import JamiDaemon from './JamiDaemon.js'
simon7a7b4d52022-09-23 02:09:42 -040028import Account from "./model/Account";
29import {ExtendedError} from "socket.io/dist/namespace";
idillon8e6c0062022-09-16 13:34:43 -040030// import { sentrySetUp } from './sentry.js'
Adrien Béraude74741b2021-04-19 13:22:54 -040031
32const configPath = 'jamiServerConfig.json'
Larbi Gharibe9af9732021-03-31 15:08:01 +010033
Adrien Béraud6ecaa402021-04-06 17:37:25 -040034//const sessionStore = new RedisStore({ client: redis })
35const sessionStore = new session.MemoryStore()
Larbi Gharibe9af9732021-03-31 15:08:01 +010036
simon7a7b4d52022-09-23 02:09:42 -040037interface UserConfig {
38 accounts: string;
39 password?: string;
40 username?: string;
41 type?: string
42}
43
44interface AppConfig {
45 users: Record<string, UserConfig>
46 authMethods: any[]
47}
48
49const loadConfig = async (filePath: string): Promise<AppConfig> => {
Adrien Béraude74741b2021-04-19 13:22:54 -040050 const config = {users: {}, authMethods: []}
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040051 try {
simonc7d52452022-09-23 02:09:42 -040052 return Object.assign(config, JSON.parse((await fs.readFile(filePath)).toString()))
Adrien Béraude74741b2021-04-19 13:22:54 -040053 } catch(e) {
54 console.log(e)
55 return config
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040056 }
Adrien Béraud824a7132021-04-17 17:25:27 -040057}
58
simon7a7b4d52022-09-23 02:09:42 -040059const saveConfig = (filePath: string, config: AppConfig) => {
Adrien Béraude74741b2021-04-19 13:22:54 -040060 return fs.writeFile(filePath, JSON.stringify(config))
61}
62
Larbi Gharibe9af9732021-03-31 15:08:01 +010063/*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040064Share sessions between Passport.js and Socket.io
Larbi Gharibe9af9732021-03-31 15:08:01 +010065*/
66
67function logSuccess() {
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -040068 console.log('passportSocketIo authorized user with Success 😁')
Larbi Gharibe9af9732021-03-31 15:08:01 +010069}
70
71function logFail() {
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -040072 console.log('passportSocketIo failed to authorized user 👺')
Larbi Gharibe9af9732021-03-31 15:08:01 +010073}
74
75/*
Larbi Gharibe9af9732021-03-31 15:08:01 +010076
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040077tempAccounts holds users accounts while tempting to authenticate them on Jams.
78connectedUsers holds users accounts after they got authenticated by Jams.
Larbi Gharibe9af9732021-03-31 15:08:01 +010079
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040080Users should be removed from connectedUsers when receiving a disconnect
81web socket call
Larbi Gharibe9af9732021-03-31 15:08:01 +010082
83*/
simon7a7b4d52022-09-23 02:09:42 -040084const tempAccounts: Record<string, any> = {}
85const connectedUsers: Record<string, any> = {}
Larbi Gharibe9af9732021-03-31 15:08:01 +010086
simon7a7b4d52022-09-23 02:09:42 -040087const createServer = async (appConfig: AppConfig) => {
simonc7d52452022-09-23 02:09:42 -040088 const node_env = process.env.NODE_ENV || 'development'
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040089 const app = express()
simonc7d52452022-09-23 02:09:42 -040090 console.log(`Loading server for ${node_env} with config:`)
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040091 console.log(appConfig)
Larbi Gharibe9af9732021-03-31 15:08:01 +010092
Adrien Béraud4e287b92021-04-24 16:15:56 -040093 var corsOptions = {
94 origin: 'http://127.0.0.1:3000'
95 }
96
simonc7d52452022-09-23 02:09:42 -040097 if (node_env === 'development') {
98 const webpack = await import('webpack')
99 const webpackDev = await import('webpack-dev-middleware')
100 const webpackHot = await import ('webpack-hot-middleware')
simon7a7b4d52022-09-23 02:09:42 -0400101 const {default: webpackConfig} = await import('jami-web-client/webpack.config.js')
simonc7d52452022-09-23 02:09:42 -0400102
103 const compiler = webpack.default(webpackConfig)
Adrien Béraude74741b2021-04-19 13:22:54 -0400104 app.use(webpackDev.default(compiler, {
simon7a7b4d52022-09-23 02:09:42 -0400105 publicPath: webpackConfig.output?.publicPath
Adrien Béraude74741b2021-04-19 13:22:54 -0400106 }))
107 app.use(webpackHot.default(compiler))
Larbi Gharibe9af9732021-03-31 15:08:01 +0100108 }
Larbi Gharibe9af9732021-03-31 15:08:01 +0100109
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400110 /*
111 Configuation for Passeport Js
112 */
Adrien Béraude74741b2021-04-19 13:22:54 -0400113 app.disable('x-powered-by')
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400114
simon7a7b4d52022-09-23 02:09:42 -0400115 const secret_key = process.env.SECRET_KEY_BASE;
116
117 if (!secret_key) {
118 throw new Error("SECRET_KEY_BASE undefined")
119 }
120
Adrien Béraud4e287b92021-04-24 16:15:56 -0400121 const sessionMiddleware = session({
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400122 store: sessionStore,
123 resave: false,
124 saveUninitialized: true,
125 cookie: {
126 secure: false,//!development,
127 maxAge: 2419200000
128 },
simon7a7b4d52022-09-23 02:09:42 -0400129 secret: secret_key
130 });
Adrien Béraud4e287b92021-04-24 16:15:56 -0400131
132 app.use(sessionMiddleware)
Adrien Béraude74741b2021-04-19 13:22:54 -0400133 app.use(passport.initialize())
134 app.use(passport.session())
135 // app.use(app.router)
Adrien Béraud4e287b92021-04-24 16:15:56 -0400136 app.use(cors(corsOptions))
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400137
simon7a7b4d52022-09-23 02:09:42 -0400138 const jami = new JamiDaemon((account: Account, conversation: any, message: any) => {
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -0400139 console.log('JamiDaemon onMessage')
Adrien Béraudabba2e52021-04-24 21:39:56 -0400140
141 if (conversation.listeners) {
simonc7d52452022-09-23 02:09:42 -0400142 Object.values(conversation.listeners).forEach((listener: any) => {
Adrien Béraudabba2e52021-04-24 21:39:56 -0400143 listener.socket.emit('newMessage', message)
144 })
145 }
146 })
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400147 const apiRouter = new JamiRestApi(jami).getRouter()
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400148
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400149 /*
150 io.use(passportSocketIo.authorize({
151 key: 'connect.sid',
152 secret: process.env.SECRET_KEY_BASE,
153 store: sessionStore,
154 passport: passport,
155 cookieParser: cookieParser,
156 //success: logSuccess(),
157 // fail: logFail(),
Adrien Béraude74741b2021-04-19 13:22:54 -0400158 }))
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400159 */
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400160
Adrien Béraude74741b2021-04-19 13:22:54 -0400161 const isSetupComplete = () => {
162 return 'admin' in appConfig.users
163 }
164
simon7a7b4d52022-09-23 02:09:42 -0400165 const accountFilter = (filter: string | any[]) => {
Adrien Béraude74741b2021-04-19 13:22:54 -0400166 if (typeof filter === 'string') {
167 if (filter === '*')
168 return undefined
169 else
simon7a7b4d52022-09-23 02:09:42 -0400170 return (account: Account) => account.getId() === filter
Adrien Béraude74741b2021-04-19 13:22:54 -0400171 } else if (Array.isArray(filter)) {
simon7a7b4d52022-09-23 02:09:42 -0400172 return (account: Account) => filter.includes(account.getId())
Adrien Béraude74741b2021-04-19 13:22:54 -0400173 } else {
174 throw new Error('Invalid account filter string')
175 }
176 }
177
simon7a7b4d52022-09-23 02:09:42 -0400178 const user = (id: string, config: UserConfig) => {
Adrien Béraude74741b2021-04-19 13:22:54 -0400179 return {
180 id,
181 config,
182 username: config.username || id,
183 accountFilter: accountFilter(config.accounts)
184 }
185 }
186
simon7a7b4d52022-09-23 02:09:42 -0400187 passport.serializeUser((user: any, done) => {
Adrien Béraude74741b2021-04-19 13:22:54 -0400188 connectedUsers[user.id] = user.config
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -0400189 console.log('=============================SerializeUser called ' + user.id)
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400190 console.log(user)
Adrien Béraude74741b2021-04-19 13:22:54 -0400191 done(null, user.id)
192 })
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400193
simon7a7b4d52022-09-23 02:09:42 -0400194 const deserializeUser = (id: string, done: (err: any, user?: Express.User | false | null) => void) => {
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -0400195 console.log('=============================DeserializeUser called on: ' + id)
Adrien Béraude74741b2021-04-19 13:22:54 -0400196 const userConfig = connectedUsers[id]
197 console.log(userConfig)
198 if (userConfig) {
199 done(null, user(id, userConfig))
200 } else
201 done(404, null)
202 }
203 passport.deserializeUser(deserializeUser)
Larbi Gharibe9af9732021-03-31 15:08:01 +0100204
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400205 const jamsStrategy = new LocalStrategy(
simonc7d52452022-09-23 02:09:42 -0400206 async (username, password, done) => {
207 const accountId = await jami.addAccount({
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400208 'managerUri': 'https://jams.savoirfairelinux.com',
209 'managerUsername': username,
210 'archivePassword': password
Adrien Béraude74741b2021-04-19 13:22:54 -0400211 })
212 const id = `jams_${username}`
213 const userConfig = { username, type: 'jams', accounts: accountId }
214 const newUser = user(id, userConfig)
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -0400215 console.log('AccountId: ' + accountId)
Adrien Béraude74741b2021-04-19 13:22:54 -0400216 tempAccounts[accountId] = { done, newUser }
Larbi Gharibe9af9732021-03-31 15:08:01 +0100217
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400218 }
Adrien Béraude74741b2021-04-19 13:22:54 -0400219 )
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -0400220 jamsStrategy.name = 'jams'
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400221
222 const localStrategy = new LocalStrategy(
223 (username, password, done) => {
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -0400224 console.log('localStrategy: ' + username + ' ' + password)
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400225
Adrien Béraude74741b2021-04-19 13:22:54 -0400226 const id = username
227 const userConfig = appConfig.users[username]
228 if (!userConfig) {
229 return done(null, false, { message: 'Incorrect username.' })
230 }
231 if (userConfig.password !== password) {
232 return done(null, false, { message: 'Incorrect password.' })
233 }
234 userConfig.type = 'local'
235
236 done(null, user(id, userConfig))
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400237 }
Adrien Béraude74741b2021-04-19 13:22:54 -0400238 )
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400239
Adrien Béraude74741b2021-04-19 13:22:54 -0400240 passport.use(jamsStrategy)
241 passport.use(localStrategy)
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400242
simon7a7b4d52022-09-23 02:09:42 -0400243 const secured = (req: Request, res: Response, next: NextFunction) => {
Adrien Béraude74741b2021-04-19 13:22:54 -0400244 if (req.user) {
245 return next()
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400246 }
247 res.status(401).end()
Adrien Béraude74741b2021-04-19 13:22:54 -0400248 }
simon7a7b4d52022-09-23 02:09:42 -0400249 const securedRedirect = (req: Request, res: Response, next: NextFunction) => {
250 if (req.user && (req.user as any)?.accountId) {
Adrien Béraude74741b2021-04-19 13:22:54 -0400251 return next()
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400252 }
simon7a7b4d52022-09-23 02:09:42 -0400253 (req.session as any).returnTo = req.originalUrl
Adrien Béraude74741b2021-04-19 13:22:54 -0400254 res.redirect('/login')
255 }
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400256
Adrien Béraude74741b2021-04-19 13:22:54 -0400257 app.use(express.json())
258 app.post('/setup', (req, res) => {
259 if (isSetupComplete()) {
260 return res.status(404).end()
261 }
262 if (!req.body.password) {
263 return res.status(400).end()
264 }
265 console.log(req.body)
266 appConfig.users.admin = {
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -0400267 'accounts': '*',
Adrien Béraude74741b2021-04-19 13:22:54 -0400268 password: req.body.password
269 }
270 res.status(200).end()
271 saveConfig(configPath, appConfig)
272 })
273 app.post('/auth/jams', passport.authenticate('jams'), (req, res) => {
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400274 res.json({ loggedin: true })
Adrien Béraude74741b2021-04-19 13:22:54 -0400275 })
276 app.post('/auth/local', passport.authenticate('local'), (req, res) => {
simon7a7b4d52022-09-23 02:09:42 -0400277 res.json({ loggedin: true, user: (req.user as any)?.id })
Adrien Béraude74741b2021-04-19 13:22:54 -0400278 })
Adrien Béraude5cad982021-06-07 10:05:50 -0400279
simon7a7b4d52022-09-23 02:09:42 -0400280 const getState = (req: Request) => {
Adrien Béraude74741b2021-04-19 13:22:54 -0400281 if (req.user) {
simon7a7b4d52022-09-23 02:09:42 -0400282 const user = (req.user || {}) as UserConfig
283 return { loggedin: true, username: user.username, type: user.type }
Adrien Béraude74741b2021-04-19 13:22:54 -0400284 } else if (isSetupComplete()) {
Adrien Béraude5cad982021-06-07 10:05:50 -0400285 return {}
Adrien Béraude74741b2021-04-19 13:22:54 -0400286 } else {
Adrien Béraude5cad982021-06-07 10:05:50 -0400287 return { setupComplete: false }
288 }
289 }
290
idillon8e6c0062022-09-16 13:34:43 -0400291 // sentrySetUp(app);
idillon452e2102022-09-16 13:23:28 -0400292
Adrien Béraude5cad982021-06-07 10:05:50 -0400293 app.get('/auth', (req, res) => {
294 const state = getState(req)
295 if (req.user) {
296 res.json(state)
297 } else {
298 res.status(401).json(state)
Adrien Béraude74741b2021-04-19 13:22:54 -0400299 }
300 })
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400301
Adrien Béraude74741b2021-04-19 13:22:54 -0400302 app.use('/api', secured, apiRouter)
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400303
Adrien Béraude74741b2021-04-19 13:22:54 -0400304 app.use('/', indexRouter)
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400305
306 /* GET React App */
307
simonc7d52452022-09-23 02:09:42 -0400308 const cwd = process.cwd()
309 app.use(express.static(path.join(cwd, 'client/dist')));
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400310
simonc7d52452022-09-23 02:09:42 -0400311 app.use((req, res) => {
312 res.render(path.join(cwd, 'client/dist/index.ejs'), {
Adrien Béraude5cad982021-06-07 10:05:50 -0400313 initdata: JSON.stringify(getState(req))
314 })
Adrien Béraude74741b2021-04-19 13:22:54 -0400315 })
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400316
idillon452e2102022-09-16 13:23:28 -0400317
simonc7d52452022-09-23 02:09:42 -0400318 // @ts-ignore TODO: Fix the typescript error
Adrien Béraud4e287b92021-04-24 16:15:56 -0400319 const server = http.Server(app)
320
321 const io = new Server(server, { cors: corsOptions })
simon7a7b4d52022-09-23 02:09:42 -0400322 const wrap = (middleware: any) => (socket: Socket, next: (err?: ExtendedError) => void ) => middleware(socket.request, {}, next)
Adrien Béraud4e287b92021-04-24 16:15:56 -0400323 io.use(wrap(sessionMiddleware))
324 io.use(wrap(passport.initialize()))
325 io.use(wrap(passport.session()))
326 io.use((socket, next) => {
simonc7d52452022-09-23 02:09:42 -0400327 if ((socket.request as any).user) {
Adrien Béraud4e287b92021-04-24 16:15:56 -0400328 next()
329 } else {
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -0400330 next(new Error('unauthorized'))
Adrien Béraud4e287b92021-04-24 16:15:56 -0400331 }
332 })
333 io.on('connect', (socket) => {
334 console.log(`new connection ${socket.id}`)
simonc7d52452022-09-23 02:09:42 -0400335 const session = (socket.request as any).session
Adrien Béraud4e287b92021-04-24 16:15:56 -0400336 console.log(`saving sid ${socket.id} in session ${session.id}`)
337 session.socketId = socket.id
338 session.save()
Adrien Béraudabba2e52021-04-24 21:39:56 -0400339
Adrien Béraud2b3c2cd2022-09-18 14:24:33 -0400340 socket.on('conversation', (data) => {
341 console.log('io conversation')
342 console.log(data)
Adrien Béraudabba2e52021-04-24 21:39:56 -0400343 if (session.conversation) {
344 console.log(`disconnect from old conversation ${session.conversation.conversationId}`)
345 const conversation = jami.getConversation(session.conversation.accountId, session.conversation.conversationId)
346 delete conversation.listeners[socket.id]
347 }
348 session.conversation = { accountId: data.accountId, conversationId: data.conversationId }
349 const conversation = jami.getConversation(data.accountId, data.conversationId)
350 if (!conversation.listeners)
351 conversation.listeners = {}
352 conversation.listeners[socket.id] = {
353 socket, session
354 }
355 session.save()
356 })
Adrien Béraud4e287b92021-04-24 16:15:56 -0400357 })
358
359 return server
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400360}
361
Adrien Béraude74741b2021-04-19 13:22:54 -0400362loadConfig(configPath)
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400363 .then(createServer)
364 .then(server => {
Adrien Béraude74741b2021-04-19 13:22:54 -0400365 server.listen(3000)
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400366 })