diff --git a/README.md b/README.md index e090119..361b6cb 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,27 @@ ng serve Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. +## Supabase setup + +This frontend now uses Supabase for authentication. + +Update the following files with your Supabase project values before testing login or signup: + +```text +src/environments/environment.ts +src/environments/environment.development.ts +``` + +Set: + +```ts +supabaseUrl: 'https://your-project-ref.supabase.co' +supabaseAnonKey: 'your-supabase-anon-key' +passwordResetRedirectUrl: 'http://localhost:4200/login' +``` + +In your Supabase dashboard, enable Email authentication for the project and make sure the redirect URL matches your local frontend URL. + ## Code scaffolding Angular CLI includes powerful code scaffolding tools. To generate a new component, run: diff --git a/angular.json b/angular.json index 3cc2f9b..825e9ea 100644 --- a/angular.json +++ b/angular.json @@ -56,6 +56,12 @@ }, "configurations": { "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.ts" + } + ], "budgets": [ { "type": "initial", @@ -71,6 +77,12 @@ "outputHashing": "all" }, "development": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ], "optimization": false, "extractLicenses": false, "sourceMap": true diff --git a/package-lock.json b/package-lock.json index 018f9df..ed59241 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@angular/forms": "^21.1.0", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", + "@supabase/supabase-js": "^2.100.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "^0.16.0" @@ -5734,6 +5735,92 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.100.0.tgz", + "integrity": "sha512-pdT3ye3UVRN1Cg0wom6BmyY+XTtp5DiJaYnPi6j8ht5i8Lq8kfqxJMJz9GI9YDKk3w1nhGOPnh6Qz5qpyYm+1w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.100.0.tgz", + "integrity": "sha512-keLg79RPwP+uiwHuxFPTFgDRxPV46LM4j/swjyR2GKJgWniTVSsgiBHfbIBDcrQwehLepy09b/9QSHUywtKRWQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", + "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.100.0.tgz", + "integrity": "sha512-xYNvNbBJaXOGcrZ44wxwp5830uo1okMHGS8h8dm3u4f0xcZ39yzbryUsubTJW41MG2gbL/6U57cA4Pi6YMZ9pA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.100.0.tgz", + "integrity": "sha512-2AZs00zzEF0HuCKY8grz5eCYlwEfVi5HONLZFoNR6aDfxQivl8zdQYNjyFoqN2MZiVhQHD7u6XV/xHwM8mCEHw==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.0", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.100.0.tgz", + "integrity": "sha512-d4EeuK6RNIgYNA2MU9kj8lQrLm5AzZ+WwpWjGkii6SADQNIGTC/uiaTRu02XJ5AmFALQfo8fLl9xuCkO6Xw+iQ==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.100.0.tgz", + "integrity": "sha512-r0tlcukejJXJ1m/2eG/Ya5eYs4W8AC7oZfShpG3+SIo/eIU9uIt76ZeYI1SoUwUmcmzlAbgch+HDZDR/toVQPQ==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.100.0", + "@supabase/functions-js": "2.100.0", + "@supabase/postgrest-js": "2.100.0", + "@supabase/realtime-js": "2.100.0", + "@supabase/storage-js": "2.100.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -5925,7 +6012,6 @@ "version": "25.2.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -6033,7 +6119,6 @@ "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -8818,7 +8903,6 @@ "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -10229,6 +10313,15 @@ "node": ">=10.18" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -15477,7 +15570,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -16630,7 +16722,6 @@ "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index bba42f6..4c1fd57 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@angular/forms": "^21.1.0", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", + "@supabase/supabase-js": "^2.100.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "^0.16.0" diff --git a/src/app/components/accept-invitation/accept-invitation.spec.ts b/src/app/components/accept-invitation/accept-invitation.spec.ts index d0743c9..b168877 100644 --- a/src/app/components/accept-invitation/accept-invitation.spec.ts +++ b/src/app/components/accept-invitation/accept-invitation.spec.ts @@ -27,7 +27,7 @@ describe('AcceptInvitationComponent', () => { role: 'member' })); mockWorkspaceMemberService.acceptInvitation.and.returnValue(of({ workspaceId: '1', role: 'member' })); - mockAuthService.isLoggedIn.and.returnValue(true); + mockAuthService.isLoggedIn.and.returnValue(of(true)); await TestBed.configureTestingModule({ imports: [AcceptInvitationComponent, RouterTestingModule], @@ -65,7 +65,7 @@ describe('AcceptInvitationComponent', () => { it('should redirect to login when not authenticated', () => { spyOn(router, 'navigate'); - mockAuthService.isLoggedIn.and.returnValue(false); + mockAuthService.isLoggedIn.and.returnValue(of(false)); component.joinWorkspace(); diff --git a/src/app/components/accept-invitation/accept-invitation.ts b/src/app/components/accept-invitation/accept-invitation.ts index 51439e1..31d31ab 100644 --- a/src/app/components/accept-invitation/accept-invitation.ts +++ b/src/app/components/accept-invitation/accept-invitation.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { finalize } from 'rxjs'; import { InvitationValidation } from '../../models/workspace-member.model'; import { AuthService } from '../../services/auth'; import { WorkspaceMemberService } from '../../services/workspace-member.service'; @@ -37,23 +38,30 @@ export class AcceptInvitationComponent implements OnInit { } joinWorkspace(): void { - if (!this.authService.isLoggedIn()) { - this.router.navigate(['/login'], { queryParams: { redirectTo: `/invitations/${this.token}` } }); - return; - } - - this.isSubmitting = true; - this.workspaceMemberService.acceptInvitation(this.token).subscribe({ - next: response => { - this.success = `You joined the workspace as ${response.role}. Redirecting now.`; - setTimeout(() => { - this.router.navigate(['/workspaces', response.workspaceId, 'decisions']); - }, 900); - }, - error: (error: Error) => { - this.isSubmitting = false; - this.error = error.message; + this.authService.isLoggedIn().subscribe(isLoggedIn => { + if (!isLoggedIn) { + this.router.navigate(['/login'], { queryParams: { redirectTo: `/invitations/${this.token}` } }); + return; } + + this.isSubmitting = true; + this.workspaceMemberService.acceptInvitation(this.token).pipe( + finalize(() => { + if (!this.success) { + this.isSubmitting = false; + } + }) + ).subscribe({ + next: response => { + this.success = `You joined the workspace as ${response.role}. Redirecting now.`; + setTimeout(() => { + this.router.navigate(['/workspaces', response.workspaceId, 'decisions']); + }, 900); + }, + error: (error: Error) => { + this.error = error.message; + } + }); }); } } diff --git a/src/app/components/login/login.spec.ts b/src/app/components/login/login.spec.ts index 39daf48..6c63020 100644 --- a/src/app/components/login/login.spec.ts +++ b/src/app/components/login/login.spec.ts @@ -1,8 +1,8 @@ import { DOCUMENT } from '@angular/common'; -import { HttpErrorResponse } from '@angular/common/http'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { Session } from '@supabase/supabase-js'; import { of, throwError } from 'rxjs'; import { Login } from './login'; import { AuthService } from '../../services/auth'; @@ -15,7 +15,7 @@ describe('Login', () => { let document: Document; beforeEach(async () => { - mockAuthService = jasmine.createSpyObj('AuthService', ['login', 'signup']); + mockAuthService = jasmine.createSpyObj('AuthService', ['login', 'signup', 'resetPassword']); spyOn(localStorage, 'getItem').and.returnValue(null); spyOn(localStorage, 'setItem'); @@ -92,7 +92,21 @@ describe('Login', () => { it('shows a success message and redirects after login', fakeAsync(() => { spyOn(router, 'navigate'); - mockAuthService.login.and.returnValue(of({ token: 'token-1' })); + const session = { + access_token: 'access-token', + refresh_token: 'refresh-token', + expires_in: 3600, + expires_at: 9999999999, + token_type: 'bearer', + user: { + id: 'user-1', + app_metadata: {}, + user_metadata: {}, + aud: 'authenticated', + created_at: '2026-03-25T00:00:00.000Z' + } + } as Session; + mockAuthService.login.and.returnValue(of(session)); component.loginEmail = 'user@example.com'; component.loginPassword = 'secret'; @@ -107,7 +121,7 @@ describe('Login', () => { it('maps a 401 login response to invalid credentials', () => { mockAuthService.login.and.returnValue( - throwError(() => new HttpErrorResponse({ status: 401, error: 'invalid credentials' })) + throwError(() => new Error('Invalid login credentials')) ); component.loginEmail = 'user@example.com'; component.loginPassword = 'wrong'; @@ -148,7 +162,7 @@ describe('Login', () => { it('shows a duplicate email message when signup returns a conflict', () => { mockAuthService.signup.and.returnValue( - throwError(() => new HttpErrorResponse({ status: 409, error: 'already exists' })) + throwError(() => new Error('User already registered')) ); component.activeTab = 'register'; component.regEmail = 'user@example.com'; @@ -184,4 +198,27 @@ describe('Login', () => { expect(component.forgotError).toBe('Enter a valid email address'); expect(component.isForgotSubmitting).toBeFalse(); }); + + it('shows a success message after a password reset request', () => { + mockAuthService.resetPassword.and.returnValue(of(void 0)); + component.showForgotPassword(); + component.forgotEmail = 'user@example.com'; + + component.handleForgot(); + + expect(mockAuthService.resetPassword).toHaveBeenCalledWith('user@example.com'); + expect(component.showSuccess).toBeTrue(); + expect(component.successTitle).toBe('Check your email'); + }); + + it('shows an error when the password reset request fails', () => { + mockAuthService.resetPassword.and.returnValue(throwError(() => new Error('Reset failed'))); + component.showForgotPassword(); + component.forgotEmail = 'user@example.com'; + + component.handleForgot(); + + expect(component.forgotError).toBe('Unable to send reset link. Please try again.'); + expect(component.isForgotSubmitting).toBeFalse(); + }); }); diff --git a/src/app/components/login/login.ts b/src/app/components/login/login.ts index e3e2c38..28f3317 100644 --- a/src/app/components/login/login.ts +++ b/src/app/components/login/login.ts @@ -1,5 +1,4 @@ import { CommonModule, DOCUMENT } from '@angular/common'; -import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectorRef, Component, OnInit, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router, RouterModule } from '@angular/router'; @@ -102,9 +101,9 @@ export class Login implements OnInit { this.syncView(); setTimeout(() => this.router.navigate(['/dashboard']), 1200); }, - error: (err: HttpErrorResponse) => { - const backendMessage = typeof err.error === 'string' ? err.error.toLowerCase() : ''; - if (err.status === 401 || backendMessage.includes('invalid credentials')) { + error: (err: unknown) => { + const errorMessage = this.getErrorMessage(err); + if (errorMessage.includes('invalid login credentials') || errorMessage.includes('invalid credentials')) { this.loginError = 'Invalid credentials'; this.syncView(); return; @@ -152,13 +151,13 @@ export class Login implements OnInit { this.syncView(); }, 1500); }, - error: (err: HttpErrorResponse) => { + error: (err: unknown) => { this.activeTab = 'register'; this.showForgot = false; this.showSuccess = false; - const backendMessage = typeof err.error === 'string' ? err.error.toLowerCase() : ''; - if (err.status === 409 || backendMessage.includes('already exists')) { + const errorMessage = this.getErrorMessage(err); + if (errorMessage.includes('already registered') || errorMessage.includes('already exists')) { this.registerError = 'Email already exists'; this.syncView(); return; @@ -176,10 +175,23 @@ export class Login implements OnInit { return; } this.isForgotSubmitting = true; - setTimeout(() => { - this.isForgotSubmitting = false; - this.showSuccessMessage('Check your email', 'We sent a password reset link.'); - }, 1200); + + this.authService.resetPassword(this.forgotEmail.trim()).pipe( + timeout(8000), + finalize(() => { + this.isForgotSubmitting = false; + this.syncView(); + }) + ).subscribe({ + next: () => { + this.showSuccessMessage('Check your email', 'We sent a password reset link.'); + this.syncView(); + }, + error: () => { + this.forgotError = 'Unable to send reset link. Please try again.'; + this.syncView(); + } + }); } private showSuccessMessage(title: string, text: string): void { @@ -246,6 +258,14 @@ export class Login implements OnInit { return this.emailPattern.test(email.trim()); } + private getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message.toLowerCase(); + } + + return ''; + } + private syncView(): void { this.cdr.detectChanges(); } diff --git a/src/app/guards/auth-guard.spec.ts b/src/app/guards/auth-guard.spec.ts index 982427e..2c23ce6 100644 --- a/src/app/guards/auth-guard.spec.ts +++ b/src/app/guards/auth-guard.spec.ts @@ -1,5 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; +import { firstValueFrom, Observable, of } from 'rxjs'; import { authGuard } from './auth-guard'; import { AuthService } from '../services/auth'; @@ -19,21 +20,29 @@ describe('authGuard', () => { }); }); - it('returns true when the user is logged in', () => { - mockAuthService.isLoggedIn.and.returnValue(true); + it('returns true when the user is logged in', async () => { + mockAuthService.isLoggedIn.and.returnValue(of(true)); - const result = TestBed.runInInjectionContext(() => authGuard({} as never, {} as never)); + await TestBed.runInInjectionContext(async () => { + const result = await firstValueFrom( + authGuard({} as never, {} as never) as Observable + ); - expect(result).toBeTrue(); - expect(mockRouter.navigate).not.toHaveBeenCalled(); + expect(result).toBeTrue(); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); }); - it('redirects to login when the user is not logged in', () => { - mockAuthService.isLoggedIn.and.returnValue(false); + it('redirects to login when the user is not logged in', async () => { + mockAuthService.isLoggedIn.and.returnValue(of(false)); - const result = TestBed.runInInjectionContext(() => authGuard({} as never, {} as never)); + await TestBed.runInInjectionContext(async () => { + const result = await firstValueFrom( + authGuard({} as never, {} as never) as Observable + ); - expect(result).toBeFalse(); - expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']); + expect(result).toBeFalse(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']); + }); }); }); diff --git a/src/app/guards/auth-guard.ts b/src/app/guards/auth-guard.ts index c4e5078..23499d0 100644 --- a/src/app/guards/auth-guard.ts +++ b/src/app/guards/auth-guard.ts @@ -1,15 +1,20 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; +import { map } from 'rxjs'; import { AuthService } from '../services/auth'; export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); - if (authService.isLoggedIn()) { - return true; - } + return authService.isLoggedIn().pipe( + map(isLoggedIn => { + if (isLoggedIn) { + return true; + } - router.navigate(['/login']); - return false; + router.navigate(['/login']); + return false; + }) + ); }; diff --git a/src/app/services/auth.ts b/src/app/services/auth.ts index 2f58e01..b426a09 100644 --- a/src/app/services/auth.ts +++ b/src/app/services/auth.ts @@ -1,47 +1,86 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpResponse } from '@angular/common/http'; -import { map, Observable, tap } from 'rxjs'; - -interface AuthResponse { - token: string; -} +import { Injectable } from '@angular/core'; +import { Session } from '@supabase/supabase-js'; +import { from, map, Observable } from 'rxjs'; +import { supabase } from '../supabase'; +import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root', }) export class AuthService { - private http = inject(HttpClient); - private apiUrl = '/api'; - private tokenKey = 'sentinent_token'; - signup(email: string, password: string): Observable { - return this.http.post(`${this.apiUrl}/signup`, { email, password }, { - observe: 'response', - responseType: 'text' - }).pipe( - map((_res: HttpResponse) => undefined) + return from(supabase.auth.signUp({ + email, + password + })).pipe( + map(({ error }) => { + if (error) { + throw this.normalizeError(error.message); + } + return undefined; + }) ); } - login(email: string, password: string): Observable { - return this.http.post(`${this.apiUrl}/login`, { email, password }).pipe( - tap(res => this.setToken(res.token)) + login(email: string, password: string): Observable { + return from(supabase.auth.signInWithPassword({ + email, + password + })).pipe( + map(({ data, error }) => { + if (error) { + throw this.normalizeError(error.message); + } + if (!data.session) { + throw this.normalizeError('Sign in failed. Please try again.'); + } + return data.session; + }) ); } - logout(): void { - localStorage.removeItem(this.tokenKey); + resetPassword(email: string): Observable { + return from(supabase.auth.resetPasswordForEmail(email, { + redirectTo: environment.passwordResetRedirectUrl + })).pipe( + map(({ error }) => { + if (error) { + throw this.normalizeError(error.message); + } + return undefined; + }) + ); } - isLoggedIn(): boolean { - return !!this.getToken(); + logout(): Observable { + return from(supabase.auth.signOut()).pipe( + map(({ error }) => { + if (error) { + throw this.normalizeError(error.message); + } + return undefined; + }) + ); } - getToken(): string | null { - return localStorage.getItem(this.tokenKey); + isLoggedIn(): Observable { + return from(supabase.auth.getSession()).pipe( + map(({ data, error }) => { + if (error) { + return false; + } + return !!data.session; + }) + ); } - private setToken(token: string): void { - localStorage.setItem(this.tokenKey, token); + private normalizeError(message: string): Error { + const normalizedMessage = message.trim(); + + if (!normalizedMessage) { + return new Error('Authentication failed. Please try again.'); + } + + return new Error(normalizedMessage); } } diff --git a/src/app/supabase.ts b/src/app/supabase.ts new file mode 100644 index 0000000..6be8fe7 --- /dev/null +++ b/src/app/supabase.ts @@ -0,0 +1,10 @@ +import { createClient } from '@supabase/supabase-js'; +import { environment } from '../environments/environment'; + +export const supabase = createClient(environment.supabaseUrl, environment.supabaseAnonKey, { + auth: { + persistSession: true, + autoRefreshToken: true, + detectSessionInUrl: true + } +}); diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts new file mode 100644 index 0000000..d13a374 --- /dev/null +++ b/src/environments/environment.development.ts @@ -0,0 +1,6 @@ +export const environment = { + production: false, + supabaseUrl: 'https://your-project-ref.supabase.co', + supabaseAnonKey: 'your-supabase-anon-key', + passwordResetRedirectUrl: 'http://localhost:4200/login' +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..c8ef0c3 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,6 @@ +export const environment = { + production: true, + supabaseUrl: 'https://your-project-ref.supabase.co', + supabaseAnonKey: 'your-supabase-anon-key', + passwordResetRedirectUrl: 'http://localhost:4200/login' +};