Update the angular user management quickstart (#9509)

* Updae the angular user management quickstart

* Update repository references

* Update apps/reference/docs/guides/with-angular.mdx

Co-authored-by: dng <danny@supabase.io>

* Update apps/reference/docs/guides/with-angular.mdx

Co-authored-by: dng <danny@supabase.io>

* Update apps/reference/docs/guides/with-angular.mdx

Co-authored-by: dng <danny@supabase.io>

* Update apps/reference/docs/guides/with-angular.mdx

Co-authored-by: dng <danny@supabase.io>

Co-authored-by: dng <danny@supabase.io>
This commit is contained in:
Andrew Smithanddng authored and GitHub committed 2022-10-15 15:05:05 +02:00
1 parent 9e1f5fdfdd
commit 77660bebbd
1 file changed
+322 -207
+322 -207
View File
@@ -33,7 +33,7 @@ If you want to do it yourself, let's get started!
### GitHub
Whenever you get stuck at any point, take a look at [this repo](https://github.com/angular-supa/supabase-angular-user-management).
Whenever you get stuck at any point, take a look at [this repo](https://github.com/supabase/examples/tree/main/supabase-js-v2/user-management/angular-user-management).
## Project set up
@@ -180,281 +180,365 @@ export const environment = {
Now that we have the API credentials in place, let's create a **SupabaseService** with `ng g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API.
```ts title="src/app/supabase.service.ts"
import { Injectable } from '@angular/core'
import { Injectable } from '@angular/core';
import {
AuthChangeEvent,
AuthSession,
createClient,
Session,
SupabaseClient,
} from '@supabase/supabase-js'
import { environment } from '../environments/environment'
User,
} from '@supabase/supabase-js';
import { environment } from 'src/environments/environment';
import { Database } from 'src/schema';
export interface Profile {
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
private supabase: SupabaseClient;
_session: AuthSession | null = null;
constructor() {
this.supabase = createClient(
environment.supabaseUrl,
environment.supabaseKey
)
}
get user() {
return this.supabase.auth.user()
);
}
get session() {
return this.supabase.auth.session()
this.supabase.auth.getSession().then(({ data }) => {
this._session = data.session;
})
return this._session;
}
get profile() {
profile(user: User) {
return this.supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', this.user?.id)
.single()
.eq('id', user.id)
.single();
}
authChanges(
callback: (event: AuthChangeEvent, session: Session | null) => void
) {
return this.supabase.auth.onAuthStateChange(callback)
return this.supabase.auth.onAuthStateChange(callback);
}
signIn(email: string) {
return this.supabase.auth.signIn({ email })
return this.supabase.auth.signInWithOtp({ email });
}
signOut() {
return this.supabase.auth.signOut()
return this.supabase.auth.signOut();
}
updateProfile(profile: Profile) {
const update = {
...profile,
id: this.user?.id,
updated_at: new Date(),
}
};
return this.supabase.from('profiles').upsert(update, {
returning: 'minimal', // Don't return the value after inserting
})
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);
}
}
```
And one optional step is to update the CSS file `src/index.css` to make the app look nice.
You can find the full contents of this file [here](https://raw.githubusercontent.com/angular-supa/supabase-angular-user-management/main/src/styles.css).
Optionally, update [src/styles.css](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/angular-user-management/src/styles.css) to style the app.
### Set up a Login component
Let's set up an Angular component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
Create an **AuthComponent** with `ng g c auth` Angular CLI command.
```ts title="src/app/auth.component.ts"
import { Component } from '@angular/core'
import { SupabaseService } from './supabase.service'
```ts title="src/app/auth/auth.component.ts"
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { SupabaseService } from '../supabase.service';
@Component({
selector: 'app-auth',
template: `
<div class="row flex-center flex">
<form class="col-6 form-widget">
<h1 class="header">Supabase + Angular</h1>
<p class="description">Sign in via magic link with your email below</p>
<div>
<input
#input
class="inputField"
type="email"
placeholder="Your email"
/>
</div>
<div>
<button
type="submit"
(click)="handleLogin(input.value)"
class="button block"
[disabled]="loading"
>
{{ loading ? 'Loading' : 'Send magic link' }}
</button>
</div>
</form>
</div>
`,
templateUrl: './auth.component.html',
styleUrls: ['./auth.component.css'],
})
export class AuthComponent {
loading = false
export class AuthComponent implements OnInit {
loading = false;
constructor(private readonly supabase: SupabaseService) {}
signInForm = this.formBuilder.group({
email: '',
});
async handleLogin(input: string) {
constructor(
private readonly supabase: SupabaseService,
private readonly formBuilder: FormBuilder
) {}
ngOnInit(): void {}
async onSubmit(): Promise<void> {
try {
this.loading = true
await this.supabase.signIn(input)
alert('Check your email for the login link!')
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) {
alert(error.error_description || error.message)
if (error instanceof Error) {
alert(error.message);
}
} finally {
this.loading = false
this.signInForm.reset();
this.loading = false;
}
}
}
```
```html title="src/app/auth/auth.component.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
```
### Account page
After a user is signed in we can allow them to edit their profile details and manage their account.
Create an **AccountComponent** with `ng g c account` Angular CLI command.
Users also need a way to edit their profile details and manage their accounts after signing in.
Create an **AccountComponent** with the `ng g c account` Angular CLI command.
```ts title="src/app/account.component.ts"
import { Component, Input, OnInit } from '@angular/core'
import { Profile, SupabaseService } from './supabase.service'
import { Session } from '@supabase/supabase-js'
```ts title="src/app/account/account.component.ts"
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AuthSession } from '@supabase/supabase-js';
import { Profile, SupabaseService } from '../supabase.service';
@Component({
selector: 'app-account',
template: `
<div class="form-widget">
<div>
<label for="email">Email</label>
<input id="email" type="text" [value]="session?.user?.email" disabled />
</div>
<div>
<label for="username">Name</label>
<input
#username
id="username"
type="text"
[value]="profile?.username ?? ''"
/>
</div>
<div>
<label for="website">Website</label>
<input
#website
id="website"
type="url"
[value]="profile?.website ?? ''"
/>
</div>
<div>
<button
class="button primary block"
(click)="updateProfile(username.value, website.value)"
[disabled]="loading"
>
{{ loading ? 'Loading ...' : 'Update' }}
</button>
</div>
<div>
<button class="button block" (click)="signOut()">Sign Out</button>
</div>
</div>
`,
templateUrl: './account.component.html',
styleUrls: ['./account.component.css'],
})
export class AccountComponent implements OnInit {
loading = false
profile: Profile | undefined
loading = false;
profile!: Profile;
@Input() session: Session | undefined
@Input()
session!: AuthSession;
constructor(private readonly supabase: SupabaseService) {}
updateProfileForm = this.formBuilder.group({
username: '',
website: '',
avatar_url: '',
});
ngOnInit() {
this.getProfile()
constructor(
private readonly supabase: SupabaseService,
private formBuilder: FormBuilder
) {}
async ngOnInit(): Promise<void> {
await this.getProfile();
const { username, website, avatar_url } = this.profile;
this.updateProfileForm.patchValue({
username,
website,
avatar_url,
});
}
async getProfile() {
try {
this.loading = true
let { data: profile, error, status } = await this.supabase.profile
this.loading = true;
const { user } = this.session;
let { data: profile, error, status } = await this.supabase.profile(user);
if (error && status !== 406) {
throw error
throw error;
}
if (profile) {
this.profile = profile
this.profile = profile;
}
} catch (error) {
alert(error.message)
if (error instanceof Error) {
alert(error.message);
}
} finally {
this.loading = false
this.loading = false;
}
}
async updateProfile(
username: string,
website: string,
avatar_url: string = ''
) {
async updateProfile(): Promise<void> {
try {
this.loading = true
await this.supabase.updateProfile({ username, website, avatar_url })
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) {
alert(error.message)
if (error instanceof Error) {
alert(error.message);
}
} finally {
this.loading = false
this.loading = false;
}
}
async signOut() {
await this.supabase.signOut()
await this.supabase.signOut();
}
}
```
```html title="src/app/account/account.component.html"
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
<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>
```
### Launch!
Now that we have all the components in place, let's update **AppComponent**:
```ts title="src/app/app.component.ts"
import { Component, OnInit } from '@angular/core'
import { SupabaseService } from './supabase.service'
import { Component, OnInit } from '@angular/core';
import { SupabaseService } from './supabase.service';
@Component({
selector: 'app-root',
template: `
<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>
`,
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
session = this.supabase.session
title = 'angular-user-management';
session = this.supabase.session;
constructor(private readonly supabase: SupabaseService) {}
ngOnInit() {
this.supabase.authChanges((_, session) => (this.session = session))
this.supabase.authChanges((_, session) => (this.session = session));
}
}
```
```html title="src/app/app.component.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>
```
`app.module.ts` also needs to be modified to include the `ReactiveFormsModule` from the `@angular/forms` package.
```ts title="src/app/app.module.ts"
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { AuthComponent } from './auth/auth.component';
import { AccountComponent } from './account/account.component';
import { ReactiveFormsModule } from '@angular/forms';
import { AvatarComponent } from './avatar/avatar.component';
@NgModule({
declarations: [
AppComponent,
AuthComponent,
AccountComponent,
AvatarComponent,
],
imports: [BrowserModule, ReactiveFormsModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
```
Once that's done, run this in a terminal window:
```bash
@@ -474,110 +558,141 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
Let's 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.
```ts title="src/app/avatar.component.ts"
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { SupabaseService } from './supabase.service'
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
```ts title="src/app/avatar/avatar.component.ts"
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser';
import { SupabaseService } from '../supabase.service';
@Component({
selector: 'app-avatar',
template: `
<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>
`,
templateUrl: './avatar.component.html',
styleUrls: ['./avatar.component.css'],
})
export class AvatarComponent {
_avatarUrl: SafeResourceUrl | undefined
uploading = false
export class AvatarComponent implements OnInit {
_avatarUrl: SafeResourceUrl | undefined;
uploading = false;
@Input()
set avatarUrl(url: string | undefined) {
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,
private readonly dom: DomSanitizer
) {}
ngOnInit(): void {}
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)
)
);
}
} catch (error) {
console.error('Error downloading image: ', error.message)
if (error instanceof Error) {
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 fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`
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) {
alert(error.message)
if (error instanceof Error) {
alert(error.message);
}
} finally {
this.uploading = false
this.uploading = false;
}
}
}
```
```html title="src/app/avatar/avatar.component.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>
```
### Add the new widget
And then we can add the widget on top of the **AccountComponent** html template:
```ts title="src/app/account.component.ts"
template: `
<app-avatar
[avatarUrl]="this.profile?.avatar_url"
(upload)="updateProfile(username.value, website.value, $event)">
</app-avatar>
```html title="src/app/account.component.html"
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
<app-avatar
[avatarUrl]="this.avatarUrl"
(upload)="updateAvatar($event)">
</app-avatar>
<!-- input fields -->
</form>
```
<!-- input fields -->
`
And add an `updateAvatar` function along with an `avatarUrl` getter to the **AccountComponent** typescript file:
```ts title="src/app/account.component.ts"
@Component({
selector: 'app-account',
templateUrl: './account.component.html',
styleUrls: ['./account.component.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();
}
// ...
}
```
## Next steps