|
1 | | -import create from "zustand"; |
| 1 | +import { create } from "zustand"; |
2 | 2 | import { persist } from "zustand/middleware"; |
| 3 | +import { devtools } from "zustand/middleware"; |
| 4 | + |
| 5 | +const ENCRYPTION_KEY_NAME = "stellar-hunts-ek"; |
| 6 | + |
| 7 | +async function getOrCreateEncryptionKey() { |
| 8 | + const stored = sessionStorage.getItem(ENCRYPTION_KEY_NAME); |
| 9 | + if (stored) { |
| 10 | + const raw = Uint8Array.from(atob(stored), (c) => c.charCodeAt(0)); |
| 11 | + return crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, true, [ |
| 12 | + "encrypt", |
| 13 | + "decrypt", |
| 14 | + ]); |
| 15 | + } |
| 16 | + const key = await crypto.subtle.generateKey( |
| 17 | + { name: "AES-GCM", length: 256 }, |
| 18 | + true, |
| 19 | + ["encrypt", "decrypt"] |
| 20 | + ); |
| 21 | + const exported = await crypto.subtle.exportKey("raw", key); |
| 22 | + sessionStorage.setItem( |
| 23 | + ENCRYPTION_KEY_NAME, |
| 24 | + btoa(String.fromCharCode(...new Uint8Array(exported))) |
| 25 | + ); |
| 26 | + return key; |
| 27 | +} |
| 28 | + |
| 29 | +async function encryptToken(token) { |
| 30 | + if (!token) return null; |
| 31 | + const key = await getOrCreateEncryptionKey(); |
| 32 | + const iv = crypto.getRandomValues(new Uint8Array(12)); |
| 33 | + const encoded = new TextEncoder().encode(token); |
| 34 | + const ciphertext = await crypto.subtle.encrypt( |
| 35 | + { name: "AES-GCM", iv }, |
| 36 | + key, |
| 37 | + encoded |
| 38 | + ); |
| 39 | + const combined = new Uint8Array(iv.length + ciphertext.byteLength); |
| 40 | + combined.set(iv); |
| 41 | + combined.set(new Uint8Array(ciphertext), iv.length); |
| 42 | + return btoa(String.fromCharCode(...combined)); |
| 43 | +} |
| 44 | + |
| 45 | +async function decryptToken(encrypted) { |
| 46 | + if (!encrypted) return null; |
| 47 | + try { |
| 48 | + const key = await getOrCreateEncryptionKey(); |
| 49 | + const raw = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0)); |
| 50 | + const iv = raw.slice(0, 12); |
| 51 | + const ciphertext = raw.slice(12); |
| 52 | + const decrypted = await crypto.subtle.decrypt( |
| 53 | + { name: "AES-GCM", iv }, |
| 54 | + key, |
| 55 | + ciphertext |
| 56 | + ); |
| 57 | + return new TextDecoder().decode(decrypted); |
| 58 | + } catch { |
| 59 | + return null; |
| 60 | + } |
| 61 | +} |
3 | 62 |
|
4 | 63 | const useAuthStore = create( |
5 | 64 | devtools( |
6 | 65 | persist( |
7 | | - (set) => ({ |
| 66 | + (set, get) => ({ |
8 | 67 | user: null, |
9 | 68 | token: null, |
10 | 69 | isAuthenticated: false, |
| 70 | + |
11 | 71 | register: async (userData) => { |
12 | 72 | try { |
13 | 73 | const response = await fetch("/api/register", { |
14 | 74 | method: "POST", |
15 | | - headers: { |
16 | | - "Content-Type": "application/json", |
17 | | - }, |
| 75 | + headers: { "Content-Type": "application/json" }, |
18 | 76 | body: JSON.stringify(userData), |
19 | 77 | }); |
20 | 78 |
|
21 | | - if (!response.ok) { |
22 | | - throw new Error("Registration failed"); |
23 | | - } |
| 79 | + if (!response.ok) throw new Error("Registration failed"); |
24 | 80 |
|
25 | 81 | const data = await response.json(); |
26 | 82 | const { user, token } = data; |
27 | | - set({ user, token, isAuthenticated: true }); |
28 | | - // Optionally, store the token in localStorage or set it in headers for future requests |
| 83 | + const encrypted = await encryptToken(token); |
| 84 | + set({ user, token: encrypted, isAuthenticated: true }); |
29 | 85 | } catch (error) { |
30 | 86 | console.error("Registration error:", error); |
31 | | - // Handle registration error (e.g., show notification) |
32 | 87 | } |
33 | 88 | }, |
| 89 | + |
34 | 90 | login: async (credentials) => { |
35 | 91 | try { |
36 | 92 | const response = await fetch("/api/login", { |
37 | 93 | method: "POST", |
38 | | - headers: { |
39 | | - "Content-Type": "application/json", |
40 | | - }, |
| 94 | + headers: { "Content-Type": "application/json" }, |
41 | 95 | body: JSON.stringify(credentials), |
42 | 96 | }); |
43 | 97 |
|
44 | | - if (!response.ok) { |
45 | | - throw new Error("Login failed"); |
46 | | - } |
| 98 | + if (!response.ok) throw new Error("Login failed"); |
47 | 99 |
|
48 | 100 | const data = await response.json(); |
49 | 101 | const { user, token } = data; |
50 | | - set({ user, token, isAuthenticated: true }); |
51 | | - // Optionally, store the token in localStorage or set it in headers for future requests |
| 102 | + const encrypted = await encryptToken(token); |
| 103 | + set({ user, token: encrypted, isAuthenticated: true }); |
52 | 104 | } catch (error) { |
53 | 105 | console.error("Login error:", error); |
54 | | - // Handle login error (e.g., show notification) |
55 | 106 | } |
56 | 107 | }, |
| 108 | + |
57 | 109 | logout: () => { |
58 | 110 | set({ user: null, token: null, isAuthenticated: false }); |
59 | | - // Optionally, remove the token from localStorage or headers |
60 | 111 | }, |
| 112 | + |
| 113 | + getDecryptedToken: async () => { |
| 114 | + const { token } = get(); |
| 115 | + return decryptToken(token); |
| 116 | + }, |
| 117 | + |
61 | 118 | fetchUser: async () => { |
62 | 119 | try { |
| 120 | + const decryptedToken = await get().getDecryptedToken(); |
63 | 121 | const response = await fetch("/api/user", { |
64 | 122 | method: "GET", |
65 | 123 | headers: { |
66 | 124 | "Content-Type": "application/json", |
67 | | - // Include authorization header with the token if required |
| 125 | + ...(decryptedToken |
| 126 | + ? { Authorization: `Bearer ${decryptedToken}` } |
| 127 | + : {}), |
68 | 128 | }, |
69 | 129 | }); |
70 | 130 |
|
71 | | - if (!response.ok) { |
72 | | - throw new Error("Fetching user failed"); |
73 | | - } |
| 131 | + if (!response.ok) throw new Error("Fetching user failed"); |
74 | 132 |
|
75 | 133 | const user = await response.json(); |
76 | 134 | set({ user, isAuthenticated: true }); |
77 | 135 | } catch (error) { |
78 | 136 | console.error("Fetching user error:", error); |
79 | | - // Handle error (e.g., redirect to login) |
80 | 137 | } |
81 | 138 | }, |
82 | 139 | }), |
83 | 140 | { |
84 | 141 | name: "auth-storage", |
85 | 142 | getStorage: () => localStorage, |
86 | | - }, |
| 143 | + } |
87 | 144 | ), |
88 | | - { name: "AuthStore" }, |
89 | | - ), |
| 145 | + { name: "AuthStore" } |
| 146 | + ) |
90 | 147 | ); |
91 | 148 |
|
92 | 149 | export default useAuthStore; |
0 commit comments