main
ts 170 lines 4.33 KB
Raw
1 import type { TOTPValidateRequest } from "@/api/endpoints/totp"
2 import type { AuthUser, JWTPayload, LoginPayload, RouteMetaAuthRole } from "@/types/auth.d"
3 import type { ApiError } from "@/types/common.d"
4 import * as jose from "jose"
5 import _castArray from "lodash/castArray"
6 import _toLower from "lodash/toLower"
7 import _toNumber from "lodash/toNumber"
8 import { acceptHMRUpdate, defineStore } from "pinia"
9 import Api from "@/api"
10 import { AuthUserRole, RouteRole } from "@/types/auth.d"
11 import { getAvatar, getNameInitials } from "@/utils"
12 import { jwtRoleToUserRole } from "@/utils/auth"
13 import { piniaStorage, removePersistentSessionKey } from "@/utils/secure-storage"
14
15 export const useAuthStore = defineStore("auth", {
16 state: () => ({
17 user: {
18 access_token: "",
19 username: "",
20 email: "",
21 role: AuthUserRole.Unknown
22 } as AuthUser,
23 tokenDebounceTime: _toNumber(import.meta.env.VITE_TOKEN_DEBOUNCE_TIME) // seconds
24 }),
25 actions: {
26 setLogged(token: string) {
27 // NOTE: decodeJwt is intentionally used without signature verification.
28 // The token was just issued by our own backend (POST /auth/token or /auth/refresh)
29 // over HTTPS. We read `sub` and `scopes` solely to populate the UI state.
30 // All cryptographic validation is enforced server-side on every API call.
31 // This is by design and not a security vulnerability. // noqa: CWE-347
32 const jwtPayload = jose.decodeJwt<JWTPayload>(token)
33 const scopes = jwtPayload.scopes
34
35 this.user = {
36 access_token: token,
37 username: jwtPayload.sub || "",
38 email: "",
39 role: jwtRoleToUserRole(scopes)
40 }
41 },
42 setToken(token: string) {
43 this.user.access_token = token
44 },
45 setLogout() {
46 this.user = {
47 access_token: "",
48 username: "",
49 email: "",
50 role: AuthUserRole.Unknown
51 }
52
53 removePersistentSessionKey()
54 },
55 async login(payload: LoginPayload) {
56 try {
57 const response = await Api.auth.login(payload)
58
59 // If 2FA is required, don't set the temp token as logged-in state
60 if (response.data.requires_2fa) {
61 return response.data
62 }
63
64 if (response.data.access_token) {
65 this.setLogged(response.data.access_token)
66 this.getEmail()
67 }
68
69 return response.data
70 } catch (err) {
71 const error = err as ApiError
72 throw error.response?.data
73 }
74 },
75 async verify2fa(payload: TOTPValidateRequest) {
76 try {
77 const response = await Api.totp.validate(payload)
78
79 if (response.data.access_token) {
80 this.setLogged(response.data.access_token)
81 this.getEmail()
82 }
83
84 return response.data
85 } catch (err) {
86 const error = err as ApiError
87 throw error.response?.data
88 }
89 },
90 getEmail() {
91 Api.users.getUsers().then(res => {
92 if (res.data.users) {
93 const user = res.data.users.find(o => o.username === this.userName)
94 this.user.email = user?.email || ""
95 }
96 })
97 },
98 refreshToken() {
99 return new Promise((resolve, reject) => {
100 Api.auth
101 .refresh()
102 .then(res => {
103 if (res.data.access_token) {
104 this.setToken(res.data.access_token)
105 resolve(res.data)
106 } else {
107 reject(res.data)
108 }
109 })
110 .catch(err => {
111 reject(err.response?.data)
112 })
113 })
114 }
115 },
116 getters: {
117 isLogged(state): boolean {
118 return !!state.user?.access_token
119 },
120 userToken(state): string | null {
121 return state.user?.access_token || null
122 },
123 userName(state): string {
124 return state.user?.username
125 },
126 userEmail(state): string {
127 return state.user?.email
128 },
129 userRole(state): AuthUserRole {
130 return state.user?.role
131 },
132 userRoleName(state): string {
133 return AuthUserRole[(state.user?.role ?? 0) as number] ?? ""
134 },
135 userPic(): string {
136 const initial = getNameInitials(this.userName)
137
138 return getAvatar({ seed: initial, text: initial, size: 64 })
139 },
140 isAdmin(): boolean {
141 return _toLower(this.userRoleName) === "admin"
142 },
143 isRoleGranted() {
144 return (roles?: RouteMetaAuthRole | RouteMetaAuthRole[]) => {
145 if (!roles) {
146 return true
147 }
148 if (!this.userRole) {
149 return false
150 }
151
152 const arrRoles: RouteMetaAuthRole[] = _castArray(roles)
153
154 if (arrRoles.includes(RouteRole.All)) {
155 return true
156 }
157
158 return arrRoles.includes(this.userRole)
159 }
160 }
161 },
162 persist: {
163 storage: piniaStorage({ session: true }),
164 pick: ["user"]
165 }
166 })
167
168 if (import.meta.hot) {
169 import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot))
170 }