mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-26 08:02:19 +00:00
120 lines
4.1 KiB
TypeScript
120 lines
4.1 KiB
TypeScript
import setToken from "./helper/setToken";
|
|
import type Token from "./api/models/auth/Token";
|
|
|
|
export function isStrongPassword(password: string): boolean {
|
|
const minLength = 8;
|
|
const hasUpperCase = /[A-Z]/.test(password);
|
|
const hasLowerCase = /[a-z]/.test(password);
|
|
const hasNumber = /\d/.test(password);
|
|
|
|
return (
|
|
password.length >= minLength &&
|
|
hasUpperCase &&
|
|
hasLowerCase &&
|
|
hasNumber
|
|
);
|
|
}
|
|
|
|
const ACCESS_TOKEN = "access_token"
|
|
const ACCESS_TOKEN_EXPIRATION = "access_token_expires_at"
|
|
const REFRESH_TOKEN = "refresh_token"
|
|
const REFRESH_TOKEN_EXPIRATION = "refresh_token_expires_at"
|
|
|
|
export const TOKENS = [
|
|
ACCESS_TOKEN, ACCESS_TOKEN_EXPIRATION, REFRESH_TOKEN, REFRESH_TOKEN_EXPIRATION
|
|
]
|
|
|
|
const toDate = (value: unknown): Date => value instanceof Date ? value : new Date(String(value));
|
|
|
|
export const saveTokens = (data: Record<string, unknown>) => {
|
|
TOKENS.forEach((k) => {
|
|
const value = data[k];
|
|
if (value === null || value === undefined) {
|
|
localStorage.removeItem(k);
|
|
return;
|
|
}
|
|
|
|
const str = value instanceof Date ? value.toISOString() : String(value);
|
|
localStorage.setItem(k, str);
|
|
});
|
|
|
|
const access = data[ACCESS_TOKEN];
|
|
const accessExp = data[ACCESS_TOKEN_EXPIRATION];
|
|
const refresh = data[REFRESH_TOKEN];
|
|
const refreshExp = data[REFRESH_TOKEN_EXPIRATION];
|
|
|
|
if (access && accessExp && refresh && refreshExp) {
|
|
const token: Token = {
|
|
access_token: String(access),
|
|
access_token_expires_at: toDate(accessExp),
|
|
refresh_token: String(refresh),
|
|
refresh_token_expires_at: toDate(refreshExp),
|
|
};
|
|
setToken(token);
|
|
}
|
|
}
|
|
|
|
// Everything written to browser storage that belongs to the signed-in person
|
|
// rather than to the browser. Prefix-matched because the keys embed ids.
|
|
//
|
|
// Reply drafts are the reason this exists: a draft holds the full body,
|
|
// subject and recipients of an unsent email, keyed by user, org and thread, and
|
|
// it survived signing out on a shared machine. Column widths and dismissed
|
|
// banners are deliberately not here; they are not the user's data.
|
|
const SESSION_SCOPED_KEY_PREFIXES = [
|
|
"warmbly-reply-draft:",
|
|
];
|
|
|
|
const SESSION_SCOPED_KEYS = [
|
|
"sso_binding",
|
|
];
|
|
|
|
// clearTokens ends the client's half of the session: the tokens, and the
|
|
// content those tokens were used to fetch. Called from logout and from every
|
|
// path that discovers the session is gone, so neither leaves the other behind.
|
|
export const clearTokens = () => {
|
|
TOKENS.forEach((k) => localStorage.removeItem(k));
|
|
SESSION_SCOPED_KEYS.forEach((k) => {
|
|
localStorage.removeItem(k);
|
|
sessionStorage.removeItem(k);
|
|
});
|
|
|
|
// Collect first, then remove: removing while iterating shifts the indices.
|
|
const stale: string[] = [];
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (key && SESSION_SCOPED_KEY_PREFIXES.some((p) => key.startsWith(p))) stale.push(key);
|
|
}
|
|
stale.forEach((k) => localStorage.removeItem(k));
|
|
|
|
setToken(null);
|
|
}
|
|
|
|
// SESSION_ENDED_EVENT is how the api client, which is not a component and
|
|
// holds no router, says the session is over.
|
|
//
|
|
// Clearing the tokens was never enough on its own. The three queries
|
|
// UserProvider watches are cached and none of them polls, so nothing re-ran to
|
|
// notice, and the app stayed on a page it could no longer authenticate while
|
|
// the socket retried its handshake every few seconds for as long as the tab
|
|
// was open.
|
|
export const SESSION_ENDED_EVENT = "warmbly:session-ended";
|
|
|
|
// announceSessionEnded says the session is over without touching storage.
|
|
// Separate from endSession because the "there is no token at all" path has
|
|
// nothing to clear, and clearing there would take the SSO binding and the
|
|
// reply drafts with it on a path that also runs before anyone has signed in.
|
|
export const announceSessionEnded = () => {
|
|
if (typeof window !== "undefined") {
|
|
window.dispatchEvent(new Event(SESSION_ENDED_EVENT));
|
|
}
|
|
};
|
|
|
|
// endSession ends the session and says so. Use it wherever the server has
|
|
// refused the credentials; clearTokens alone is for a sign-out that already
|
|
// owns the navigation.
|
|
export const endSession = () => {
|
|
clearTokens();
|
|
announceSessionEnded();
|
|
};
|