Files
SweetHome/app/services/AuthService.js

220 lines
5.6 KiB
JavaScript

import Cookies from "js-cookie";
import { getCustomerByUserId, getOwnerByUserId, loginWithEmail, loginWithPhone } from "../utils/api";
/**
* AuthService
* Manages authentication tokens and user role detection via JWT decoding.
*
* Roles (from JWT claims):
* - Owner: roles array contains "Owner"
* - Customer: authenticated but no "Owner" role
* - Guest: no token
*
* Methods:
* addToken(token) — store JWT token
* getToken() — retrieve JWT token
* deleteToken() — remove JWT token
* decodeToken() — decode JWT payload
* getUser() — get decoded user info
* getRoles() — get roles array from JWT
* isOwner() — check if user has Owner role
* isCustomer() — check if user is authenticated but not Owner
* isGuest() — check if no token exists
* isAuthenticated() — check if token exists
*/
const TOKEN_KEY = "auth_token";
const USER_KEY = "cached_user";
const AuthService = Object.freeze({
addToken(token) {
if (!token || typeof token !== "string") return;
Cookies.set(TOKEN_KEY, token, {
expires: 7,
secure: true,
sameSite: "lax",
path: "/",
});
},
getToken() {
if (typeof window === "undefined") return null;
return Cookies.get(TOKEN_KEY);
},
deleteToken() {
Cookies.remove(USER_KEY);
Cookies.remove(TOKEN_KEY);
},
/**
* Cache full user profile (from API)
* @param {object} user — { name, email, phone, ... }
*/
cacheUser(user) {
Cookies.set(USER_KEY, JSON.stringify(user), {
expires: 7,
secure: true,
sameSite: "lax",
path: "/",
});
},
/**
* Get cached user profile
* @returns {object|null}
*/
getCachedUser() {
const user = Cookies.get(USER_KEY);
if (!user) return null;
try {
return JSON.parse(user);
} catch {
return null;
}
},
/**
* Decode JWT payload (base64)
* @returns {object|null}
*/
decodeToken() {
const token = this.getToken();
if (!token) return null;
try {
const payload = token.split(".")[1];
return JSON.parse(atob(payload));
} catch {
return null;
}
},
/**
* Extract user info from JWT
* @returns {object|null} — { id, name, email, phone, roles }
*/
getUser() {
const payload = this.decodeToken();
if (!payload) return null;
const cached = this.getCachedUser();
return {
id: payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"] || payload.sub || null,
name: cached?.name || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"] || null,
email: cached?.email || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] || null,
phone: cached?.phone || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone"] || null,
roles: cached?.roles,
};
},
/**
* Get current authenticated user id
* @returns {number|string|null}
*/
getUserId() {
const user = this.getUser();
if (!user?.id) return null;
const parsedId = Number(user.id);
return Number.isFinite(parsedId) ? parsedId : user.id;
},
/**
* Get roles array from JWT
* @returns {string[]}
*/
getRoles() {
const payload = this.decodeToken();
if (!payload) return [];
const roles = payload["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
if (Array.isArray(roles)) return roles;
if (typeof roles === "string") return [roles];
return [];
},
async cacheCurrentUser(getOwnerByUserId, getCustomerByUserId) {
const authUser = this.getUser();
if (!authUser?.id) {
return;
}
const fetchFn = this.isOwner() ? getOwnerByUserId : getCustomerByUserId;
const profile = await fetchFn(authUser.id);
this.cacheUser({
name: profile.firstName + profile.lastName,
email: profile.email,
phone: profile.phoneNumber,
roles: this.getRoles(),
});
},
/**
* User has Owner role
* @returns {boolean}
*/
isOwner() {
return this.getRoles().includes("Owner");
},
/**
* User has RealEstateAgent role
* @returns {boolean}
*/
isAgent() {
return this.getRoles().includes("RealEstateAgent");
},
/**
* Authenticated user without Owner role (i.e. customer)
* @returns {boolean}
*/
isCustomer() {
return this.isAuthenticated() && !this.isOwner() && !this.isAgent();
},
/**
* No token — guest user
* @returns {boolean}
*/
isGuest() {
return !this.getToken();
},
/**
* Token exists
* @returns {boolean}
*/
isAuthenticated() {
return !!this.getToken();
},
async login({ loginMethod, credential, password }) {
const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone;
const result = await loginFn(credential, password);
switch (result.status) {
case 200: {
const token = typeof result.data === "string" ? result.data : result.data.token;
this.addToken(token);
await this.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId);
return {
type: "SUCCESS",
};
}
case 206: {
const token = typeof result.data === "string" ? result.data : result.data.token;
this.addToken(token);
if (loginMethod === "email") await sendEmailOTP();
else await sendPhoneOTP();
return {
type: "OTP_REQUIRED",
};
}
default:
return {
type: "ERROR",
result,
};
}
},
});
export default AuthService;