blob: b86cb64f402b3fbea4df4f56cfb8ad6d7a782f29 [file] [log] [blame]
simon26e79f72022-10-05 22:16:08 -04001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
simond47ef9e2022-09-28 22:24:28 -040018'use strict';
Larbi Gharibe9af9732021-03-31 15:08:01 +010019
simon7e00d612022-10-01 20:06:53 -040020import dotenv from 'dotenv';
21const node_env = process.env.NODE_ENV || 'development';
22dotenv.config({ path: `.env.${node_env}` });
23
simon07b4eb02022-09-29 17:50:26 -040024import cors from 'cors';
25import express, { NextFunction, Request, Response } from 'express';
26import session from 'express-session';
simond47ef9e2022-09-28 22:24:28 -040027import { promises as fs } from 'fs';
28import http from 'http';
simond47ef9e2022-09-28 22:24:28 -040029import passport from 'passport';
simon06527b02022-10-01 15:01:47 -040030import { IVerifyOptions, Strategy as LocalStrategy } from 'passport-local';
simon07b4eb02022-09-29 17:50:26 -040031import path from 'path';
32import { Server, Socket } from 'socket.io';
33import { ExtendedError } from 'socket.io/dist/namespace';
34
simon06527b02022-10-01 15:01:47 -040035import JamiDaemon from './JamiDaemon';
simon07b4eb02022-09-29 17:50:26 -040036import Account from './model/Account';
simon06527b02022-10-01 15:01:47 -040037import { Session } from './model/util';
Adrien Béraude74741b2021-04-19 13:22:54 -040038//import { createRequire } from 'module';
39//const require = createRequire(import.meta.url);
Adrien Béraud947e8792021-04-15 18:32:44 -040040//const redis = require('redis-url').connect()
41//const RedisStore = require('connect-redis')(session)
Adrien Béraud6ecaa402021-04-06 17:37:25 -040042/*const passportSocketIo = require('passport.socketio')*/
simond47ef9e2022-09-28 22:24:28 -040043import indexRouter from './routes/index.js';
simond47ef9e2022-09-28 22:24:28 -040044import JamiRestApi from './routes/jami.js';
idillon8e6c0062022-09-16 13:34:43 -040045// import { sentrySetUp } from './sentry.js'
Adrien Béraude74741b2021-04-19 13:22:54 -040046
simond47ef9e2022-09-28 22:24:28 -040047const configPath = 'jamiServerConfig.json';
Larbi Gharibe9af9732021-03-31 15:08:01 +010048
Adrien Béraud6ecaa402021-04-06 17:37:25 -040049//const sessionStore = new RedisStore({ client: redis })
simond47ef9e2022-09-28 22:24:28 -040050const sessionStore = new session.MemoryStore();
Larbi Gharibe9af9732021-03-31 15:08:01 +010051
simon06527b02022-10-01 15:01:47 -040052interface User {
53 id: string;
54 config: UserConfig;
55 username: string;
56 accountFilter?: (account: any) => boolean;
57}
58
simon7a7b4d52022-09-23 02:09:42 -040059interface UserConfig {
simon06527b02022-10-01 15:01:47 -040060 accountId?: string;
simon7a7b4d52022-09-23 02:09:42 -040061 accounts: string;
62 password?: string;
63 username?: string;
simond47ef9e2022-09-28 22:24:28 -040064 type?: string;
simon7a7b4d52022-09-23 02:09:42 -040065}
66
67interface AppConfig {
simond47ef9e2022-09-28 22:24:28 -040068 users: Record<string, UserConfig>;
simon06527b02022-10-01 15:01:47 -040069 authMethods: unknown[];
simon7a7b4d52022-09-23 02:09:42 -040070}
71
72const loadConfig = async (filePath: string): Promise<AppConfig> => {
simond47ef9e2022-09-28 22:24:28 -040073 const config = { users: {}, authMethods: [] };
74 try {
75 return Object.assign(config, JSON.parse((await fs.readFile(filePath)).toString()));
76 } catch (e) {
77 console.log(e);
78 return config;
79 }
80};
Adrien Béraud824a7132021-04-17 17:25:27 -040081
simon7a7b4d52022-09-23 02:09:42 -040082const saveConfig = (filePath: string, config: AppConfig) => {
simond47ef9e2022-09-28 22:24:28 -040083 return fs.writeFile(filePath, JSON.stringify(config));
84};
Adrien Béraude74741b2021-04-19 13:22:54 -040085
Larbi Gharibe9af9732021-03-31 15:08:01 +010086/*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040087Share sessions between Passport.js and Socket.io
Larbi Gharibe9af9732021-03-31 15:08:01 +010088*/
89
90function logSuccess() {
simond47ef9e2022-09-28 22:24:28 -040091 console.log('passportSocketIo authorized user with Success 😁');
Larbi Gharibe9af9732021-03-31 15:08:01 +010092}
93
94function logFail() {
simond47ef9e2022-09-28 22:24:28 -040095 console.log('passportSocketIo failed to authorized user 👺');
Larbi Gharibe9af9732021-03-31 15:08:01 +010096}
97
98/*
Larbi Gharibe9af9732021-03-31 15:08:01 +010099
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400100tempAccounts holds users accounts while tempting to authenticate them on Jams.
101connectedUsers holds users accounts after they got authenticated by Jams.
Larbi Gharibe9af9732021-03-31 15:08:01 +0100102
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400103Users should be removed from connectedUsers when receiving a disconnect
104web socket call
Larbi Gharibe9af9732021-03-31 15:08:01 +0100105
106*/
simon06527b02022-10-01 15:01:47 -0400107const tempAccounts: Record<
108 string,
109 {
110 newUser: Express.User;
111 done: (error: any, user?: any, options?: IVerifyOptions) => void;
112 }
113> = {};
114const connectedUsers: Record<string, UserConfig> = {};
Larbi Gharibe9af9732021-03-31 15:08:01 +0100115
simon7a7b4d52022-09-23 02:09:42 -0400116const createServer = async (appConfig: AppConfig) => {
simond47ef9e2022-09-28 22:24:28 -0400117 const app = express();
118 console.log(`Loading server for ${node_env} with config:`);
119 console.log(appConfig);
Larbi Gharibe9af9732021-03-31 15:08:01 +0100120
simond47ef9e2022-09-28 22:24:28 -0400121 const corsOptions = {
122 origin: 'http://127.0.0.1:3000',
123 };
Adrien Béraud4e287b92021-04-24 16:15:56 -0400124
simond47ef9e2022-09-28 22:24:28 -0400125 if (node_env === 'development') {
126 const webpack = await import('webpack');
127 const webpackDev = await import('webpack-dev-middleware');
128 const webpackHot = await import('webpack-hot-middleware');
129 const { default: webpackConfig } = await import('jami-web-client/webpack.config.js');
simonc7d52452022-09-23 02:09:42 -0400130
simond47ef9e2022-09-28 22:24:28 -0400131 const compiler = webpack.default(webpackConfig);
132 app.use(
133 webpackDev.default(compiler, {
134 publicPath: webpackConfig.output?.publicPath,
135 })
136 );
137 app.use(webpackHot.default(compiler));
138 }
Larbi Gharibe9af9732021-03-31 15:08:01 +0100139
simond47ef9e2022-09-28 22:24:28 -0400140 /*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400141 Configuation for Passeport Js
142 */
simond47ef9e2022-09-28 22:24:28 -0400143 app.disable('x-powered-by');
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400144
simond47ef9e2022-09-28 22:24:28 -0400145 const secret_key = process.env.SECRET_KEY_BASE;
simon7a7b4d52022-09-23 02:09:42 -0400146
simond47ef9e2022-09-28 22:24:28 -0400147 if (!secret_key) {
148 throw new Error('SECRET_KEY_BASE undefined');
149 }
150
151 const sessionMiddleware = session({
152 store: sessionStore,
153 resave: false,
154 saveUninitialized: true,
155 cookie: {
156 secure: false, //!development,
157 maxAge: 2419200000,
158 },
159 secret: secret_key,
160 });
161
162 app.use(sessionMiddleware);
163 app.use(passport.initialize());
164 app.use(passport.session());
165 // app.use(app.router)
166 app.use(cors(corsOptions));
167
168 const jami = new JamiDaemon((account: Account, conversation: any, message: any) => {
169 console.log('JamiDaemon onMessage');
170
171 if (conversation.listeners) {
172 Object.values(conversation.listeners).forEach((listener: any) => {
173 listener.socket.emit('newMessage', message);
174 });
simon7a7b4d52022-09-23 02:09:42 -0400175 }
simond47ef9e2022-09-28 22:24:28 -0400176 });
177 const apiRouter = new JamiRestApi(jami).getRouter();
simon7a7b4d52022-09-23 02:09:42 -0400178
simond47ef9e2022-09-28 22:24:28 -0400179 /*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400180 io.use(passportSocketIo.authorize({
181 key: 'connect.sid',
182 secret: process.env.SECRET_KEY_BASE,
183 store: sessionStore,
184 passport: passport,
185 cookieParser: cookieParser,
186 //success: logSuccess(),
187 // fail: logFail(),
Adrien Béraude74741b2021-04-19 13:22:54 -0400188 }))
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400189 */
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400190
simond47ef9e2022-09-28 22:24:28 -0400191 const isSetupComplete = () => {
192 return 'admin' in appConfig.users;
193 };
194
simon06527b02022-10-01 15:01:47 -0400195 const accountFilter = (filter: string | string[]) => {
simond47ef9e2022-09-28 22:24:28 -0400196 if (typeof filter === 'string') {
197 if (filter === '*') return undefined;
198 else return (account: Account) => account.getId() === filter;
199 } else if (Array.isArray(filter)) {
200 return (account: Account) => filter.includes(account.getId());
201 } else {
202 throw new Error('Invalid account filter string');
Adrien Béraude74741b2021-04-19 13:22:54 -0400203 }
simond47ef9e2022-09-28 22:24:28 -0400204 };
Adrien Béraude74741b2021-04-19 13:22:54 -0400205
simond47ef9e2022-09-28 22:24:28 -0400206 const user = (id: string, config: UserConfig) => {
207 return {
208 id,
209 config,
210 username: config.username || id,
211 accountFilter: accountFilter(config.accounts),
212 };
213 };
214
215 passport.serializeUser((user: any, done) => {
simon06527b02022-10-01 15:01:47 -0400216 user = user as User;
simond47ef9e2022-09-28 22:24:28 -0400217 connectedUsers[user.id] = user.config;
218 console.log('=============================SerializeUser called ' + user.id);
219 console.log(user);
220 done(null, user.id);
221 });
222
223 const deserializeUser = (id: string, done: (err: any, user?: Express.User | false | null) => void) => {
224 console.log('=============================DeserializeUser called on: ' + id);
225 const userConfig = connectedUsers[id];
226 console.log(userConfig);
227 if (userConfig) {
228 done(null, user(id, userConfig));
229 } else done(404, null);
230 };
231 passport.deserializeUser(deserializeUser);
232
233 const jamsStrategy = new LocalStrategy(async (username, password, done) => {
234 const accountId = await jami.addAccount({
235 managerUri: 'https://jams.savoirfairelinux.com',
236 managerUsername: username,
237 archivePassword: password,
238 });
239 const id = `jams_${username}`;
240 const userConfig = { username, type: 'jams', accounts: accountId };
241 const newUser = user(id, userConfig);
242 console.log('AccountId: ' + accountId);
243 tempAccounts[accountId] = { done, newUser };
244 });
245 jamsStrategy.name = 'jams';
246
247 const localStrategy = new LocalStrategy((username, password, done) => {
248 console.log('localStrategy: ' + username + ' ' + password);
249
250 const id = username;
251 const userConfig = appConfig.users[username];
252 if (!userConfig) {
253 return done(null, false, { message: 'Incorrect username.' });
Adrien Béraude74741b2021-04-19 13:22:54 -0400254 }
simond47ef9e2022-09-28 22:24:28 -0400255 if (userConfig.password !== password) {
256 return done(null, false, { message: 'Incorrect password.' });
Adrien Béraude74741b2021-04-19 13:22:54 -0400257 }
simond47ef9e2022-09-28 22:24:28 -0400258 userConfig.type = 'local';
Adrien Béraude74741b2021-04-19 13:22:54 -0400259
simond47ef9e2022-09-28 22:24:28 -0400260 done(null, user(id, userConfig));
261 });
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400262
simond47ef9e2022-09-28 22:24:28 -0400263 passport.use(jamsStrategy);
264 passport.use(localStrategy);
265
266 const secured = (req: Request, res: Response, next: NextFunction) => {
267 if (req.user) {
268 return next();
Adrien Béraude74741b2021-04-19 13:22:54 -0400269 }
simond47ef9e2022-09-28 22:24:28 -0400270 res.status(401).end();
271 };
272 const securedRedirect = (req: Request, res: Response, next: NextFunction) => {
simon06527b02022-10-01 15:01:47 -0400273 const user = req.user as UserConfig | undefined;
274 if (user?.accountId) {
simond47ef9e2022-09-28 22:24:28 -0400275 return next();
Adrien Béraude74741b2021-04-19 13:22:54 -0400276 }
simond47ef9e2022-09-28 22:24:28 -0400277 (req.session as any).returnTo = req.originalUrl;
278 res.redirect('/login');
279 };
280
281 app.use(express.json());
282 app.post('/setup', (req, res) => {
283 if (isSetupComplete()) {
284 return res.status(404).end();
Adrien Béraude74741b2021-04-19 13:22:54 -0400285 }
simond47ef9e2022-09-28 22:24:28 -0400286 if (!req.body.password) {
287 return res.status(400).end();
Adrien Béraude5cad982021-06-07 10:05:50 -0400288 }
simond47ef9e2022-09-28 22:24:28 -0400289 console.log(req.body);
290 appConfig.users.admin = {
291 accounts: '*',
292 password: req.body.password,
293 };
294 res.status(200).end();
295 saveConfig(configPath, appConfig);
296 });
297 app.post('/auth/jams', passport.authenticate('jams'), (req, res) => {
298 res.json({ loggedin: true });
299 });
300 app.post('/auth/local', passport.authenticate('local'), (req, res) => {
simon06527b02022-10-01 15:01:47 -0400301 res.json({ loggedin: true, user: (req.user as User | undefined)?.id });
simond47ef9e2022-09-28 22:24:28 -0400302 });
Adrien Béraude5cad982021-06-07 10:05:50 -0400303
simond47ef9e2022-09-28 22:24:28 -0400304 const getState = (req: Request) => {
305 if (req.user) {
simon06527b02022-10-01 15:01:47 -0400306 const user = req.user as UserConfig;
simond47ef9e2022-09-28 22:24:28 -0400307 return { loggedin: true, username: user.username, type: user.type };
308 } else if (isSetupComplete()) {
309 return {};
310 } else {
311 return { setupComplete: false };
312 }
313 };
idillon452e2102022-09-16 13:23:28 -0400314
simond47ef9e2022-09-28 22:24:28 -0400315 // sentrySetUp(app);
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400316
simond47ef9e2022-09-28 22:24:28 -0400317 app.get('/auth', (req, res) => {
318 const state = getState(req);
319 if (req.user) {
320 res.json(state);
321 } else {
322 res.status(401).json(state);
323 }
324 });
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400325
simond47ef9e2022-09-28 22:24:28 -0400326 app.use('/api', secured, apiRouter);
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400327
simond47ef9e2022-09-28 22:24:28 -0400328 app.use('/', indexRouter);
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400329
simond47ef9e2022-09-28 22:24:28 -0400330 /* GET React App */
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400331
simond47ef9e2022-09-28 22:24:28 -0400332 const cwd = process.cwd();
333 app.use(express.static(path.join(cwd, 'client/dist')));
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400334
simond47ef9e2022-09-28 22:24:28 -0400335 app.use((req, res) => {
336 res.render(path.join(cwd, 'client/dist/index.ejs'), {
337 initdata: JSON.stringify(getState(req)),
338 });
339 });
idillon452e2102022-09-16 13:23:28 -0400340
simond47ef9e2022-09-28 22:24:28 -0400341 // @ts-ignore TODO: Fix the typescript error
342 const server = http.Server(app);
Adrien Béraud4e287b92021-04-24 16:15:56 -0400343
simond47ef9e2022-09-28 22:24:28 -0400344 const io = new Server(server, { cors: corsOptions });
345 const wrap = (middleware: any) => (socket: Socket, next: (err?: ExtendedError) => void) =>
346 middleware(socket.request, {}, next);
347 io.use(wrap(sessionMiddleware));
348 io.use(wrap(passport.initialize()));
349 io.use(wrap(passport.session()));
350 io.use((socket, next) => {
351 if ((socket.request as any).user) {
352 next();
353 } else {
354 next(new Error('unauthorized'));
355 }
356 });
357 io.on('connect', (socket) => {
358 console.log(`new connection ${socket.id}`);
simon06527b02022-10-01 15:01:47 -0400359 const session: Session = (socket.request as any).session;
simond47ef9e2022-09-28 22:24:28 -0400360 console.log(`saving sid ${socket.id} in session ${session.id}`);
361 session.socketId = socket.id;
362 session.save();
Adrien Béraudabba2e52021-04-24 21:39:56 -0400363
simond47ef9e2022-09-28 22:24:28 -0400364 socket.on('conversation', (data) => {
365 console.log('io conversation');
366 console.log(data);
367 if (session.conversation) {
368 console.log(`disconnect from old conversation ${session.conversation.conversationId}`);
369 const conversation = jami.getConversation(session.conversation.accountId, session.conversation.conversationId);
simon06527b02022-10-01 15:01:47 -0400370 if (conversation) {
371 delete conversation.listeners[socket.id];
372 }
simond47ef9e2022-09-28 22:24:28 -0400373 }
374 session.conversation = { accountId: data.accountId, conversationId: data.conversationId };
375 const conversation = jami.getConversation(data.accountId, data.conversationId);
simon06527b02022-10-01 15:01:47 -0400376 if (conversation) {
377 if (!conversation.listeners) {
378 conversation.listeners = {};
379 }
380 conversation.listeners[socket.id] = {
381 socket,
382 session,
383 };
384 }
simond47ef9e2022-09-28 22:24:28 -0400385 session.save();
386 });
387 });
Adrien Béraud4e287b92021-04-24 16:15:56 -0400388
simond47ef9e2022-09-28 22:24:28 -0400389 return server;
390};
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400391
Adrien Béraude74741b2021-04-19 13:22:54 -0400392loadConfig(configPath)
simond47ef9e2022-09-28 22:24:28 -0400393 .then(createServer)
394 .then((server) => {
395 server.listen(3000);
396 });