blob: 42e81a8708ee91f7c50675f5f6b59216ca4f543c [file] [log] [blame]
simond47ef9e2022-09-28 22:24:28 -04001'use strict';
Larbi Gharibe9af9732021-03-31 15:08:01 +01002
simon7e00d612022-10-01 20:06:53 -04003import dotenv from 'dotenv';
4const node_env = process.env.NODE_ENV || 'development';
5dotenv.config({ path: `.env.${node_env}` });
6
simon07b4eb02022-09-29 17:50:26 -04007import cors from 'cors';
8import express, { NextFunction, Request, Response } from 'express';
9import session from 'express-session';
simond47ef9e2022-09-28 22:24:28 -040010import { promises as fs } from 'fs';
11import http from 'http';
simond47ef9e2022-09-28 22:24:28 -040012import passport from 'passport';
simon06527b02022-10-01 15:01:47 -040013import { IVerifyOptions, Strategy as LocalStrategy } from 'passport-local';
simon07b4eb02022-09-29 17:50:26 -040014import path from 'path';
15import { Server, Socket } from 'socket.io';
16import { ExtendedError } from 'socket.io/dist/namespace';
17
simon06527b02022-10-01 15:01:47 -040018import JamiDaemon from './JamiDaemon';
simon07b4eb02022-09-29 17:50:26 -040019import Account from './model/Account';
simon06527b02022-10-01 15:01:47 -040020import { Session } from './model/util';
Adrien Béraude74741b2021-04-19 13:22:54 -040021//import { createRequire } from 'module';
22//const require = createRequire(import.meta.url);
Adrien Béraud947e8792021-04-15 18:32:44 -040023//const redis = require('redis-url').connect()
24//const RedisStore = require('connect-redis')(session)
Adrien Béraud6ecaa402021-04-06 17:37:25 -040025/*const passportSocketIo = require('passport.socketio')*/
simond47ef9e2022-09-28 22:24:28 -040026import indexRouter from './routes/index.js';
simond47ef9e2022-09-28 22:24:28 -040027import JamiRestApi from './routes/jami.js';
idillon8e6c0062022-09-16 13:34:43 -040028// import { sentrySetUp } from './sentry.js'
Adrien Béraude74741b2021-04-19 13:22:54 -040029
simond47ef9e2022-09-28 22:24:28 -040030const configPath = 'jamiServerConfig.json';
Larbi Gharibe9af9732021-03-31 15:08:01 +010031
Adrien Béraud6ecaa402021-04-06 17:37:25 -040032//const sessionStore = new RedisStore({ client: redis })
simond47ef9e2022-09-28 22:24:28 -040033const sessionStore = new session.MemoryStore();
Larbi Gharibe9af9732021-03-31 15:08:01 +010034
simon06527b02022-10-01 15:01:47 -040035interface User {
36 id: string;
37 config: UserConfig;
38 username: string;
39 accountFilter?: (account: any) => boolean;
40}
41
simon7a7b4d52022-09-23 02:09:42 -040042interface UserConfig {
simon06527b02022-10-01 15:01:47 -040043 accountId?: string;
simon7a7b4d52022-09-23 02:09:42 -040044 accounts: string;
45 password?: string;
46 username?: string;
simond47ef9e2022-09-28 22:24:28 -040047 type?: string;
simon7a7b4d52022-09-23 02:09:42 -040048}
49
50interface AppConfig {
simond47ef9e2022-09-28 22:24:28 -040051 users: Record<string, UserConfig>;
simon06527b02022-10-01 15:01:47 -040052 authMethods: unknown[];
simon7a7b4d52022-09-23 02:09:42 -040053}
54
55const loadConfig = async (filePath: string): Promise<AppConfig> => {
simond47ef9e2022-09-28 22:24:28 -040056 const config = { users: {}, authMethods: [] };
57 try {
58 return Object.assign(config, JSON.parse((await fs.readFile(filePath)).toString()));
59 } catch (e) {
60 console.log(e);
61 return config;
62 }
63};
Adrien Béraud824a7132021-04-17 17:25:27 -040064
simon7a7b4d52022-09-23 02:09:42 -040065const saveConfig = (filePath: string, config: AppConfig) => {
simond47ef9e2022-09-28 22:24:28 -040066 return fs.writeFile(filePath, JSON.stringify(config));
67};
Adrien Béraude74741b2021-04-19 13:22:54 -040068
Larbi Gharibe9af9732021-03-31 15:08:01 +010069/*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040070Share sessions between Passport.js and Socket.io
Larbi Gharibe9af9732021-03-31 15:08:01 +010071*/
72
73function logSuccess() {
simond47ef9e2022-09-28 22:24:28 -040074 console.log('passportSocketIo authorized user with Success 😁');
Larbi Gharibe9af9732021-03-31 15:08:01 +010075}
76
77function logFail() {
simond47ef9e2022-09-28 22:24:28 -040078 console.log('passportSocketIo failed to authorized user 👺');
Larbi Gharibe9af9732021-03-31 15:08:01 +010079}
80
81/*
Larbi Gharibe9af9732021-03-31 15:08:01 +010082
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040083tempAccounts holds users accounts while tempting to authenticate them on Jams.
84connectedUsers holds users accounts after they got authenticated by Jams.
Larbi Gharibe9af9732021-03-31 15:08:01 +010085
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040086Users should be removed from connectedUsers when receiving a disconnect
87web socket call
Larbi Gharibe9af9732021-03-31 15:08:01 +010088
89*/
simon06527b02022-10-01 15:01:47 -040090const tempAccounts: Record<
91 string,
92 {
93 newUser: Express.User;
94 done: (error: any, user?: any, options?: IVerifyOptions) => void;
95 }
96> = {};
97const connectedUsers: Record<string, UserConfig> = {};
Larbi Gharibe9af9732021-03-31 15:08:01 +010098
simon7a7b4d52022-09-23 02:09:42 -040099const createServer = async (appConfig: AppConfig) => {
simond47ef9e2022-09-28 22:24:28 -0400100 const app = express();
101 console.log(`Loading server for ${node_env} with config:`);
102 console.log(appConfig);
Larbi Gharibe9af9732021-03-31 15:08:01 +0100103
simond47ef9e2022-09-28 22:24:28 -0400104 const corsOptions = {
105 origin: 'http://127.0.0.1:3000',
106 };
Adrien Béraud4e287b92021-04-24 16:15:56 -0400107
simond47ef9e2022-09-28 22:24:28 -0400108 if (node_env === 'development') {
109 const webpack = await import('webpack');
110 const webpackDev = await import('webpack-dev-middleware');
111 const webpackHot = await import('webpack-hot-middleware');
112 const { default: webpackConfig } = await import('jami-web-client/webpack.config.js');
simonc7d52452022-09-23 02:09:42 -0400113
simond47ef9e2022-09-28 22:24:28 -0400114 const compiler = webpack.default(webpackConfig);
115 app.use(
116 webpackDev.default(compiler, {
117 publicPath: webpackConfig.output?.publicPath,
118 })
119 );
120 app.use(webpackHot.default(compiler));
121 }
Larbi Gharibe9af9732021-03-31 15:08:01 +0100122
simond47ef9e2022-09-28 22:24:28 -0400123 /*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400124 Configuation for Passeport Js
125 */
simond47ef9e2022-09-28 22:24:28 -0400126 app.disable('x-powered-by');
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400127
simond47ef9e2022-09-28 22:24:28 -0400128 const secret_key = process.env.SECRET_KEY_BASE;
simon7a7b4d52022-09-23 02:09:42 -0400129
simond47ef9e2022-09-28 22:24:28 -0400130 if (!secret_key) {
131 throw new Error('SECRET_KEY_BASE undefined');
132 }
133
134 const sessionMiddleware = session({
135 store: sessionStore,
136 resave: false,
137 saveUninitialized: true,
138 cookie: {
139 secure: false, //!development,
140 maxAge: 2419200000,
141 },
142 secret: secret_key,
143 });
144
145 app.use(sessionMiddleware);
146 app.use(passport.initialize());
147 app.use(passport.session());
148 // app.use(app.router)
149 app.use(cors(corsOptions));
150
151 const jami = new JamiDaemon((account: Account, conversation: any, message: any) => {
152 console.log('JamiDaemon onMessage');
153
154 if (conversation.listeners) {
155 Object.values(conversation.listeners).forEach((listener: any) => {
156 listener.socket.emit('newMessage', message);
157 });
simon7a7b4d52022-09-23 02:09:42 -0400158 }
simond47ef9e2022-09-28 22:24:28 -0400159 });
160 const apiRouter = new JamiRestApi(jami).getRouter();
simon7a7b4d52022-09-23 02:09:42 -0400161
simond47ef9e2022-09-28 22:24:28 -0400162 /*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400163 io.use(passportSocketIo.authorize({
164 key: 'connect.sid',
165 secret: process.env.SECRET_KEY_BASE,
166 store: sessionStore,
167 passport: passport,
168 cookieParser: cookieParser,
169 //success: logSuccess(),
170 // fail: logFail(),
Adrien Béraude74741b2021-04-19 13:22:54 -0400171 }))
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400172 */
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400173
simond47ef9e2022-09-28 22:24:28 -0400174 const isSetupComplete = () => {
175 return 'admin' in appConfig.users;
176 };
177
simon06527b02022-10-01 15:01:47 -0400178 const accountFilter = (filter: string | string[]) => {
simond47ef9e2022-09-28 22:24:28 -0400179 if (typeof filter === 'string') {
180 if (filter === '*') return undefined;
181 else return (account: Account) => account.getId() === filter;
182 } else if (Array.isArray(filter)) {
183 return (account: Account) => filter.includes(account.getId());
184 } else {
185 throw new Error('Invalid account filter string');
Adrien Béraude74741b2021-04-19 13:22:54 -0400186 }
simond47ef9e2022-09-28 22:24:28 -0400187 };
Adrien Béraude74741b2021-04-19 13:22:54 -0400188
simond47ef9e2022-09-28 22:24:28 -0400189 const user = (id: string, config: UserConfig) => {
190 return {
191 id,
192 config,
193 username: config.username || id,
194 accountFilter: accountFilter(config.accounts),
195 };
196 };
197
198 passport.serializeUser((user: any, done) => {
simon06527b02022-10-01 15:01:47 -0400199 user = user as User;
simond47ef9e2022-09-28 22:24:28 -0400200 connectedUsers[user.id] = user.config;
201 console.log('=============================SerializeUser called ' + user.id);
202 console.log(user);
203 done(null, user.id);
204 });
205
206 const deserializeUser = (id: string, done: (err: any, user?: Express.User | false | null) => void) => {
207 console.log('=============================DeserializeUser called on: ' + id);
208 const userConfig = connectedUsers[id];
209 console.log(userConfig);
210 if (userConfig) {
211 done(null, user(id, userConfig));
212 } else done(404, null);
213 };
214 passport.deserializeUser(deserializeUser);
215
216 const jamsStrategy = new LocalStrategy(async (username, password, done) => {
217 const accountId = await jami.addAccount({
218 managerUri: 'https://jams.savoirfairelinux.com',
219 managerUsername: username,
220 archivePassword: password,
221 });
222 const id = `jams_${username}`;
223 const userConfig = { username, type: 'jams', accounts: accountId };
224 const newUser = user(id, userConfig);
225 console.log('AccountId: ' + accountId);
226 tempAccounts[accountId] = { done, newUser };
227 });
228 jamsStrategy.name = 'jams';
229
230 const localStrategy = new LocalStrategy((username, password, done) => {
231 console.log('localStrategy: ' + username + ' ' + password);
232
233 const id = username;
234 const userConfig = appConfig.users[username];
235 if (!userConfig) {
236 return done(null, false, { message: 'Incorrect username.' });
Adrien Béraude74741b2021-04-19 13:22:54 -0400237 }
simond47ef9e2022-09-28 22:24:28 -0400238 if (userConfig.password !== password) {
239 return done(null, false, { message: 'Incorrect password.' });
Adrien Béraude74741b2021-04-19 13:22:54 -0400240 }
simond47ef9e2022-09-28 22:24:28 -0400241 userConfig.type = 'local';
Adrien Béraude74741b2021-04-19 13:22:54 -0400242
simond47ef9e2022-09-28 22:24:28 -0400243 done(null, user(id, userConfig));
244 });
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400245
simond47ef9e2022-09-28 22:24:28 -0400246 passport.use(jamsStrategy);
247 passport.use(localStrategy);
248
249 const secured = (req: Request, res: Response, next: NextFunction) => {
250 if (req.user) {
251 return next();
Adrien Béraude74741b2021-04-19 13:22:54 -0400252 }
simond47ef9e2022-09-28 22:24:28 -0400253 res.status(401).end();
254 };
255 const securedRedirect = (req: Request, res: Response, next: NextFunction) => {
simon06527b02022-10-01 15:01:47 -0400256 const user = req.user as UserConfig | undefined;
257 if (user?.accountId) {
simond47ef9e2022-09-28 22:24:28 -0400258 return next();
Adrien Béraude74741b2021-04-19 13:22:54 -0400259 }
simond47ef9e2022-09-28 22:24:28 -0400260 (req.session as any).returnTo = req.originalUrl;
261 res.redirect('/login');
262 };
263
264 app.use(express.json());
265 app.post('/setup', (req, res) => {
266 if (isSetupComplete()) {
267 return res.status(404).end();
Adrien Béraude74741b2021-04-19 13:22:54 -0400268 }
simond47ef9e2022-09-28 22:24:28 -0400269 if (!req.body.password) {
270 return res.status(400).end();
Adrien Béraude5cad982021-06-07 10:05:50 -0400271 }
simond47ef9e2022-09-28 22:24:28 -0400272 console.log(req.body);
273 appConfig.users.admin = {
274 accounts: '*',
275 password: req.body.password,
276 };
277 res.status(200).end();
278 saveConfig(configPath, appConfig);
279 });
280 app.post('/auth/jams', passport.authenticate('jams'), (req, res) => {
281 res.json({ loggedin: true });
282 });
283 app.post('/auth/local', passport.authenticate('local'), (req, res) => {
simon06527b02022-10-01 15:01:47 -0400284 res.json({ loggedin: true, user: (req.user as User | undefined)?.id });
simond47ef9e2022-09-28 22:24:28 -0400285 });
Adrien Béraude5cad982021-06-07 10:05:50 -0400286
simond47ef9e2022-09-28 22:24:28 -0400287 const getState = (req: Request) => {
288 if (req.user) {
simon06527b02022-10-01 15:01:47 -0400289 const user = req.user as UserConfig;
simond47ef9e2022-09-28 22:24:28 -0400290 return { loggedin: true, username: user.username, type: user.type };
291 } else if (isSetupComplete()) {
292 return {};
293 } else {
294 return { setupComplete: false };
295 }
296 };
idillon452e2102022-09-16 13:23:28 -0400297
simond47ef9e2022-09-28 22:24:28 -0400298 // sentrySetUp(app);
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400299
simond47ef9e2022-09-28 22:24:28 -0400300 app.get('/auth', (req, res) => {
301 const state = getState(req);
302 if (req.user) {
303 res.json(state);
304 } else {
305 res.status(401).json(state);
306 }
307 });
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400308
simond47ef9e2022-09-28 22:24:28 -0400309 app.use('/api', secured, apiRouter);
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400310
simond47ef9e2022-09-28 22:24:28 -0400311 app.use('/', indexRouter);
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400312
simond47ef9e2022-09-28 22:24:28 -0400313 /* GET React App */
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400314
simond47ef9e2022-09-28 22:24:28 -0400315 const cwd = process.cwd();
316 app.use(express.static(path.join(cwd, 'client/dist')));
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400317
simond47ef9e2022-09-28 22:24:28 -0400318 app.use((req, res) => {
319 res.render(path.join(cwd, 'client/dist/index.ejs'), {
320 initdata: JSON.stringify(getState(req)),
321 });
322 });
idillon452e2102022-09-16 13:23:28 -0400323
simond47ef9e2022-09-28 22:24:28 -0400324 // @ts-ignore TODO: Fix the typescript error
325 const server = http.Server(app);
Adrien Béraud4e287b92021-04-24 16:15:56 -0400326
simond47ef9e2022-09-28 22:24:28 -0400327 const io = new Server(server, { cors: corsOptions });
328 const wrap = (middleware: any) => (socket: Socket, next: (err?: ExtendedError) => void) =>
329 middleware(socket.request, {}, next);
330 io.use(wrap(sessionMiddleware));
331 io.use(wrap(passport.initialize()));
332 io.use(wrap(passport.session()));
333 io.use((socket, next) => {
334 if ((socket.request as any).user) {
335 next();
336 } else {
337 next(new Error('unauthorized'));
338 }
339 });
340 io.on('connect', (socket) => {
341 console.log(`new connection ${socket.id}`);
simon06527b02022-10-01 15:01:47 -0400342 const session: Session = (socket.request as any).session;
simond47ef9e2022-09-28 22:24:28 -0400343 console.log(`saving sid ${socket.id} in session ${session.id}`);
344 session.socketId = socket.id;
345 session.save();
Adrien Béraudabba2e52021-04-24 21:39:56 -0400346
simond47ef9e2022-09-28 22:24:28 -0400347 socket.on('conversation', (data) => {
348 console.log('io conversation');
349 console.log(data);
350 if (session.conversation) {
351 console.log(`disconnect from old conversation ${session.conversation.conversationId}`);
352 const conversation = jami.getConversation(session.conversation.accountId, session.conversation.conversationId);
simon06527b02022-10-01 15:01:47 -0400353 if (conversation) {
354 delete conversation.listeners[socket.id];
355 }
simond47ef9e2022-09-28 22:24:28 -0400356 }
357 session.conversation = { accountId: data.accountId, conversationId: data.conversationId };
358 const conversation = jami.getConversation(data.accountId, data.conversationId);
simon06527b02022-10-01 15:01:47 -0400359 if (conversation) {
360 if (!conversation.listeners) {
361 conversation.listeners = {};
362 }
363 conversation.listeners[socket.id] = {
364 socket,
365 session,
366 };
367 }
simond47ef9e2022-09-28 22:24:28 -0400368 session.save();
369 });
370 });
Adrien Béraud4e287b92021-04-24 16:15:56 -0400371
simond47ef9e2022-09-28 22:24:28 -0400372 return server;
373};
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400374
Adrien Béraude74741b2021-04-19 13:22:54 -0400375loadConfig(configPath)
simond47ef9e2022-09-28 22:24:28 -0400376 .then(createServer)
377 .then((server) => {
378 server.listen(3000);
379 });