Select Git revision
SessionManager.ts 12.24 KiB
import * as jwt from 'jsonwebtoken';
import User from '../sharedByClients/models/User';
import LocalConfigKeys from '../types/LocalConfigKeys';
import axios, { HttpStatusCode } from 'axios';
import HttpManager from './HttpManager';
import ora from 'ora';
import Permissions from '../types/Permissions';
import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute';
import DojoBackendManager from './DojoBackendManager';
import Config from '../config/Config';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
import DojoGitlabCredentials from '../sharedByClients/types/Dojo/DojoGitlabCredentials';
import * as http from 'http';
import EventEmitter from 'events';
import SharedConfig from '../shared/config/SharedConfig';
import chalk from 'chalk';
import inquirer from 'inquirer';
import SharedGitlabManager from '../shared/managers/SharedGitlabManager';
import GitlabManager from './GitlabManager';
import GitlabToken from '../shared/types/Gitlab/GitlabToken';
import open from 'open';
import { sessionConfigFile } from '../config/ConfigFiles';
class LoginServer {
readonly events: EventEmitter = new EventEmitter();
private server: http.Server;
constructor() {
this.server = http.createServer((req, res) => {
const sendError = (error: string) => {
this.events.emit('error', error);
res.writeHead(HttpStatusCode.InternalServerError, { 'Content-Type': 'text/html' });
res.write(`<html lang="en"><body><h1 style="color: red">DojoCLI login error</h1><h3>Please look at your CLI for more informations.</h3></body></html>`);
res.end();
};
if ( req.url?.match(Config.login.server.route) ) {
const urlParts = req.url.split('=');
if ( urlParts.length > 0 ) {
this.events.emit('code', urlParts[1]);
res.writeHead(HttpStatusCode.Ok, { 'Content-Type': 'text/html' });
res.write(`<html lang="en"><body><h1 style="color: green">DojoCLI login successful</h1><h3>You can close this window.</h3></body></html>`);
res.end();
return;
}
sendError(`Incorrect call => ${ req.url }`);
return;
}
//sendError(`Unknown route call => ${ req.url }`);
});
}
start() {
try {
this.server.listen(Config.login.server.port);
this.events.emit('started');
} catch ( error ) {
this.events.emit('error', error);
}
}
stop() {
try {
this.server.close();
this.events.emit('stopped');
} catch ( error ) {
this.events.emit('error', error);
}
}
}
class SessionManager {
public profile: User | undefined = undefined;
get isLogged(): boolean {
return this.apiToken !== null && this.apiToken !== '';
}
get apiToken(): string {
const apisToken = sessionConfigFile.getParam(LocalConfigKeys.APIS_TOKEN) as null | { [key: string]: string };
if ( apisToken !== null && ClientsSharedConfig.apiURL in apisToken ) {
return apisToken[ClientsSharedConfig.apiURL];
}
return '';
}
set apiToken(token: string) {
let apisToken = sessionConfigFile.getParam(LocalConfigKeys.APIS_TOKEN) as null | { [key: string]: string };
if ( apisToken === null ) {
apisToken = {};
}
apisToken[ClientsSharedConfig.apiURL] = token;
sessionConfigFile.setParam(LocalConfigKeys.APIS_TOKEN, apisToken);
try {
const payload = jwt.decode(token);
if ( payload && typeof payload === 'object' && payload.profile ) {
this.profile = payload.profile as User;
}
} catch ( error ) {
this.profile = undefined;
}
}
get gitlabCredentials(): DojoGitlabCredentials {
return sessionConfigFile.getParam(LocalConfigKeys.GITLAB) as DojoGitlabCredentials;
}
set gitlabCredentials(credentials: DojoGitlabCredentials) {
sessionConfigFile.setParam(LocalConfigKeys.GITLAB, credentials);
}
constructor() { }
private async getGitlabCodeFromHeadlessEnvironment(): Promise<string> {
const indent: string = ' ';
console.log(`${ indent }Please open the following URL in your web browser and accept to give the requested permissions to Dojo:`);
console.log(chalk.blue(`${ indent }${ Config.login.gitlab.url.code }`));
console.log(`${ indent }Then, copy the code at the end of the redirected url and paste it bellow.`);
console.log(`${ indent }Example of url (here the code is 123456): ${ chalk.blue(`${ SharedConfig.login.gitlab.url.redirect }?code=`) }${ chalk.green('123456') }`);
return (await inquirer.prompt({
type : 'password',
name : 'code',
message: `${ chalk.green('?') } Please paste the Gitlab code here`,
mask : '*',
prefix : ' '
})).code;
}
private getGitlabCodeFromGraphicEnvironment(): Promise<string> {
return new Promise<string>((resolve, reject) => {
ora({
text : 'GUI mode (for headless mode, use the --cli option)',
indent: 4
}).start().info();
let currentSpinner: ora.Ora = ora({
text : 'Starting login server',
indent: 4
}).start();
const loginServer = new LoginServer();
loginServer.events.on('started', () => {
currentSpinner.succeed('Login server started');
currentSpinner = ora({
text : `Waiting for user to authorize the application in his web browser. If the browser does not open automatically, please go to : ${ Config.login.gitlab.url.code }`,
indent: 4
}).start();
open(Config.login.gitlab.url.code).then();
});
loginServer.events.on('error', (error: string) => {
currentSpinner.fail(`Login server error: ${ error }`);
reject();
});
loginServer.events.on('stopped', () => {
currentSpinner.succeed('Login server stopped');
});
loginServer.events.on('code', (code: string) => {
currentSpinner.succeed('Login code received');
currentSpinner = ora({
text : 'Stopping login server',
indent: 4
}).start();
loginServer.events.on('stopped', () => {
resolve(code);
});
loginServer.stop();
resolve(code);
});
loginServer.start();
});
}
async login(headless: boolean = false) {
try {
this.logout();
} catch ( error ) {
console.log(error);
ora('Unknown error').start().fail();
throw error;
}
ora(`Login with Gitlab (${ SharedConfig.gitlab.URL }):`).start().info();
let gitlabCode: string;
if ( !headless ) {
gitlabCode = await this.getGitlabCodeFromGraphicEnvironment();
} else {
gitlabCode = await this.getGitlabCodeFromHeadlessEnvironment();
}
const gitlabTokensSpinner = ora({
text : 'Retrieving gitlab tokens',
indent: 4
}).start();
let gitlabTokens: GitlabToken;
try {
gitlabTokens = await SharedGitlabManager.getTokens(gitlabCode);
this.gitlabCredentials = {
refreshToken: gitlabTokens.refresh_token,
accessToken : gitlabTokens.access_token
};
gitlabTokensSpinner.succeed('Gitlab tokens retrieved');
} catch ( error ) {
gitlabTokensSpinner.fail('Error while retrieving gitlab tokens');
throw error;
}
const isGitlabTokensValid = (await GitlabManager.testToken()).every((value) => value);
if ( !isGitlabTokensValid ) {
throw new Error('Gitlab tokens are invalid');
}
ora(`Login to Dojo backend:`).start().info();
const dojoLoginSpinner = ora({
text : 'Login to Dojo backend',
indent: 4
}).start();
try {
await DojoBackendManager.login(gitlabTokens);
dojoLoginSpinner.succeed('Logged in');
} catch ( error ) {
dojoLoginSpinner.fail('Login failed');
throw error;
}
await this.testSession(true);
}
async refreshTokens() {
const gitlabTokens = await DojoBackendManager.refreshTokens(this.gitlabCredentials.refreshToken!);
this.gitlabCredentials = {
refreshToken: gitlabTokens.refresh_token,
accessToken : gitlabTokens.access_token
};
}
logout() {
this.apiToken = '';
this.gitlabCredentials = {
refreshToken: '',
accessToken : ''
};
}
checkPermissions(verbose: boolean = true, indent: number = 8, checkPermissions: Array<string> | null = []): Permissions {
const hasPermission = (permissionPredicate: () => boolean, verboseText: string): boolean => {
const isAllowed: boolean = this.profile !== undefined && permissionPredicate();
if ( verbose ) {
const spinner: ora.Ora = ora({
text : verboseText,
indent: indent
}).start();
isAllowed ? spinner.succeed() : spinner.fail();
}
return isAllowed;
};
return {
student : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('student')) ? hasPermission(() => true, 'Student permissions') : false,
teachingStaff: checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('teachingStaff')) ? hasPermission(() => this.profile?.isTeachingStaff ?? false, 'Teaching staff permissions') : false,
admin : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('admin')) ? hasPermission(() => this.profile?.isAdmin ?? false, 'Admin permissions') : false
};
}
async testSession(verbose: boolean = true, checkPermissions: Array<string> | null = []): Promise<false | Permissions> {
if ( verbose ) {
ora('Checking Dojo session: ').start().info();
}
HttpManager.handleAuthorizationCommandErrors = false;
const spinner: ora.Ora = ora({
text : `Testing Dojo session`,
indent: 4
});
if ( verbose ) {
spinner.start();
}
try {
await axios.get(DojoBackendManager.getApiUrl(ApiRoute.TEST_SESSION), {});
if ( verbose ) {
spinner.succeed(`The session is valid`);
}
} catch ( error ) {
if ( verbose ) {
spinner.fail(`The session is invalid`);
}
return false;
}
return this.checkPermissions(verbose, 8, checkPermissions);
}
}
export default new SessionManager();