From a043d06feda3f9f664fc7d7839a624ca57af6a6f Mon Sep 17 00:00:00 2001 From: geromegrignon Date: Fri, 16 Jul 2021 02:41:50 +0200 Subject: [PATCH] feat: add Angular user management Quickstart add Angular Quickstart guide add Angular demo image reference the quickstart in the sidebar --- web/docs/guides/with-angular.mdx | 582 +++++++++++++++++++++++ web/sidebars.js | 1 + web/static/img/supabase-angular-demo.png | Bin 0 -> 22073 bytes 3 files changed, 583 insertions(+) create mode 100644 web/docs/guides/with-angular.mdx create mode 100644 web/static/img/supabase-angular-demo.png diff --git a/web/docs/guides/with-angular.mdx b/web/docs/guides/with-angular.mdx new file mode 100644 index 00000000000..c1fc84ef5bc --- /dev/null +++ b/web/docs/guides/with-angular.mdx @@ -0,0 +1,582 @@ +--- +id: with-angular +title: "Quickstart: Angular" +description: Learn how to use Supabase in your Angular App. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Intro + +This example provides the steps to build a simple user management app (from scratch!) using Supabase and Angular. It includes: + +- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data. +- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email). +- Supabase [Storage](/docs/guides/storage): users can upload a photo. +- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data. +- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables. + +By the end of this guide you'll have an app which allows users to login and update some basic profile details: + +![Supabase User Management example](/img/user-management-demo.png) + +Clicking this button the application will: + +- Launch and prepare the Postgres database in Supabase. +- Launch the app in Vercel. +- Fork the example into your own GitHub account. +- Prepare the deployed application with all the necessary environment variables. + +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). + +## Project set up + +Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase +and then creating a "schema" inside the database. + +### Create a project + +1. Go to [app.supabase.io](https://app.supabase.io). +1. Click on "New Project". +1. Enter your project details. +1. Wait for the new database to launch. + + +### Set up the database schema + +Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor, +or you can just copy/paste the SQL from below and run it yourself. + + + + + ```sh + 1. Go to the "SQL" section. + 2. Click "User Management Starter". + 4. Click "Run". + ``` + + + + + + + ```sql + -- Create a table for public "profiles" + create table profiles ( + id uuid references auth.users not null, + 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) + ); + + 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 ( auth.uid() = id ); + + create policy "Users can update own profile." + on profiles for update + using ( 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! + 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' ); + + ``` + + + + + +### Get the API Keys + +Now that you've created some database tables, you are ready to insert data using the auto-generated API. +We just need to get the URL and `anon` key from the API settings. + + + + + ```sh + 1. Go to the "Settings" section. + 2. Click "API" in the sidebar. + 3. Find your API URL in this page. + 4. Find your "anon" and "service_role" keys on this page. + ``` + + + + + + +## Building the App + +Let's start building the Angular app from scratch. + +### Initialize a Angular app + +We can use the [Angular CLI](https://angular.io/cli) to initialize +an app called `supabase-angular`: + +```bash +npx ng new supabase-angular --routing false --style css +cd supabase-angular +``` + +Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js) + +```bash +npm install @supabase/supabase-js +``` + +And finally we want to save the environment variables in the `environment.ts` file. +All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). +These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. + + +```ts title="environment.ts" +export const environment = { + production: false, + supabaseUrl: "YOUR_SUPABASE_URL", + supbaseKey: "YOUR_SUPABASE_KEY" +}; +``` + +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 {AuthChangeEvent, createClient, Session, SupabaseClient} from '@supabase/supabase-js'; +import {environment} from "../environments/environment"; + +export interface Profile { + username: string; + website: string; + avatar_url: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class SupabaseService { + private supabase: SupabaseClient; + + constructor() { + this.supabase = createClient(environment.supabaseUrl, environment.supbaseKey); + } + + get user() { + return this.supabase.auth.user(); + } + + get session() { + return this.supabase.auth.session(); + } + + get profile() { + return this.supabase + .from('profiles') + .select(`username, website, avatar_url`) + .eq('id', this.user?.id) + .single(); + } + + authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) { + return this.supabase.auth.onAuthStateChange(callback); + } + + signIn(email: string) { + return this.supabase.auth.signIn({email}); + } + + 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 + }); + } + + 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); + } +} + +``` + +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). + +### Set up a Login component + +Let's set up a 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"; + +@Component({ + selector: 'app-auth', + template: ` +
+
+

Supabase + Angular

+

Sign in via magic link with your email below

+
+ +
+
+ +
+
+
+ `, +}) +export class AuthComponent { + loading = false; + + constructor(private readonly supabase: SupabaseService) { } + + async handleLogin(input: string) { + try { + this.loading = true; + await this.supabase.signIn(input); + alert('Check your email for the login link!'); + } catch (error) { + alert(error.error_description || error.message) + } finally { + this.loading = false; + } + } +} +``` + +### 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. + +```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"; + +@Component({ + selector: 'app-account', + template: ` +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+ +
+
+ ` +}) +export class AccountComponent implements OnInit { + loading = false; + profile: Profile | undefined; + + @Input() session: Session | undefined; + + constructor(private readonly supabase: SupabaseService) { } + + ngOnInit() { + this.getProfile(); + } + + async getProfile() { + try { + this.loading = true; + let {data: profile, error, status} = await this.supabase.profile; + + if (error && status !== 406) { + throw error; + } + + if (profile) { + this.profile = profile; + } + } catch (error) { + alert(error.message) + } finally { + this.loading = false; + } + } + + async updateProfile(username: string, website: string, avatar_url: string = '') { + try { + this.loading = true; + await this.supabase.updateProfile({username, website, avatar_url}); + } catch (error) { + alert(error.message); + } finally { + this.loading = false; + } + } + + async signOut() { + await this.supabase.signOut(); + } +} +``` + + + +### 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"; + +@Component({ + selector: 'app-root', + template: ` +
+ + + + +
+ ` +}) +export class AppComponent implements OnInit { + session = this.supabase.session; + + constructor(private readonly supabase: SupabaseService) { } + + ngOnInit() { + this.supabase.authChanges((_, session) => this.session = session); + } +} +``` + +Once that's done, run this in a terminal window: + +```bash +npm run start +``` + +And then open the browser to [localhost:4200](http://localhost:4200) and you should see the completed app. + + +![Supabase Angular](/img/supabase-angular-demo.png) + +## 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 + +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"; + +@Component({ + selector: 'app-avatar', + template: ` +
+ Avatar
+
+
+ + +
+ `, +}) +export class AvatarComponent { + _avatarUrl: SafeResourceUrl | undefined; + uploading = false; + + @Input() + set avatarUrl(url: string | undefined) { + if (url) { + this.downloadImage(url); + } + }; + + @Output() upload = new EventEmitter(); + + constructor( + private readonly supabase: SupabaseService, + private readonly dom: DomSanitizer + ) { } + + async downloadImage(path: string) { + try { + const {data} = await this.supabase.downLoadImage(path); + this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data)); + } catch (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 fileName = `${Math.random()}.${fileExt}`; + const filePath = `${fileName}`; + + await this.supabase.uploadAvatar(filePath, file); + this.upload.emit(filePath); + } catch (error) { + alert(error.message); + } finally { + this.uploading = false; + } + } +} +``` + + +### 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: ` + + + + +` +``` + +## Next steps + +At this stage you have a fully functional application! + +- Got a question? [Ask here](https://github.com/supabase/supabase/discussions). +- Sign in: [app.supabase.io](https://app.supabase.io) diff --git a/web/sidebars.js b/web/sidebars.js index 7f35c1dfee4..b8ded06f3a2 100755 --- a/web/sidebars.js +++ b/web/sidebars.js @@ -88,6 +88,7 @@ module.exports = { 'guides/with-react', 'guides/with-svelte', 'guides/with-vue-3', + 'guides/with-angular', 'guides/self-hosting', ], }, diff --git a/web/static/img/supabase-angular-demo.png b/web/static/img/supabase-angular-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..7fbeaa64199b71fa220dd1cf9de9f110024ddfaf GIT binary patch literal 22073 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 KDtYnd!~X&#P6+b= literal 0 HcmV?d00001