mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
docs: Rework Angular tutorial with correct auth methods, new best practices, and more (#42151)
* Changes * Draft * Restructure example to correct location * Update apps/docs/content/guides/getting-started/tutorials/with-angular.mdx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update examples/user-management/angular-user-management/angular.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Revert CSS * Prettier * Update dependencies * Update examples/user-management/angular-user-management/src/styles.css Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
5f4fb17a18
commit
23fb86f9a6
38 files changed
+15521
-983
No files matched your search
@@ -21,11 +21,11 @@ Start with building the Angular app from scratch.
|
||||
|
||||
### Initialize an Angular app
|
||||
|
||||
You can use the [Angular CLI](https://angular.io/cli) to initialize
|
||||
an app called `supabase-angular`. The command sets some defaults, that you change to suit your needs:
|
||||
You can use the [Angular CLI](https://angular.io/cli) to initialize an app called `supabase-angular`.
|
||||
The command sets some defaults, that you change to suit your needs:
|
||||
|
||||
```bash
|
||||
npx ng new supabase-angular --routing false --style css --standalone false --zoneless true --ssr false
|
||||
npx ng new supabase-angular --routing false --style css --standalone false --ssr false
|
||||
cd supabase-angular
|
||||
```
|
||||
|
||||
@@ -35,105 +35,26 @@ Then, install the only additional dependency: [supabase-js](https://github.com/s
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
Finally, save the environment variables in the `src/environments/environment.ts` file.
|
||||
Finally, save the environment variables in a new `src/environments/environment.ts` file.
|
||||
You need to create the `src/environments` directory first.
|
||||
All you need are the API URL and the key that you copied [earlier](#get-api-details).
|
||||
The application exposes these variables in the browser, and that's fine as you have [Row Level Security](/docs/guides/auth#row-level-security) enabled on the Database.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/environments/environment.ts
|
||||
export const environment = {
|
||||
production: false,
|
||||
supabaseUrl: 'YOUR_SUPABASE_URL',
|
||||
supabaseKey: 'YOUR_SUPABASE_KEY',
|
||||
}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/environments/environment.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/environments/environment.ts"
|
||||
/>
|
||||
|
||||
Now you have the API credentials in place, create a `SupabaseService` with `ng g s supabase` and add the following code to initialize the Supabase client and implement functions to communicate with the Supabase API.
|
||||
|
||||
<$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/supabase.service.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/supabase.service.ts"
|
||||
/>
|
||||
|
||||
```ts name=src/app/supabase.service.ts
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
AuthChangeEvent,
|
||||
AuthSession,
|
||||
createClient,
|
||||
Session,
|
||||
SupabaseClient,
|
||||
User,
|
||||
} from '@supabase/supabase-js'
|
||||
import { environment } from '../environments/environment'
|
||||
|
||||
export interface Profile {
|
||||
id?: string
|
||||
username: string
|
||||
website: string
|
||||
avatar_url: string
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SupabaseService {
|
||||
private supabase: SupabaseClient
|
||||
_session: AuthSession | null = null
|
||||
|
||||
constructor() {
|
||||
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)
|
||||
}
|
||||
|
||||
get session() {
|
||||
this.supabase.auth.getSession().then(({ data }) => {
|
||||
this._session = data.session
|
||||
})
|
||||
return this._session
|
||||
}
|
||||
|
||||
profile(user: User) {
|
||||
return this.supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
}
|
||||
|
||||
authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {
|
||||
return this.supabase.auth.onAuthStateChange(callback)
|
||||
}
|
||||
|
||||
signIn(email: string) {
|
||||
return this.supabase.auth.signInWithOtp({ email })
|
||||
}
|
||||
|
||||
signOut() {
|
||||
return this.supabase.auth.signOut()
|
||||
}
|
||||
|
||||
updateProfile(profile: Profile) {
|
||||
const update = {
|
||||
...profile,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
return this.supabase.from('profiles').upsert(update)
|
||||
}
|
||||
|
||||
downLoadImage(path: string) {
|
||||
return this.supabase.storage.from('avatars').download(path)
|
||||
}
|
||||
|
||||
uploadAvatar(filePath: string, file: File) {
|
||||
return this.supabase.storage.from('avatars').upload(filePath, file)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Optionally, update `src/styles.css` [with the following styles](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/angular-user-management/src/styles.css) to style the app.
|
||||
Optionally, update `src/styles.css` to style the app. You can find the full contents of this file [in the example repository](https://github.com/supabase/supabase/tree/master/examples/user-management/angular-user-management/src/styles.css).
|
||||
|
||||
### Set up a login component
|
||||
|
||||
@@ -143,75 +64,17 @@ Create an `AuthComponent` with the `ng g c auth` Angular CLI command and add the
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/auth/auth.ts
|
||||
import { Component } from '@angular/core'
|
||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/auth/auth.component.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/auth/auth.component.ts"
|
||||
/>
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth',
|
||||
templateUrl: './auth.html',
|
||||
styleUrls: ['./auth.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AuthComponent {
|
||||
signInForm!: FormGroup
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
loading = false
|
||||
ngOnInit() {
|
||||
this.signInForm = this.formBuilder.group({
|
||||
email: '',
|
||||
})
|
||||
}
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
try {
|
||||
this.loading = true
|
||||
const email = this.signInForm.value.email as string
|
||||
const { error } = await this.supabase.signIn(email)
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.signInForm.reset()
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/auth/auth.html
|
||||
<div class="row flex-center flex">
|
||||
<div class="col-6 form-widget" aria-live="polite">
|
||||
<h1 class="header">Supabase + Angular</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<form [formGroup]="signInForm" (ngSubmit)="onSubmit()" class="form-widget">
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
formControlName="email"
|
||||
class="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button block" [disabled]="loading">
|
||||
{{ loading ? "Loading" : "Send magic link" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/auth/auth.component.html"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/auth/auth.component.html"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -222,138 +85,42 @@ Create an `AccountComponent` with the `ng g c account` Angular CLI command and a
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/account/account.ts
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||
import { AuthSession } from '@supabase/supabase-js'
|
||||
import { Profile, SupabaseService } from '../supabase.service'
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/account/account.component.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/account/account.component.ts"
|
||||
/>
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.html',
|
||||
styleUrls: ['./account.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
loading = false
|
||||
profile!: Profile
|
||||
updateProfileForm!: FormGroup
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/account/account.component.html"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/account/account.component.html"
|
||||
/>
|
||||
|
||||
get avatarUrl() {
|
||||
return this.updateProfileForm.value.avatar_url as string
|
||||
}
|
||||
async updateAvatar(event: string): Promise<void> {
|
||||
this.updateProfileForm.patchValue({
|
||||
avatar_url: event,
|
||||
})
|
||||
await this.updateProfile()
|
||||
}
|
||||
</$CodeTabs>
|
||||
|
||||
@Input()
|
||||
session!: AuthSession
|
||||
## Profile photos
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
this.updateProfileForm = this.formBuilder.group({
|
||||
username: '',
|
||||
website: '',
|
||||
avatar_url: '',
|
||||
})
|
||||
}
|
||||
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos.
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.getProfile()
|
||||
### Create an upload widget
|
||||
|
||||
const { username, website, avatar_url } = this.profile
|
||||
this.updateProfileForm.patchValue({
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
})
|
||||
}
|
||||
Create an avatar for the user so that they can upload a profile photo.
|
||||
Create an `AvatarComponent` with `ng g c avatar` Angular CLI command and add the following code.
|
||||
|
||||
async getProfile() {
|
||||
try {
|
||||
this.loading = true
|
||||
const { user } = this.session
|
||||
const { data: profile, error, status } = await this.supabase.profile(user)
|
||||
<$CodeTabs>
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
}
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/avatar/avatar.component.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/avatar/avatar.component.ts"
|
||||
/>
|
||||
|
||||
if (profile) {
|
||||
this.profile = profile
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async updateProfile(): Promise<void> {
|
||||
try {
|
||||
this.loading = true
|
||||
const { user } = this.session
|
||||
|
||||
const username = this.updateProfileForm.value.username as string
|
||||
const website = this.updateProfileForm.value.website as string
|
||||
const avatar_url = this.updateProfileForm.value.avatar_url as string
|
||||
|
||||
const { error } = await this.supabase.updateProfile({
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
})
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
await this.supabase.signOut()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/account/account.html
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"> </app-avatar>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" [value]="session.user.email" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input formControlName="username" id="username" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input formControlName="website" id="website" type="url" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="button primary block" [disabled]="loading">
|
||||
{{ loading ? "Loading ..." : "Update" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="button block" (click)="signOut()">Sign Out</button>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/avatar/avatar.component.html"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/avatar/avatar.component.html"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -363,65 +130,27 @@ Now you have all the components in place, update `AppComponent`:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/app.ts
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { SupabaseService } from './supabase.service'
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/app.component.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/app.component.ts"
|
||||
/>
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ['./app.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(private readonly supabase: SupabaseService) {}
|
||||
|
||||
title = 'angular-user-management'
|
||||
session: any
|
||||
|
||||
ngOnInit() {
|
||||
this.session = this.supabase.session
|
||||
this.supabase.authChanges((_, session) => (this.session = session))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/app.html
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<app-account *ngIf="session; else auth" [session]="session"></app-account>
|
||||
<ng-template #auth>
|
||||
<app-auth></app-auth>
|
||||
</ng-template>
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/app.component.html"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/app.component.html"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
You also need to change `app.module.ts` to include the `ReactiveFormsModule` from the `@angular/forms` package.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/app.module.ts
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
|
||||
import { AppComponent } from './app'
|
||||
import { AuthComponent } from './auth/auth'
|
||||
import { AccountComponent } from './account/account'
|
||||
import { ReactiveFormsModule } from '@angular/forms'
|
||||
import { AvatarComponent } from './avatar/avatar'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
|
||||
imports: [BrowserModule, ReactiveFormsModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
exports: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/app.module.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/app.module.ts"
|
||||
/>
|
||||
|
||||
Once that's done, run the application in a terminal:
|
||||
|
||||
@@ -433,152 +162,4 @@ Open the browser to [localhost:4200](http://localhost:4200) and you should see t
|
||||
|
||||

|
||||
|
||||
## Bonus: Profile photos
|
||||
|
||||
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos.
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Create an avatar for the user so that they can upload a profile photo.
|
||||
Create an `AvatarComponent` with `ng g c avatar` Angular CLI command and add the following code.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/avatar/avatar.ts
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-avatar',
|
||||
templateUrl: './avatar.html',
|
||||
styleUrls: ['./avatar.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AvatarComponent {
|
||||
_avatarUrl: SafeResourceUrl | undefined
|
||||
uploading = false
|
||||
|
||||
@Input()
|
||||
set avatarUrl(url: string | null) {
|
||||
if (url) {
|
||||
this.downloadImage(url)
|
||||
}
|
||||
}
|
||||
|
||||
@Output() upload = new EventEmitter<string>()
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private readonly dom: DomSanitizer
|
||||
) {}
|
||||
|
||||
async downloadImage(path: string) {
|
||||
try {
|
||||
const { data } = await this.supabase.downLoadImage(path)
|
||||
if (data instanceof Blob) {
|
||||
this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data))
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadAvatar(event: any) {
|
||||
try {
|
||||
this.uploading = true
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = event.target.files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
await this.supabase.uploadAvatar(filePath, file)
|
||||
this.upload.emit(filePath)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.uploading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/avatar/avatar.html
|
||||
<div>
|
||||
<img
|
||||
*ngIf="_avatarUrl"
|
||||
[src]="_avatarUrl"
|
||||
alt="Avatar"
|
||||
class="avatar image"
|
||||
style="height: 150px; width: 150px"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!_avatarUrl" class="avatar no-image" style="height: 150px; width: 150px"></div>
|
||||
<div style="width: 150px">
|
||||
<label class="button primary block" for="single">
|
||||
{{ uploading ? "Uploading ..." : "Upload" }}
|
||||
</label>
|
||||
<input
|
||||
style="visibility: hidden; position: absolute"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
(change)="uploadAvatar($event)"
|
||||
[disabled]="uploading"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget on top of the `AccountComponent` HTML template:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/app/account.html
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"></app-avatar>
|
||||
<!-- input fields -->
|
||||
</form>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
And add an `updateAvatar` function along with an `avatarUrl` getter to the `AccountComponent` typescript file:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/account.ts
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.html',
|
||||
styleUrls: ['./account.css'],
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
// ...
|
||||
get avatarUrl() {
|
||||
return this.updateProfileForm.value.avatar_url as string
|
||||
}
|
||||
|
||||
async updateAvatar(event: string): Promise<void> {
|
||||
this.updateProfileForm.patchValue({
|
||||
avatar_url: event,
|
||||
})
|
||||
await this.updateProfile()
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -1,107 +1,89 @@
|
||||
# AngularUserManagement
|
||||
# Supabase Angular User Management
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.5.
|
||||
This example demonstrates how to build a user management app with Angular and Supabase.
|
||||
|
||||
## Development server
|
||||
## Features
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||
- Magic link authentication (passwordless)
|
||||
- User profile management
|
||||
- Avatar upload with Supabase Storage
|
||||
|
||||
## Code scaffolding
|
||||
## Getting Started
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
### 1. Create a Supabase project
|
||||
|
||||
## Build
|
||||
Create a new project in the [Supabase Dashboard](https://supabase.com/dashboard).
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
### 2. Set up the database
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
## Build from scratch
|
||||
|
||||
### 1. Create new project
|
||||
|
||||
Sign up to Supabase - [https://supabase.com/dashboard](https://supabase.com/dashboard) and create a new project. Wait for your database to start.
|
||||
|
||||
### 2. Run "User Management" Quickstart
|
||||
|
||||
Once your database has started, head over to your project's `SQL Editor` and run the "User Management Starter" quickstart. On the `SQL editor` page, scroll down until you see `User Management Starter: Sets up a public Profiles table which you can access with your API`. Click that, then click `RUN` to execute that query and create a new `profiles` table. When that's finished, head over to the `Table Editor` and see your new `profiles` table.
|
||||
|
||||
### 3. Get the URL and Key
|
||||
|
||||
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
|
||||
|
||||
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security).
|
||||
|
||||

|
||||
|
||||
**_NOTE_**: The `service_role` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser.
|
||||
|
||||
### 4. Env vars
|
||||
|
||||
Edit the `src/environments/environment.ts` file and populate this file with your URL and Key.
|
||||
|
||||
### 5. Run the application
|
||||
|
||||
Run the application: `npm run start`. Open your browser to `https://localhost:4200/` and you are ready to go 🚀.
|
||||
|
||||
## Supabase details
|
||||
|
||||
### Postgres Row level security
|
||||
|
||||
This project uses very high-level Authorization using Postgres' Row Level Security.
|
||||
When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
|
||||
When a user logs in, they are issued a JWT with the role `authenticated` and their UUID.
|
||||
We can use these details to provide fine-grained control over what each user can and cannot do.
|
||||
|
||||
This is a trimmed-down schema, with the policies:
|
||||
Run the following SQL in your Supabase SQL Editor to create the `profiles` table:
|
||||
|
||||
```sql
|
||||
-- Create a table for Public Profiles
|
||||
-- Create a table for public profiles
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
id uuid references auth.users on delete cascade not null primary key,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
primary key (id),
|
||||
unique(username),
|
||||
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
-- Set up Row Level Security (RLS)
|
||||
alter table profiles enable row level security;
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( (select auth.uid()) = id );
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( (select auth.uid()) = id );
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
-- Set up Storage!
|
||||
|
||||
create policy "Public profiles are viewable by everyone." on profiles
|
||||
for select using (true);
|
||||
|
||||
create policy "Users can insert their own profile." on profiles
|
||||
for insert with check ((select auth.uid()) = id);
|
||||
|
||||
create policy "Users can update own profile." on profiles
|
||||
for update using ((select auth.uid()) = id);
|
||||
|
||||
-- Set up Storage
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
-- Set up access controls for storage
|
||||
create policy "Avatar images are publicly accessible." on storage.objects
|
||||
for select using (bucket_id = 'avatars');
|
||||
|
||||
create policy "Anyone can upload an avatar." on storage.objects
|
||||
for insert with check (bucket_id = 'avatars');
|
||||
```
|
||||
|
||||
## Authors
|
||||
### 3. Configure environment variables
|
||||
|
||||
- [Supabase](https://supabase.com)
|
||||
Update the `src/environments/environment.ts` file with your Supabase project URL and anon key:
|
||||
|
||||
Supabase is open source. We'd love for you to follow along and get involved at https://github.com/supabase/supabase
|
||||
```typescript
|
||||
export const environment = {
|
||||
production: false,
|
||||
supabaseUrl: 'YOUR_SUPABASE_URL',
|
||||
supabaseKey: 'YOUR_SUPABASE_ANON_KEY',
|
||||
}
|
||||
```
|
||||
|
||||
You can find these values in your Supabase project settings under API.
|
||||
|
||||
### 4. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 5. Run the development server
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Supabase Documentation](https://supabase.com/docs)
|
||||
- [Angular Documentation](https://angular.io/docs)
|
||||
- [Supabase Angular Tutorial](https://supabase.com/docs/guides/getting-started/tutorials/with-angular)
|
||||
@@ -3,36 +3,24 @@
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-user-management": {
|
||||
"supabase-angular": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"standalone": false
|
||||
}
|
||||
},
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/supabase-angular",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": ["src/styles.css"]
|
||||
"assets": [{ "glob": "**/*", "input": "public" }],
|
||||
"styles": ["src/styles.css"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -59,38 +47,28 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "angular-user-management:build:production"
|
||||
"buildTarget": "supabase-angular:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular-user-management:build:development"
|
||||
"buildTarget": "supabase-angular:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:karma",
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": ["src/styles.css"]
|
||||
"assets": ["{ \"glob\": \"**/*\", \"input\": \"public\" }"],
|
||||
"styles": ["src/styles.css"],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "angular-user-management",
|
||||
"name": "supabase-angular",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
@@ -8,40 +8,31 @@
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"prettier": {
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^20.1.2",
|
||||
"@angular/compiler": "^20.1.2",
|
||||
"@angular/core": "^20.1.2",
|
||||
"@angular/forms": "^20.1.2",
|
||||
"@angular/platform-browser": "^20.1.2",
|
||||
"@angular/router": "^20.1.2",
|
||||
"@supabase/supabase-js": "^2.52.0",
|
||||
"@angular/animations": "^21.1.1",
|
||||
"@angular/common": "^21.1.1",
|
||||
"@angular/compiler": "^21.1.1",
|
||||
"@angular/core": "^21.1.1",
|
||||
"@angular/forms": "^21.1.1",
|
||||
"@angular/platform-browser": "^21.1.1",
|
||||
"@angular/platform-browser-dynamic": "^21.1.1",
|
||||
"@supabase/supabase-js": "^2.93.1",
|
||||
"rxjs": "~7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "~0.15.1"
|
||||
"zone.js": "~0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^20.1.1",
|
||||
"@angular/cli": "^20.1.1",
|
||||
"@angular/compiler-cli": "^20.1.2",
|
||||
"@types/jasmine": "~5.1.8",
|
||||
"jasmine-core": "~5.9.0",
|
||||
"@angular-devkit/build-angular": "^21.1.1",
|
||||
"@angular/cli": "^21.1.1",
|
||||
"@angular/compiler-cli": "^21.1.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.5.0",
|
||||
"karma": "~6.4.4",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.1",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.8.3"
|
||||
"typescript": "~5.9.3"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
/* Account component styles */
|
||||
+3
-8
@@ -1,13 +1,8 @@
|
||||
<form
|
||||
[formGroup]="updateProfileForm"
|
||||
(ngSubmit)="updateProfile()"
|
||||
class="form-widget"
|
||||
>
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)">
|
||||
</app-avatar>
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"></app-avatar>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" [value]="session.user.email" disabled />
|
||||
<input id="email" type="text" [value]="user.email" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
+34
-39
@@ -1,31 +1,32 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { AuthSession } from '@supabase/supabase-js';
|
||||
import { Profile, SupabaseService } from '../supabase.service';
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||
import { User } from '@supabase/supabase-js'
|
||||
import { Profile, SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.html',
|
||||
styleUrls: ['./account.css'],
|
||||
templateUrl: './account.component.html',
|
||||
styleUrls: ['./account.component.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
loading = false;
|
||||
profile!: Profile;
|
||||
updateProfileForm!: FormGroup;
|
||||
loading = false
|
||||
profile!: Profile
|
||||
updateProfileForm!: FormGroup
|
||||
|
||||
get avatarUrl() {
|
||||
return this.updateProfileForm.value.avatar_url as string;
|
||||
return this.updateProfileForm.value.avatar_url as string
|
||||
}
|
||||
|
||||
async updateAvatar(event: string): Promise<void> {
|
||||
this.updateProfileForm.patchValue({
|
||||
avatar_url: event,
|
||||
});
|
||||
await this.updateProfile();
|
||||
})
|
||||
await this.updateProfile()
|
||||
}
|
||||
|
||||
@Input()
|
||||
session!: AuthSession;
|
||||
user!: User
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
@@ -35,72 +36,66 @@ export class AccountComponent implements OnInit {
|
||||
username: '',
|
||||
website: '',
|
||||
avatar_url: '',
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.getProfile();
|
||||
await this.getProfile()
|
||||
|
||||
const { username, website, avatar_url } = this.profile;
|
||||
const { username, website, avatar_url } = this.profile
|
||||
this.updateProfileForm.patchValue({
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async getProfile() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const { user } = this.session;
|
||||
const {
|
||||
data: profile,
|
||||
error,
|
||||
status,
|
||||
} = await this.supabase.profile(user);
|
||||
this.loading = true
|
||||
const { data: profile, error, status } = await this.supabase.profile(this.user)
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
this.profile = profile;
|
||||
this.profile = profile
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async updateProfile(): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
const { user } = this.session;
|
||||
this.loading = true
|
||||
|
||||
const username = this.updateProfileForm.value.username as string;
|
||||
const website = this.updateProfileForm.value.website as string;
|
||||
const avatar_url = this.updateProfileForm.value.avatar_url as string;
|
||||
const username = this.updateProfileForm.value.username as string
|
||||
const website = this.updateProfileForm.value.website as string
|
||||
const avatar_url = this.updateProfileForm.value.avatar_url as string
|
||||
|
||||
const { error } = await this.supabase.updateProfile({
|
||||
id: user.id,
|
||||
id: this.user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
});
|
||||
if (error) throw error;
|
||||
})
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
await this.supabase.signOut();
|
||||
await this.supabase.signOut()
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,22 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccountComponent } from './account';
|
||||
|
||||
describe('Account', () => {
|
||||
let component: AccountComponent;
|
||||
let fixture: ComponentFixture<AccountComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AccountComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AccountComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<app-account *ngIf="user" [user]="user"></app-account>
|
||||
<app-auth *ngIf="!user"></app-auth>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { User } from '@supabase/supabase-js'
|
||||
import { SupabaseService } from './supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(private readonly supabase: SupabaseService) {}
|
||||
|
||||
title = 'angular-user-management'
|
||||
user: User | null = null
|
||||
|
||||
async ngOnInit() {
|
||||
this.user = await this.supabase.getUser()
|
||||
this.supabase.authChanges(async () => {
|
||||
this.user = await this.supabase.getUser()
|
||||
})
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,6 +0,0 @@
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<app-account *ngIf="session; else auth" [session]="session"></app-account>
|
||||
<ng-template #auth>
|
||||
<app-auth></app-auth>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -1,22 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { ReactiveFormsModule } from '@angular/forms'
|
||||
|
||||
import { AppComponent } from './app';
|
||||
import { AuthComponent } from './auth/auth';
|
||||
import { AccountComponent } from './account/account';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { AvatarComponent } from './avatar/avatar';
|
||||
import { AppComponent } from './app.component'
|
||||
import { AuthComponent } from './auth/auth.component'
|
||||
import { AccountComponent } from './account/account.component'
|
||||
import { AvatarComponent } from './avatar/avatar.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
AuthComponent,
|
||||
AccountComponent,
|
||||
AvatarComponent,
|
||||
],
|
||||
declarations: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
|
||||
imports: [BrowserModule, ReactiveFormsModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
exports: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AppComponent],
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain(
|
||||
'Hello, supabase-angular'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SupabaseService } from './supabase.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ['./app.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(private readonly supabase: SupabaseService) {}
|
||||
|
||||
title = 'angular-user-management';
|
||||
session: any;
|
||||
|
||||
ngOnInit() {
|
||||
this.session = this.supabase.session;
|
||||
this.supabase.authChanges((_, session) => (this.session = session));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/* Auth component styles */
|
||||
File renamed without changes.
@@ -0,0 +1,41 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth',
|
||||
templateUrl: './auth.component.html',
|
||||
styleUrls: ['./auth.component.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AuthComponent {
|
||||
signInForm!: FormGroup
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
loading = false
|
||||
ngOnInit() {
|
||||
this.signInForm = this.formBuilder.group({
|
||||
email: '',
|
||||
})
|
||||
}
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
try {
|
||||
this.loading = true
|
||||
const email = this.signInForm.value.email as string
|
||||
const { error } = await this.supabase.signIn(email)
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.signInForm.reset()
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,22 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthComponent } from './auth';
|
||||
|
||||
describe('Auth', () => {
|
||||
let component: AuthComponent;
|
||||
let fixture: ComponentFixture<AuthComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AuthComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AuthComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { SupabaseService } from '../supabase.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth',
|
||||
templateUrl: './auth.html',
|
||||
styleUrls: ['./auth.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AuthComponent {
|
||||
signInForm!: FormGroup;
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
loading = false;
|
||||
ngOnInit() {
|
||||
this.signInForm = this.formBuilder.group({
|
||||
email: '',
|
||||
});
|
||||
}
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
const email = this.signInForm.value.email as string;
|
||||
const { error } = await this.supabase.signIn(email);
|
||||
if (error) throw error;
|
||||
alert('Check your email for the login link!');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
}
|
||||
} finally {
|
||||
this.signInForm.reset();
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/* Avatar component styles */
|
||||
+1
-5
@@ -7,11 +7,7 @@
|
||||
style="height: 150px; width: 150px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!_avatarUrl"
|
||||
class="avatar no-image"
|
||||
style="height: 150px; width: 150px"
|
||||
></div>
|
||||
<div *ngIf="!_avatarUrl" class="avatar no-image" style="height: 150px; width: 150px"></div>
|
||||
<div style="width: 150px">
|
||||
<label class="button primary block" for="single">
|
||||
{{ uploading ? "Uploading ..." : "Upload" }}
|
||||
+21
-23
@@ -1,25 +1,25 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser';
|
||||
import { SupabaseService } from '../supabase.service';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-avatar',
|
||||
templateUrl: './avatar.html',
|
||||
styleUrls: ['./avatar.css'],
|
||||
templateUrl: './avatar.component.html',
|
||||
styleUrls: ['./avatar.component.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AvatarComponent {
|
||||
_avatarUrl: SafeResourceUrl | undefined;
|
||||
uploading = false;
|
||||
_avatarUrl: SafeResourceUrl | undefined
|
||||
uploading = false
|
||||
|
||||
@Input()
|
||||
set avatarUrl(url: string | null) {
|
||||
if (url) {
|
||||
this.downloadImage(url);
|
||||
this.downloadImage(url)
|
||||
}
|
||||
}
|
||||
|
||||
@Output() upload = new EventEmitter<string>();
|
||||
@Output() upload = new EventEmitter<string>()
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
@@ -28,38 +28,36 @@ export class AvatarComponent {
|
||||
|
||||
async downloadImage(path: string) {
|
||||
try {
|
||||
const { data } = await this.supabase.downLoadImage(path);
|
||||
const { data } = await this.supabase.downLoadImage(path)
|
||||
if (data instanceof Blob) {
|
||||
this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(
|
||||
URL.createObjectURL(data)
|
||||
);
|
||||
this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data))
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error downloading image: ', error.message);
|
||||
console.error('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadAvatar(event: any) {
|
||||
try {
|
||||
this.uploading = true;
|
||||
this.uploading = true
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.');
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = event.target.files[0];
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const filePath = `${Math.random()}.${fileExt}`;
|
||||
const file = event.target.files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
await this.supabase.uploadAvatar(filePath, file);
|
||||
this.upload.emit(filePath);
|
||||
await this.supabase.uploadAvatar(filePath, file)
|
||||
this.upload.emit(filePath)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
this.uploading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,22 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AvatarComponent } from './avatar';
|
||||
|
||||
describe('Avatar', () => {
|
||||
let component: AvatarComponent;
|
||||
let fixture: ComponentFixture<AvatarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AvatarComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AvatarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,40 +1,36 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
AuthChangeEvent,
|
||||
AuthSession,
|
||||
createClient,
|
||||
Session,
|
||||
SupabaseClient,
|
||||
User,
|
||||
} from '@supabase/supabase-js';
|
||||
import { environment } from '../environments/environment';
|
||||
} from '@supabase/supabase-js'
|
||||
import { environment } from '../environments/environment'
|
||||
|
||||
export interface Profile {
|
||||
id?: string;
|
||||
username: string;
|
||||
website: string;
|
||||
avatar_url: string;
|
||||
id?: string
|
||||
username: string
|
||||
website: string
|
||||
avatar_url: string
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SupabaseService {
|
||||
private supabase: SupabaseClient;
|
||||
_session: AuthSession | null = null;
|
||||
private supabase: SupabaseClient
|
||||
|
||||
constructor() {
|
||||
this.supabase = createClient(
|
||||
environment.supabaseUrl,
|
||||
environment.supabaseKey
|
||||
);
|
||||
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)
|
||||
}
|
||||
|
||||
get session() {
|
||||
this.supabase.auth.getSession().then(({ data }) => {
|
||||
this._session = data.session;
|
||||
});
|
||||
return this._session;
|
||||
async getUser(): Promise<User | null> {
|
||||
const { data, error } = await this.supabase.auth.getUser()
|
||||
if (error) {
|
||||
return null
|
||||
}
|
||||
return data.user
|
||||
}
|
||||
|
||||
profile(user: User) {
|
||||
@@ -42,37 +38,35 @@ export class SupabaseService {
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
.single()
|
||||
}
|
||||
|
||||
authChanges(
|
||||
callback: (event: AuthChangeEvent, session: Session | null) => void
|
||||
) {
|
||||
return this.supabase.auth.onAuthStateChange(callback);
|
||||
authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {
|
||||
return this.supabase.auth.onAuthStateChange(callback)
|
||||
}
|
||||
|
||||
signIn(email: string) {
|
||||
return this.supabase.auth.signInWithOtp({ email });
|
||||
return this.supabase.auth.signInWithOtp({ email })
|
||||
}
|
||||
|
||||
signOut() {
|
||||
return this.supabase.auth.signOut();
|
||||
return this.supabase.auth.signOut()
|
||||
}
|
||||
|
||||
updateProfile(profile: Profile) {
|
||||
const update = {
|
||||
...profile,
|
||||
updated_at: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
return this.supabase.from('profiles').upsert(update);
|
||||
return this.supabase.from('profiles').upsert(update)
|
||||
}
|
||||
|
||||
downLoadImage(path: string) {
|
||||
return this.supabase.storage.from('avatars').download(path);
|
||||
return this.supabase.storage.from('avatars').download(path)
|
||||
}
|
||||
|
||||
uploadAvatar(filePath: string, file: File) {
|
||||
return this.supabase.storage.from('avatars').upload(filePath, file);
|
||||
return this.supabase.storage.from('avatars').upload(filePath, file)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
supabaseUrl: '',
|
||||
supabaseKey: '',
|
||||
};
|
||||
@@ -1,18 +1,5 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
supabaseUrl: '',
|
||||
supabaseKey: '',
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||
supabaseUrl: 'YOUR_SUPABASE_URL',
|
||||
supabaseKey: 'YOUR_SUPABASE_KEY',
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>SupabaseAngular</title>
|
||||
<title>Supabase Angular</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { platformBrowser } from '@angular/platform-browser';
|
||||
import { AppModule } from './app/app.module';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
|
||||
platformBrowser()
|
||||
import { AppModule } from './app/app.module'
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule, {
|
||||
ngZoneEventCoalescing: true,
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
.catch((err) => console.error(err))
|
||||
@@ -270,30 +270,18 @@ input[disabled] {
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.font-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.opacity-half {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
|
||||
button,
|
||||
.button {
|
||||
color: var(--custom-color);
|
||||
border: var(--custom-border);
|
||||
background-color: var(--custom-bg-color);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border-radius: var(--custom-border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--custom-color);
|
||||
border: var(--custom-border);
|
||||
background-color: var(--custom-bg-color);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border-radius: var(--custom-border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
button.primary,
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
@@ -10,25 +9,20 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
"module": "ES2022",
|
||||
"lib": ["ES2022", "dom"]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user