hsila commited on
Commit
c21ae5c
·
1 Parent(s): 6043a6a

feat: implement secure Supabase authentication system

Browse files

- Replace custom authentication with Supabase Auth for real security
- Update login form to use email instead of username
- Implement proper RLS policies with auth.uid() for data isolation
- Fix database constraint to include user_id for multi-user support
- Add comprehensive authentication flow with session management
- Update documentation to reflect new authentication setup
- Remove sensitive data exposure and improve overall security

Files changed (3) hide show
  1. README.md +44 -30
  2. src/components/LoginForm.astro +5 -5
  3. src/pages/index.astro +94 -34
README.md CHANGED
@@ -30,19 +30,10 @@ npm install
30
  Run these SQL queries in your Supabase SQL Editor:
31
 
32
  ```sql
33
- -- Users table for authentication
34
- CREATE TABLE users (
35
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
36
- username VARCHAR(50) UNIQUE NOT NULL,
37
- password_hash VARCHAR(255) NOT NULL,
38
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
39
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
40
- );
41
-
42
  -- Task completions table for tracking progress
43
  CREATE TABLE completed_tasks (
44
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
45
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
46
  task_id VARCHAR(50) NOT NULL,
47
  task_type VARCHAR(50) NOT NULL,
48
  category VARCHAR(20) NOT NULL,
@@ -52,21 +43,44 @@ CREATE TABLE completed_tasks (
52
 
53
  -- Indexes for better performance
54
  CREATE INDEX idx_completed_tasks_user_id ON completed_tasks(user_id);
55
- CREATE INDEX idx_users_username ON users(username);
56
  ```
57
 
58
- #### Create Sample Users
59
- Run these queries to create test users:
 
 
 
 
 
 
60
 
61
  ```sql
62
- -- Create test users (passwords are stored as plain text for testing - in production, use proper hashing)
63
- INSERT INTO users (username, password_hash)
64
- VALUES ('testuser', 'password123');
65
 
66
- -- Verify users were created
67
- SELECT * FROM users;
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  ```
69
 
 
 
 
 
 
 
70
 
71
  ### 3. Environment Configuration
72
 
@@ -106,9 +120,9 @@ SUPABASE_ANON_KEY=your_supabase_anon_key
106
  ## Usage
107
 
108
  ### Authentication
109
- - **Login**: Users must log in with valid credentials to access any content
110
- - **Session**: Sessions last for 1 week
111
- - **Logout**: Clears session and refreshes the page
112
 
113
  ### Task Management
114
  - **Browse**: Filter tasks by category, type, and completion status
@@ -164,20 +178,20 @@ astra/
164
  - `src/layouts/BaseLayout.astro` - Base layout component
165
 
166
  ### Authentication Flow
167
- 1. Page loads -> Check localStorage for valid session
168
  2. If no session -> Show login overlay, hide content
169
- 3. User submits form -> Authenticate with Supabase
170
- 4. If successful -> Store session, show content
171
- 5. Session persists for 1 week
172
 
173
  ## Security Notes
174
 
175
- - **Important**: This is a demo implementation
176
- - In production, implement proper password hashing
 
177
  - Use HTTPS in production
178
- - Consider implementing proper Supabase Auth instead of custom authentication
179
- - Add input validation and sanitization
180
- - Implement rate limiting for login attempts
181
 
182
  ## Troubleshooting
183
 
 
30
  Run these SQL queries in your Supabase SQL Editor:
31
 
32
  ```sql
 
 
 
 
 
 
 
 
 
33
  -- Task completions table for tracking progress
34
  CREATE TABLE completed_tasks (
35
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
36
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
37
  task_id VARCHAR(50) NOT NULL,
38
  task_type VARCHAR(50) NOT NULL,
39
  category VARCHAR(20) NOT NULL,
 
43
 
44
  -- Indexes for better performance
45
  CREATE INDEX idx_completed_tasks_user_id ON completed_tasks(user_id);
 
46
  ```
47
 
48
+ #### Enable Supabase Auth
49
+ Go to your Supabase project dashboard:
50
+ 1. **Authentication** -> **Settings** -> Enable email/password authentication
51
+ 2. **Authentication** -> **Email Template** -> Configure confirmation email template
52
+ 3. **Authentication** -> **Settings** -> Set your site URL for redirects
53
+
54
+ #### Set Up Row Level Security (RLS)
55
+ Enable RLS for proper security with Supabase Auth:
56
 
57
  ```sql
58
+ -- Enable RLS on `completed_tasks` table
59
+ ALTER TABLE completed_tasks ENABLE ROW LEVEL SECURITY;
 
60
 
61
+ -- Users can only see their own completions
62
+ CREATE POLICY "Users can view own completions" ON completed_tasks
63
+ FOR SELECT USING (auth.uid() = user_id);
64
+
65
+ -- Users can only insert their own completions
66
+ CREATE POLICY "Users can insert own completions" ON completed_tasks
67
+ FOR INSERT WITH CHECK (auth.uid() = user_id);
68
+
69
+ -- Users can only update their own completions
70
+ CREATE POLICY "Users can update own completions" ON completed_tasks
71
+ FOR UPDATE USING (auth.uid() = user_id);
72
+
73
+ -- Users can only delete their own completions
74
+ CREATE POLICY "Users can delete own completions" ON completed_tasks
75
+ FOR DELETE USING (auth.uid() = user_id);
76
  ```
77
 
78
+ #### Create Test Users
79
+ Use the Supabase Auth UI or API to create test users:
80
+ 1. Go to **Authentication** -> **Users** in your Supabase dashboard
81
+ 2. Click **Add user** and create test accounts
82
+ 3. Or use the signup functionality in your app
83
+
84
 
85
  ### 3. Environment Configuration
86
 
 
120
  ## Usage
121
 
122
  ### Authentication
123
+ - **Login**: Users must log in with email and password to access any content
124
+ - **Session**: Sessions are managed automatically by Supabase (persistent)
125
+ - **Logout**: Clears session and shows login screen
126
 
127
  ### Task Management
128
  - **Browse**: Filter tasks by category, type, and completion status
 
178
  - `src/layouts/BaseLayout.astro` - Base layout component
179
 
180
  ### Authentication Flow
181
+ 1. Page loads -> Check Supabase auth session
182
  2. If no session -> Show login overlay, hide content
183
+ 3. User submits form -> Authenticate with Supabase Auth
184
+ 4. If successful -> Supabase manages session, show content
185
+ 5. Session persists automatically (managed by Supabase)
186
 
187
  ## Security Notes
188
 
189
+ - **Important**: Now uses Supabase Auth for secure authentication
190
+ - Row Level Security (RLS) ensures users can only access their own data
191
+ - Supabase handles password hashing and session management securely
192
  - Use HTTPS in production
193
+ - RLS policies prevent unauthorized access to user data
194
+ - No sensitive data exposed in frontend code
 
195
 
196
  ## Troubleshooting
197
 
src/components/LoginForm.astro CHANGED
@@ -10,13 +10,13 @@ const { isVisible = true } = Astro.props
10
  <div class="login-modal">
11
  <div class="login-header">
12
  <h2>Login to Track Progress</h2>
13
- <p>Enter your username and password to track completed tasks</p>
14
  </div>
15
 
16
  <form id="login-form" class="login-form">
17
  <div class="form-field">
18
- <label for="username">Username</label>
19
- <input type="text" id="username" name="username" required autocomplete="username">
20
  </div>
21
 
22
  <div class="form-field">
@@ -30,7 +30,7 @@ const { isVisible = true } = Astro.props
30
  </form>
31
 
32
  <div id="login-error" class="login-error hidden">
33
- Invalid username or password
34
  </div>
35
 
36
  <div class="login-footer">
@@ -162,4 +162,4 @@ const { isVisible = true } = Astro.props
162
  color: #8b949e;
163
  font-size: 0.8rem;
164
  }
165
- </style>
 
10
  <div class="login-modal">
11
  <div class="login-header">
12
  <h2>Login to Track Progress</h2>
13
+ <p>Enter your email and password to track completed tasks</p>
14
  </div>
15
 
16
  <form id="login-form" class="login-form">
17
  <div class="form-field">
18
+ <label for="email">Email</label>
19
+ <input type="email" id="email" name="email" required autocomplete="email">
20
  </div>
21
 
22
  <div class="form-field">
 
30
  </form>
31
 
32
  <div id="login-error" class="login-error hidden">
33
+ Invalid email or password
34
  </div>
35
 
36
  <div class="login-footer">
 
162
  color: #8b949e;
163
  font-size: 0.8rem;
164
  }
165
+ </style>
src/pages/index.astro CHANGED
@@ -246,29 +246,89 @@ const initialPageInfo = totalItems === 0 ? 'Page 0 of 0' : `Page 1 of ${totalPag
246
  console.error('Missing database environment variables');
247
  }
248
 
249
- // Real authentication and task tracking functions
250
- async function authenticateUser(username, password) {
251
  if (!supabase) {
252
- console.error('Supabase not initialized');
253
  return null;
254
  }
255
 
256
  try {
257
- const { data: user, error } = await supabase
258
- .from('users')
259
- .select('*')
260
- .eq('username', username)
261
- .eq('password_hash', password)
262
- .single();
263
 
264
- if (error || !user) {
265
  console.error('Authentication error:', error);
266
  return null;
267
  }
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  return user;
270
  } catch (error) {
271
- console.error('Error authenticating user:', error);
272
  return null;
273
  }
274
  }
@@ -525,19 +585,18 @@ const initialPageInfo = totalItems === 0 ? 'Page 0 of 0' : `Page 1 of ${totalPag
525
  `;
526
  };
527
 
528
- // Authentication functions
529
- const checkAuthStatus = () => {
530
- const sessionData = localStorage.getItem('userSession');
531
- if (sessionData) {
532
- const { user, expiresAt } = JSON.parse(sessionData);
533
- if (expiresAt > Date.now()) {
534
  state.user = user;
535
  updateUserUI();
536
  loadCompletedTasks();
537
  return true;
538
- } else {
539
- localStorage.removeItem('userSession');
540
  }
 
 
541
  }
542
  return false;
543
  };
@@ -548,7 +607,7 @@ const initialPageInfo = totalItems === 0 ? 'Page 0 of 0' : `Page 1 of ${totalPag
548
  if (state.user) {
549
  // User is authenticated - show main content and user info
550
  userInfo.classList.remove('hidden');
551
- userName.textContent = `Logged in as ${state.user.username}`;
552
  mainContent.classList.add('authenticated');
553
  loginOverlay.classList.add('hidden');
554
  loginOverlay.classList.remove('visible');
@@ -575,12 +634,10 @@ const initialPageInfo = totalItems === 0 ? 'Page 0 of 0' : `Page 1 of ${totalPag
575
  }
576
  };
577
 
578
- const login = async (username, password) => {
579
  try {
580
- const user = await authenticateUser(username, password);
581
  if (user) {
582
- const expiresAt = Date.now() + (7 * 24 * 60 * 60 * 1000); // 1 week
583
- localStorage.setItem('userSession', JSON.stringify({ user, expiresAt }));
584
  state.user = user;
585
  loginError.classList.add('hidden');
586
  updateUserUI();
@@ -597,13 +654,16 @@ const initialPageInfo = totalItems === 0 ? 'Page 0 of 0' : `Page 1 of ${totalPag
597
  }
598
  };
599
 
600
- const logout = () => {
601
- localStorage.removeItem('userSession');
602
- state.user = null;
603
- state.completedTasks = [];
604
-
605
- // Refresh the page to reset all UI state
606
- window.location.reload();
 
 
 
607
  };
608
 
609
  const handleTaskToggle = async (event) => {
@@ -798,9 +858,9 @@ const initialPageInfo = totalItems === 0 ? 'Page 0 of 0' : `Page 1 of ${totalPag
798
  loginForm.addEventListener('submit', async (event) => {
799
  event.preventDefault();
800
  const formData = new FormData(loginForm);
801
- const username = formData.get('username');
802
  const password = formData.get('password');
803
- await login(username, password);
804
  });
805
 
806
  logoutBtn.addEventListener('click', logout);
@@ -838,7 +898,7 @@ const initialPageInfo = totalItems === 0 ? 'Page 0 of 0' : `Page 1 of ${totalPag
838
 
839
  // Initialize
840
  const initializeApp = async () => {
841
- const isAuthenticated = checkAuthStatus();
842
  updateTaskOptions();
843
 
844
  // Test SuperBase connection if user is authenticated
 
246
  console.error('Missing database environment variables');
247
  }
248
 
249
+ // Real authentication and task tracking functions using Supabase Auth
250
+ async function signInUser(email, password) {
251
  if (!supabase) {
252
+ console.error('Database not initialized');
253
  return null;
254
  }
255
 
256
  try {
257
+ const { data, error } = await supabase.auth.signInWithPassword({
258
+ email: email,
259
+ password: password
260
+ });
 
 
261
 
262
+ if (error) {
263
  console.error('Authentication error:', error);
264
  return null;
265
  }
266
 
267
+ return data.user;
268
+ } catch (error) {
269
+ console.error('Error signing in user:', error);
270
+ return null;
271
+ }
272
+ }
273
+
274
+ async function signUpUser(email, password) {
275
+ if (!supabase) {
276
+ console.error('Database not initialized');
277
+ return null;
278
+ }
279
+
280
+ try {
281
+ const { data, error } = await supabase.auth.signUp({
282
+ email: email,
283
+ password: password
284
+ });
285
+
286
+ if (error) {
287
+ console.error('Sign up error:', error);
288
+ return null;
289
+ }
290
+
291
+ return data.user;
292
+ } catch (error) {
293
+ console.error('Error signing up user:', error);
294
+ return null;
295
+ }
296
+ }
297
+
298
+ async function signOutUser() {
299
+ if (!supabase) {
300
+ console.error('Database not initialized');
301
+ return false;
302
+ }
303
+
304
+ try {
305
+ const { error } = await supabase.auth.signOut();
306
+ if (error) {
307
+ console.error('Sign out error:', error);
308
+ return false;
309
+ }
310
+ return true;
311
+ } catch (error) {
312
+ console.error('Error signing out user:', error);
313
+ return false;
314
+ }
315
+ }
316
+
317
+ async function getCurrentUser() {
318
+ if (!supabase) {
319
+ console.error('Database not initialized');
320
+ return null;
321
+ }
322
+
323
+ try {
324
+ const { data: { user }, error } = await supabase.auth.getUser();
325
+ if (error) {
326
+ console.error('Get current user error:', error);
327
+ return null;
328
+ }
329
  return user;
330
  } catch (error) {
331
+ console.error('Error getting current user:', error);
332
  return null;
333
  }
334
  }
 
585
  `;
586
  };
587
 
588
+ // Authentication functions using Supabase Auth
589
+ const checkAuthStatus = async () => {
590
+ try {
591
+ const user = await getCurrentUser();
592
+ if (user) {
 
593
  state.user = user;
594
  updateUserUI();
595
  loadCompletedTasks();
596
  return true;
 
 
597
  }
598
+ } catch (error) {
599
+ console.error('Error checking auth status:', error);
600
  }
601
  return false;
602
  };
 
607
  if (state.user) {
608
  // User is authenticated - show main content and user info
609
  userInfo.classList.remove('hidden');
610
+ userName.textContent = `Logged in as ${state.user.email}`;
611
  mainContent.classList.add('authenticated');
612
  loginOverlay.classList.add('hidden');
613
  loginOverlay.classList.remove('visible');
 
634
  }
635
  };
636
 
637
+ const login = async (email, password) => {
638
  try {
639
+ const user = await signInUser(email, password);
640
  if (user) {
 
 
641
  state.user = user;
642
  loginError.classList.add('hidden');
643
  updateUserUI();
 
654
  }
655
  };
656
 
657
+ const logout = async () => {
658
+ try {
659
+ await signOutUser();
660
+ state.user = null;
661
+ state.completedTasks = [];
662
+ updateUserUI();
663
+ applyState();
664
+ } catch (error) {
665
+ console.error('Logout error:', error);
666
+ }
667
  };
668
 
669
  const handleTaskToggle = async (event) => {
 
858
  loginForm.addEventListener('submit', async (event) => {
859
  event.preventDefault();
860
  const formData = new FormData(loginForm);
861
+ const email = formData.get('email');
862
  const password = formData.get('password');
863
+ await login(email, password);
864
  });
865
 
866
  logoutBtn.addEventListener('click', logout);
 
898
 
899
  // Initialize
900
  const initializeApp = async () => {
901
+ const isAuthenticated = await checkAuthStatus();
902
  updateTaskOptions();
903
 
904
  // Test SuperBase connection if user is authenticated