blob: bfa7c6dbfb8749fff3078ec9386a81b592f71673 [file] [log] [blame]
simond47ef9e2022-09-28 22:24:28 -04001'use strict';
Larbi Gharibe9af9732021-03-31 15:08:01 +01002
simon07b4eb02022-09-29 17:50:26 -04003import cors from 'cors';
4import express, { NextFunction, Request, Response } from 'express';
5import session from 'express-session';
simond47ef9e2022-09-28 22:24:28 -04006import { promises as fs } from 'fs';
7import http from 'http';
simond47ef9e2022-09-28 22:24:28 -04008import passport from 'passport';
9import { Strategy as LocalStrategy } from 'passport-local';
simon07b4eb02022-09-29 17:50:26 -040010import path from 'path';
11import { Server, Socket } from 'socket.io';
12import { ExtendedError } from 'socket.io/dist/namespace';
13
14import JamiDaemon from './JamiDaemon.js';
15import Account from './model/Account';
Adrien Béraude74741b2021-04-19 13:22:54 -040016//import { createRequire } from 'module';
17//const require = createRequire(import.meta.url);
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')*/
simond47ef9e2022-09-28 22:24:28 -040021import indexRouter from './routes/index.js';
simond47ef9e2022-09-28 22:24:28 -040022import JamiRestApi from './routes/jami.js';
idillon8e6c0062022-09-16 13:34:43 -040023// import { sentrySetUp } from './sentry.js'
Adrien Béraude74741b2021-04-19 13:22:54 -040024
simond47ef9e2022-09-28 22:24:28 -040025const configPath = 'jamiServerConfig.json';
Larbi Gharibe9af9732021-03-31 15:08:01 +010026
Adrien Béraud6ecaa402021-04-06 17:37:25 -040027//const sessionStore = new RedisStore({ client: redis })
simond47ef9e2022-09-28 22:24:28 -040028const sessionStore = new session.MemoryStore();
Larbi Gharibe9af9732021-03-31 15:08:01 +010029
simon7a7b4d52022-09-23 02:09:42 -040030interface UserConfig {
31 accounts: string;
32 password?: string;
33 username?: string;
simond47ef9e2022-09-28 22:24:28 -040034 type?: string;
simon7a7b4d52022-09-23 02:09:42 -040035}
36
37interface AppConfig {
simond47ef9e2022-09-28 22:24:28 -040038 users: Record<string, UserConfig>;
39 authMethods: any[];
simon7a7b4d52022-09-23 02:09:42 -040040}
41
42const loadConfig = async (filePath: string): Promise<AppConfig> => {
simond47ef9e2022-09-28 22:24:28 -040043 const config = { users: {}, authMethods: [] };
44 try {
45 return Object.assign(config, JSON.parse((await fs.readFile(filePath)).toString()));
46 } catch (e) {
47 console.log(e);
48 return config;
49 }
50};
Adrien Béraud824a7132021-04-17 17:25:27 -040051
simon7a7b4d52022-09-23 02:09:42 -040052const saveConfig = (filePath: string, config: AppConfig) => {
simond47ef9e2022-09-28 22:24:28 -040053 return fs.writeFile(filePath, JSON.stringify(config));
54};
Adrien Béraude74741b2021-04-19 13:22:54 -040055
Larbi Gharibe9af9732021-03-31 15:08:01 +010056/*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040057Share sessions between Passport.js and Socket.io
Larbi Gharibe9af9732021-03-31 15:08:01 +010058*/
59
60function logSuccess() {
simond47ef9e2022-09-28 22:24:28 -040061 console.log('passportSocketIo authorized user with Success 😁');
Larbi Gharibe9af9732021-03-31 15:08:01 +010062}
63
64function logFail() {
simond47ef9e2022-09-28 22:24:28 -040065 console.log('passportSocketIo failed to authorized user 👺');
Larbi Gharibe9af9732021-03-31 15:08:01 +010066}
67
68/*
Larbi Gharibe9af9732021-03-31 15:08:01 +010069
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040070tempAccounts holds users accounts while tempting to authenticate them on Jams.
71connectedUsers holds users accounts after they got authenticated by Jams.
Larbi Gharibe9af9732021-03-31 15:08:01 +010072
Adrien Béraud3b5d9a62021-04-17 18:40:27 -040073Users should be removed from connectedUsers when receiving a disconnect
74web socket call
Larbi Gharibe9af9732021-03-31 15:08:01 +010075
76*/
simond47ef9e2022-09-28 22:24:28 -040077const tempAccounts: Record<string, any> = {};
78const connectedUsers: Record<string, any> = {};
Larbi Gharibe9af9732021-03-31 15:08:01 +010079
simon7a7b4d52022-09-23 02:09:42 -040080const createServer = async (appConfig: AppConfig) => {
simond47ef9e2022-09-28 22:24:28 -040081 const node_env = process.env.NODE_ENV || 'development';
82 const app = express();
83 console.log(`Loading server for ${node_env} with config:`);
84 console.log(appConfig);
Larbi Gharibe9af9732021-03-31 15:08:01 +010085
simond47ef9e2022-09-28 22:24:28 -040086 const corsOptions = {
87 origin: 'http://127.0.0.1:3000',
88 };
Adrien Béraud4e287b92021-04-24 16:15:56 -040089
simond47ef9e2022-09-28 22:24:28 -040090 if (node_env === 'development') {
91 const webpack = await import('webpack');
92 const webpackDev = await import('webpack-dev-middleware');
93 const webpackHot = await import('webpack-hot-middleware');
94 const { default: webpackConfig } = await import('jami-web-client/webpack.config.js');
simonc7d52452022-09-23 02:09:42 -040095
simond47ef9e2022-09-28 22:24:28 -040096 const compiler = webpack.default(webpackConfig);
97 app.use(
98 webpackDev.default(compiler, {
99 publicPath: webpackConfig.output?.publicPath,
100 })
101 );
102 app.use(webpackHot.default(compiler));
103 }
Larbi Gharibe9af9732021-03-31 15:08:01 +0100104
simond47ef9e2022-09-28 22:24:28 -0400105 /*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400106 Configuation for Passeport Js
107 */
simond47ef9e2022-09-28 22:24:28 -0400108 app.disable('x-powered-by');
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400109
simond47ef9e2022-09-28 22:24:28 -0400110 const secret_key = process.env.SECRET_KEY_BASE;
simon7a7b4d52022-09-23 02:09:42 -0400111
simond47ef9e2022-09-28 22:24:28 -0400112 if (!secret_key) {
113 throw new Error('SECRET_KEY_BASE undefined');
114 }
115
116 const sessionMiddleware = session({
117 store: sessionStore,
118 resave: false,
119 saveUninitialized: true,
120 cookie: {
121 secure: false, //!development,
122 maxAge: 2419200000,
123 },
124 secret: secret_key,
125 });
126
127 app.use(sessionMiddleware);
128 app.use(passport.initialize());
129 app.use(passport.session());
130 // app.use(app.router)
131 app.use(cors(corsOptions));
132
133 const jami = new JamiDaemon((account: Account, conversation: any, message: any) => {
134 console.log('JamiDaemon onMessage');
135
136 if (conversation.listeners) {
137 Object.values(conversation.listeners).forEach((listener: any) => {
138 listener.socket.emit('newMessage', message);
139 });
simon7a7b4d52022-09-23 02:09:42 -0400140 }
simond47ef9e2022-09-28 22:24:28 -0400141 });
142 const apiRouter = new JamiRestApi(jami).getRouter();
simon7a7b4d52022-09-23 02:09:42 -0400143
simond47ef9e2022-09-28 22:24:28 -0400144 /*
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400145 io.use(passportSocketIo.authorize({
146 key: 'connect.sid',
147 secret: process.env.SECRET_KEY_BASE,
148 store: sessionStore,
149 passport: passport,
150 cookieParser: cookieParser,
151 //success: logSuccess(),
152 // fail: logFail(),
Adrien Béraude74741b2021-04-19 13:22:54 -0400153 }))
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400154 */
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400155
simond47ef9e2022-09-28 22:24:28 -0400156 const isSetupComplete = () => {
157 return 'admin' in appConfig.users;
158 };
159
160 const accountFilter = (filter: string | any[]) => {
161 if (typeof filter === 'string') {
162 if (filter === '*') return undefined;
163 else return (account: Account) => account.getId() === filter;
164 } else if (Array.isArray(filter)) {
165 return (account: Account) => filter.includes(account.getId());
166 } else {
167 throw new Error('Invalid account filter string');
Adrien Béraude74741b2021-04-19 13:22:54 -0400168 }
simond47ef9e2022-09-28 22:24:28 -0400169 };
Adrien Béraude74741b2021-04-19 13:22:54 -0400170
simond47ef9e2022-09-28 22:24:28 -0400171 const user = (id: string, config: UserConfig) => {
172 return {
173 id,
174 config,
175 username: config.username || id,
176 accountFilter: accountFilter(config.accounts),
177 };
178 };
179
180 passport.serializeUser((user: any, done) => {
181 connectedUsers[user.id] = user.config;
182 console.log('=============================SerializeUser called ' + user.id);
183 console.log(user);
184 done(null, user.id);
185 });
186
187 const deserializeUser = (id: string, done: (err: any, user?: Express.User | false | null) => void) => {
188 console.log('=============================DeserializeUser called on: ' + id);
189 const userConfig = connectedUsers[id];
190 console.log(userConfig);
191 if (userConfig) {
192 done(null, user(id, userConfig));
193 } else done(404, null);
194 };
195 passport.deserializeUser(deserializeUser);
196
197 const jamsStrategy = new LocalStrategy(async (username, password, done) => {
198 const accountId = await jami.addAccount({
199 managerUri: 'https://jams.savoirfairelinux.com',
200 managerUsername: username,
201 archivePassword: password,
202 });
203 const id = `jams_${username}`;
204 const userConfig = { username, type: 'jams', accounts: accountId };
205 const newUser = user(id, userConfig);
206 console.log('AccountId: ' + accountId);
207 tempAccounts[accountId] = { done, newUser };
208 });
209 jamsStrategy.name = 'jams';
210
211 const localStrategy = new LocalStrategy((username, password, done) => {
212 console.log('localStrategy: ' + username + ' ' + password);
213
214 const id = username;
215 const userConfig = appConfig.users[username];
216 if (!userConfig) {
217 return done(null, false, { message: 'Incorrect username.' });
Adrien Béraude74741b2021-04-19 13:22:54 -0400218 }
simond47ef9e2022-09-28 22:24:28 -0400219 if (userConfig.password !== password) {
220 return done(null, false, { message: 'Incorrect password.' });
Adrien Béraude74741b2021-04-19 13:22:54 -0400221 }
simond47ef9e2022-09-28 22:24:28 -0400222 userConfig.type = 'local';
Adrien Béraude74741b2021-04-19 13:22:54 -0400223
simond47ef9e2022-09-28 22:24:28 -0400224 done(null, user(id, userConfig));
225 });
Adrien Béraud6ecaa402021-04-06 17:37:25 -0400226
simond47ef9e2022-09-28 22:24:28 -0400227 passport.use(jamsStrategy);
228 passport.use(localStrategy);
229
230 const secured = (req: Request, res: Response, next: NextFunction) => {
231 if (req.user) {
232 return next();
Adrien Béraude74741b2021-04-19 13:22:54 -0400233 }
simond47ef9e2022-09-28 22:24:28 -0400234 res.status(401).end();
235 };
236 const securedRedirect = (req: Request, res: Response, next: NextFunction) => {
237 if (req.user && (req.user as any)?.accountId) {
238 return next();
Adrien Béraude74741b2021-04-19 13:22:54 -0400239 }
simond47ef9e2022-09-28 22:24:28 -0400240 (req.session as any).returnTo = req.originalUrl;
241 res.redirect('/login');
242 };
243
244 app.use(express.json());
245 app.post('/setup', (req, res) => {
246 if (isSetupComplete()) {
247 return res.status(404).end();
Adrien Béraude74741b2021-04-19 13:22:54 -0400248 }
simond47ef9e2022-09-28 22:24:28 -0400249 if (!req.body.password) {
250 return res.status(400).end();
Adrien Béraude5cad982021-06-07 10:05:50 -0400251 }
simond47ef9e2022-09-28 22:24:28 -0400252 console.log(req.body);
253 appConfig.users.admin = {
254 accounts: '*',
255 password: req.body.password,
256 };
257 res.status(200).end();
258 saveConfig(configPath, appConfig);
259 });
260 app.post('/auth/jams', passport.authenticate('jams'), (req, res) => {
261 res.json({ loggedin: true });
262 });
263 app.post('/auth/local', passport.authenticate('local'), (req, res) => {
264 res.json({ loggedin: true, user: (req.user as any)?.id });
265 });
Adrien Béraude5cad982021-06-07 10:05:50 -0400266
simond47ef9e2022-09-28 22:24:28 -0400267 const getState = (req: Request) => {
268 if (req.user) {
269 const user = (req.user || {}) as UserConfig;
270 return { loggedin: true, username: user.username, type: user.type };
271 } else if (isSetupComplete()) {
272 return {};
273 } else {
274 return { setupComplete: false };
275 }
276 };
idillon452e2102022-09-16 13:23:28 -0400277
simond47ef9e2022-09-28 22:24:28 -0400278 // sentrySetUp(app);
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400279
simond47ef9e2022-09-28 22:24:28 -0400280 app.get('/auth', (req, res) => {
281 const state = getState(req);
282 if (req.user) {
283 res.json(state);
284 } else {
285 res.status(401).json(state);
286 }
287 });
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400288
simond47ef9e2022-09-28 22:24:28 -0400289 app.use('/api', secured, apiRouter);
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400290
simond47ef9e2022-09-28 22:24:28 -0400291 app.use('/', indexRouter);
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400292
simond47ef9e2022-09-28 22:24:28 -0400293 /* GET React App */
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400294
simond47ef9e2022-09-28 22:24:28 -0400295 const cwd = process.cwd();
296 app.use(express.static(path.join(cwd, 'client/dist')));
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400297
simond47ef9e2022-09-28 22:24:28 -0400298 app.use((req, res) => {
299 res.render(path.join(cwd, 'client/dist/index.ejs'), {
300 initdata: JSON.stringify(getState(req)),
301 });
302 });
idillon452e2102022-09-16 13:23:28 -0400303
simond47ef9e2022-09-28 22:24:28 -0400304 // @ts-ignore TODO: Fix the typescript error
305 const server = http.Server(app);
Adrien Béraud4e287b92021-04-24 16:15:56 -0400306
simond47ef9e2022-09-28 22:24:28 -0400307 const io = new Server(server, { cors: corsOptions });
308 const wrap = (middleware: any) => (socket: Socket, next: (err?: ExtendedError) => void) =>
309 middleware(socket.request, {}, next);
310 io.use(wrap(sessionMiddleware));
311 io.use(wrap(passport.initialize()));
312 io.use(wrap(passport.session()));
313 io.use((socket, next) => {
314 if ((socket.request as any).user) {
315 next();
316 } else {
317 next(new Error('unauthorized'));
318 }
319 });
320 io.on('connect', (socket) => {
321 console.log(`new connection ${socket.id}`);
322 const session = (socket.request as any).session;
323 console.log(`saving sid ${socket.id} in session ${session.id}`);
324 session.socketId = socket.id;
325 session.save();
Adrien Béraudabba2e52021-04-24 21:39:56 -0400326
simond47ef9e2022-09-28 22:24:28 -0400327 socket.on('conversation', (data) => {
328 console.log('io conversation');
329 console.log(data);
330 if (session.conversation) {
331 console.log(`disconnect from old conversation ${session.conversation.conversationId}`);
332 const conversation = jami.getConversation(session.conversation.accountId, session.conversation.conversationId);
333 delete conversation.listeners[socket.id];
334 }
335 session.conversation = { accountId: data.accountId, conversationId: data.conversationId };
336 const conversation = jami.getConversation(data.accountId, data.conversationId);
337 if (!conversation.listeners) conversation.listeners = {};
338 conversation.listeners[socket.id] = {
339 socket,
340 session,
341 };
342 session.save();
343 });
344 });
Adrien Béraud4e287b92021-04-24 16:15:56 -0400345
simond47ef9e2022-09-28 22:24:28 -0400346 return server;
347};
Adrien Béraud3b5d9a62021-04-17 18:40:27 -0400348
Adrien Béraude74741b2021-04-19 13:22:54 -0400349loadConfig(configPath)
simond47ef9e2022-09-28 22:24:28 -0400350 .then(createServer)
351 .then((server) => {
352 server.listen(3000);
353 });