// The MIT License (MIT) // // Copyright (c) 2016-2024 Camptocamp SA // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import * as Sentry from '@sentry/browser'; import * as $ from 'jquery'; import user, {User, UserState, loginMessageRequired} from 'gmfapi/store/user'; import configuration, { Configuration, gmfAuthenticationNoReloadRole as gmfOptionsGmfAuthenticationNoReloadRole, } from 'gmfapi/store/config'; type AuthenticationDefaultResponse = { success: boolean; }; let userTransformer = (user: User) => user; /** * Method defined in the aim to be overridden. * @param fn The callback function to apply after login. */ export function setOnSuccessfulLoginFunction(fn: typeof userTransformer): void { userTransformer = fn; } export enum RouteSuffix { CHANGE_PASSWORD = 'loginchangepassword', IS_LOGGED_IN = 'loginuser', LOGIN = 'login', LOGOUT = 'logout', RESET_PASSWORD = 'loginresetpassword', } /** * An "authentication" service for a GeoMapFish application. Upon loading, it * launches a request to determine whether a user is currently logged in or * not. * * The possible API requests it supports, which are all self-explanatory, are: * * - changePassword * - login * - logout * - resetPassword */ export class AuthenticationService { /** * The authentication url without trailing slash. * @private */ baseUrl_: string; /** * The user. * @private */ user_: User; /** * Don't request a new user object from the back-end after * logging out if the logged-in user's role has this role. * @private */ noReloadRole_: undefined | gmfOptionsGmfAuthenticationNoReloadRole; verifyConnection_: number; originalUrl: string = window.location.href; constructor() { /** * The authentication url without trailing slash. * @private */ this.baseUrl_ = null; /** * @private */ this.user_ = null; user.getProperties().subscribe({ next: (properties: User) => { this.user_ = properties; }, }); /** * Don't request a new user object from the back-end after * logging out if the logged-in user's role has this role. * @private */ this.noReloadRole_ = null; configuration.getConfig().subscribe({ next: (configuration: Configuration) => { if (configuration) { this.noReloadRole_ = configuration.gmfAuthenticationNoReloadRole; this.baseUrl_ = configuration.authenticationBaseUrl ? configuration.authenticationBaseUrl.replace(/\/$/, '') : null; if (this.baseUrl_) { this.load_(); } } }, }); this.verifyConnection_ = window.setInterval(() => { this.checkConnection_(); }, 60000); } /** * Check whether the user is connected or not like on load. * @private */ checkConnection_(): void { if (this.user_.username && this.user_.is_password_changed !== false) { const url = `${this.baseUrl_}/${RouteSuffix.IS_LOGGED_IN}`; const options: RequestInit = {method: 'GET', credentials: 'include'}; fetch(url, options) .then((resp) => resp.json()) .then((data: User) => { if (this.user_.username !== data.username) { this.handleDisconnection(); } }) .catch((err: Response) => { throw new Error(`Error on connection check: ${err.statusText}`); }); } } handleDisconnection(): void { const noReload = this.noReloadRole_ ? this.getRolesNames().includes(this.noReloadRole_) : false; this.resetUser(UserState.DISCONNECTED, noReload); } /** * Load the authentication service, which sends an async request to * determine whether the user is currently connected or not. * @private */ load_(): void { const url = `${this.baseUrl_}/${RouteSuffix.IS_LOGGED_IN}`; const options: RequestInit = {method: 'GET', credentials: 'include'}; fetch(url, options) .then((resp) => resp.json()) .then((data: User) => this.checkUser_(data)) .then( (data: User) => this.handleLogin_(true, data), () => { throw new Error('Login fail.'); }, ); } /** * @param login Login. * @param oldPwd Old password. * @param newPwd New password. * @param confPwd New password confirmation. * @param {string} [otp] One-time password. * @returns Promise. */ changePassword( login: string, oldPwd: string, newPwd: string, confPwd: string, otp: string = undefined, ): Promise { const url = `${this.baseUrl_}/${RouteSuffix.CHANGE_PASSWORD}`; const options: RequestInit = { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, credentials: 'include', body: $.param({ 'login': login, 'oldPassword': oldPwd, 'otp': otp, 'newPassword': newPwd, 'confirmNewPassword': confPwd, }), }; return fetch(url, options) .then((resp) => resp.json()) .then((data: User) => this.checkUser_(data)) .then( (data) => this.setUser_(data, UserState.LOGGED_IN), () => { throw new Error('Change password fail.'); }, ); } /** * @param login Login name. * @param pwd Password. * @param {string} [otp] One-time password. * @returns Promise. */ login(login: string, pwd: string, otp: string = undefined): Promise { const url = `${this.baseUrl_}/${RouteSuffix.LOGIN}`; const params = { 'login': login, 'password': pwd, // The `originalUrl` is used to be able to get a link with the private layers (that will open the // login panel, with the `loginMessageRequired` message). referrer_to: user.getLoginMessage().value == loginMessageRequired ? this.originalUrl : window.location.href, }; if (otp) { Object.assign(params, {'otp': otp}); } const options: RequestInit = { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, credentials: 'include', body: $.param(params), }; return fetch(url, options) .then((resp) => resp.json()) .then((data: User) => this.checkUser_(data)) .then((data: User) => userTransformer(data)) .then( (data) => this.handleLogin_(false, data), () => { throw new Error('Login fail.'); }, ); } /** * Check the user to have a user with all parameters in all cases. * @param data Ajax response. * @returns Response. */ checkUser_(data: User): User { if (!data) { return data; } const emptyUserProperties = user.getEmptyUserProperties(); data = {...emptyUserProperties, ...data}; return data; } /** * @returns Promise. */ logout(): Promise { const noReload = this.noReloadRole_ ? this.getRolesNames().includes(this.noReloadRole_) : false; const url = `${this.baseUrl_}/${RouteSuffix.LOGOUT}`; const options: RequestInit = {method: 'GET', credentials: 'include'}; return fetch(url, options).then(() => { this.resetUser(UserState.LOGGED_OUT, noReload); }); } /** * @param login Login name. * @returns Promise. */ resetPassword(login: string): Promise { const url = `${this.baseUrl_}/${RouteSuffix.RESET_PASSWORD}`; const options: RequestInit = { method: 'POST', credentials: 'include', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: $.param({'login': login}), }; return fetch(url, options).then((resp) => resp.json().then((data: AuthenticationDefaultResponse) => data), ); } /** * @returns Username */ getUsername(): string | null { return this.user_.username || null; } /** * @returns User's email */ getEmail(): string | null { return this.user_.email || null; } /** * @returns The roles IDs. */ getRolesIds(): number[] { return this.user_.roles ? this.user_.roles.map((role) => role.id) : []; } /** * @returns The roles names. */ getRolesNames(): string[] { return this.user_.roles ? this.user_.roles.map((role) => role.name) : []; } /** * @param checkingLoginStatus Checking the login status? * @param data Ajax response. * @returns Response. * @private */ handleLogin_(checkingLoginStatus: boolean, data: User): User { const userState = checkingLoginStatus ? UserState.READY : UserState.LOGGED_IN; this.setUser_(data, userState); return data; } /** * @param respData Response. * @param userState state of the user. * @private */ setUser_(respData: User, userState: UserState): void { Sentry.setUser({ username: respData.username, }); user.setUser(respData, userState); } /** * @param userState state of the user. * @param noReload Don't request a new user object from * the back-end after logging out, defaults to false. */ resetUser(userState: UserState, noReload: boolean): void { const emptyUserProperties = user.getEmptyUserProperties(); user.setUser(emptyUserProperties, userState); if (!noReload) { this.load_(); } } } const ngeoAuthService = new AuthenticationService(); export default ngeoAuthService;