Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / lib / authentication / logout.ts
blob99c4b8fbc3393b6958cee374225790664ca9333a
1 import { ForkSearchParameters } from '@proton/shared/lib/authentication/fork';
2 import noop from '@proton/utils/noop';
4 import { removeLastRefreshDate } from '../api/helpers/refreshStorage';
5 import { getAppHref } from '../apps/helper';
6 import { getSlugFromApp } from '../apps/slugHelper';
7 import type { APP_NAMES } from '../constants';
8 import { APPS, SSO_PATHS } from '../constants';
9 import { replaceUrl } from '../helpers/browser';
10 import { decodeBase64URL, encodeBase64URL } from '../helpers/encoding';
11 import type { PersistedSession } from './SessionInterface';
12 import type { AuthenticationStore } from './createAuthenticationStore';
13 import { requestFork } from './fork/consume';
14 import type { ExtraSessionForkData } from './interface';
15 import { stripLocalBasenameFromPathname } from './pathnameHelper';
16 import { getPersistedSession, removePersistedSession } from './persistedSessionStorage';
18 const clearRecoveryParam = 'clear-recovery';
20 interface PassedSession {
21     id: string;
22     s: boolean;
25 export const serializeLogoutURL = ({
26     persistedSessions,
27     clearDeviceRecoveryData,
28     appName,
29     url,
30 }: {
31     url: URL;
32     appName: APP_NAMES;
33     persistedSessions: PersistedSession[];
34     clearDeviceRecoveryData: boolean | undefined;
35 }) => {
36     const slug = getSlugFromApp(appName);
37     if (slug && appName !== APPS.PROTONACCOUNT) {
38         url.searchParams.set('product', slug);
39     }
40     if (clearDeviceRecoveryData) {
41         url.searchParams.set(clearRecoveryParam, JSON.stringify(clearDeviceRecoveryData));
42     }
43     if (persistedSessions.length) {
44         const hashParams = new URLSearchParams();
45         const sessions = persistedSessions.map((persistedSession): PassedSession => {
46             return {
47                 id: persistedSession.UserID,
48                 s: persistedSession.isSubUser,
49             };
50         });
51         hashParams.set('sessions', encodeBase64URL(JSON.stringify(sessions)));
52         url.hash = hashParams.toString();
53     }
54     return url;
57 export const getLogoutURL = ({
58     type,
59     appName,
60     mode,
61     persistedSessions: inputPersistedSessions,
62     clearDeviceRecoveryData,
63     reason,
64     localID,
65 }: {
66     type?: 'full' | 'local';
67     appName: APP_NAMES;
68     mode: 'sso' | 'standalone';
69     persistedSessions: PersistedSession[];
70     clearDeviceRecoveryData?: boolean;
71     reason: 'signout' | 'session-expired';
72     localID: number;
73 }) => {
74     if (mode === 'sso') {
75         // If it's not a full logout on account, we just strip the local id from the path in order to get redirected back
76         if (appName === APPS.PROTONACCOUNT && type !== 'full') {
77             const url = new URL(window.location.href);
78             url.pathname = stripLocalBasenameFromPathname(url.pathname);
79             if (localID !== undefined) {
80                 url.searchParams.set(ForkSearchParameters.LocalID, `${localID}`);
81             }
82             return url.toString();
83         }
85         const url = new URL(getAppHref(SSO_PATHS.SWITCH, APPS.PROTONACCOUNT));
86         url.searchParams.set('reason', reason);
87         const persistedSessions = type === 'full' ? inputPersistedSessions : [];
88         return serializeLogoutURL({ appName, url, persistedSessions, clearDeviceRecoveryData }).toString();
89     }
91     return SSO_PATHS.LOGIN;
94 const parseSessions = (sessions: string | null) => {
95     try {
96         const result = JSON.parse(decodeBase64URL(sessions || ''));
97         if (Array.isArray(result)) {
98             return result.map((session): PassedSession => {
99                 return {
100                     id: session.id,
101                     s: session.s === true,
102                 };
103             });
104         }
105         return [];
106     } catch (e) {
107         return [];
108     }
111 export const parseLogoutURL = (url: URL) => {
112     const searchParams = new URLSearchParams(url.search);
113     const hashParams = new URLSearchParams(url.hash.slice(1));
114     const sessions = parseSessions(hashParams.get('sessions'));
115     const reason = searchParams.get('reason') || searchParams.get('flow');
116     return {
117         logout: reason === 'signout' || reason === 'logout',
118         clearDeviceRecoveryData: searchParams.get(clearRecoveryParam) === 'true',
119         sessions,
120     };
123 export const handleLogout = async ({
124     appName,
125     authentication,
126     type,
127     clearDeviceRecoveryData,
128     reason = 'signout',
129     extra,
130 }: {
131     appName: APP_NAMES;
132     type: 'full' | 'local';
133     authentication: AuthenticationStore;
134     clearDeviceRecoveryData?: boolean;
135     reason?: 'signout' | 'session-expired';
136     extra?: ExtraSessionForkData;
137 }) => {
138     const UID = authentication.UID;
139     const localID = extra?.localID ?? authentication.localID;
140     const mode = authentication.mode;
142     const persistedSessions: PersistedSession[] = [];
144     if (UID) {
145         removeLastRefreshDate(UID);
146     }
148     if (localID !== undefined && mode === 'sso') {
149         const persistedSession = getPersistedSession(localID);
150         if (persistedSession) {
151             persistedSessions.push(persistedSession);
152             await removePersistedSession(localID, UID).catch(noop);
153         }
154     }
156     authentication.logout();
158     if (appName === APPS.PROTONACCOUNT || appName === APPS.PROTONVPN_SETTINGS || mode !== 'sso' || type === 'full') {
159         replaceUrl(
160             getLogoutURL({
161                 type,
162                 appName,
163                 reason,
164                 mode,
165                 persistedSessions,
166                 clearDeviceRecoveryData,
167                 localID,
168             })
169         );
170     } else {
171         requestFork({ fromApp: appName, localID, reason, extra });
172     }
175 export const handleInvalidSession = ({
176     appName,
177     authentication,
178     extra,
179 }: {
180     appName: APP_NAMES;
181     authentication: AuthenticationStore;
182     extra?: ExtraSessionForkData;
183 }) => {
184     // A session that is invalid should just do a local deletion on its own subdomain, to check if the session still exists on account.
185     handleLogout({
186         appName,
187         reason: 'session-expired',
188         authentication,
189         type: 'local',
190         clearDeviceRecoveryData: false,
191         extra,
192     });