メインコンテンツまでスキップ

クイックスタート:Angular

イントロ

この例では、SupabaseとAngularを使って、シンプルなユーザー管理アプリを(ゼロから)構築する手順を紹介します。内容は以下のとおりです。

  • Supabase Database:ユーザーデータを保存するためのPostgresデータベースです。
  • Supabase Auth:ユーザーはマジックリンクでサインインできます(パスワードは不要、メールのみ)。
  • Supabase Storage:ユーザーは写真をアップロードできます。
  • 行単位セキュリティー:データは保護されており、個人が自分のデータにしかアクセスできないようになっています。
  • インスタントAPI:データベースのテーブルを作成すると、APIが自動的に生成されます。

このガイドの最後には、ユーザーがログインして基本的なプロフィール情報を更新できるアプリが完成します。

Supabaseユーザー管理の例

GitHub

どこかで行き詰ったら、このリポジトリーを見てみましょう。

プロジェクトのセットアップ

ビルドを開始する前に、データベースとAPIをセットアップします。Supabaseで新しいプロジェクトを立ち上げ、データベース内に「スキーマ」を作成するだけです。

プロジェクトの作成

  1. app.supabase.comにアクセスします。
  2. 「New Project」をクリックします。
  3. プロジェクトの詳細を入力します。
  4. 新しいデータベースが起動するのを待ちます。

データベース・スキーマの設定

これからデータベース・スキーマを設定します。SQLエディターの「User Management Starter」クイック・スターターを使用するか、下記のSQLをコピー/ペーストして自分で実行できます。

1. 「SQL」セクションに移動します。
2. 「User Management Starter」をクリックします。
3. 「Run」をクリックします。

APIキーの取得

データベースのテーブルをいくつか作成したので、自動生成されたAPIを使ってデータを挿入する準備ができました。API設定からURLとanonキーを取得する必要があります。

1. 「Settings」セクションに移動します。
2. サイドバーの「API」をリックします。
3. そのページでAPI URLを探します。
4. 「anon」と「service_role」キーを探します。

アプリの構築

それでは、Angularアプリを一から作ってみましょう。

Angularアプリの初期化

Angular CLIを使って、supabase-angularというアプリを初期化します。

npx ng new supabase-angular --routing false --style css
cd supabase-angular

そして、唯一追加する必要がある依存関係のsupbase-jsをインストールしましょう。

npm install @supabase/supabase-js

最終的には、environment.tsファイルに環境変数を保存します。 必要なのはAPIのURLと先ほどコピーしたanonキーだけです。 これらの変数はブラウザー上で公開されますが、データベースでは行単位のセキュリティが有効になっているので、全く問題ありません。

environment.ts
export const environment = {
production: false,
supabaseUrl: "YOUR_SUPABASE_URL",
supabaseKey: "YOUR_SUPABASE_KEY"
};

API認証ができたので、Supabaseクライアントを初期化します。そのため、Supabase APIと通信するための関数を実装するために、ng g s supabaseSupabaseServiceを作成しましょう。

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.supabaseKey);
}

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);
}
}

また、オプションとして、CSSファイルのsrc/index.cssを更新して、アプリの外観を整えます。 こちらにあるファイルを参照してください。

ログイン・コンポーネントの設定

ログインとサインアップを管理するAngularコンポーネントを設定してみましょう。ここではマジック・リンクを使い、ユーザーはパスワードを使わずにメールでサインインできます。 Angular CLIコマンドのng g c authAuthComponentを作成します。

src/app/auth.component.ts
import { Component } from '@angular/core';
import {SupabaseService} from "./supabase.service";

@Component({
selector: 'app-auth',
template: `
<div class="row flex flex-center">
<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>
`,
})
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;
}
}
}

アカウントページ

ユーザーがサイン・インした後、プロフィールの詳細を編集したり、アカウントを管理したりできるようにします。 Angular CLIコマンドのng g c accountAccountComponentを作成します。

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: `
<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 block primary"
(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>
`
})
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();
}
}

ローンチ

すべてのコンポーネントがそろったところで、AppComponentを更新しましょう。

src/app/app.component.ts
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>
`
})
export class AppComponent implements OnInit {
session = this.supabase.session;

constructor(private readonly supabase: SupabaseService) { }

ngOnInit() {
this.supabase.authChanges((_, session) => this.session = session);
}
}

更新が完了したら、ターミナル・ウィンドウでこれを実行します。

npm run start

そして、ブラウザーでlocalhost:4200を開くと、完成したアプリを見ることができます。

Supabase Angular

おまけ:プロフィール写真

Supabaseのプロジェクトには、写真や動画などの大容量ファイルを管理するためのストレージが用意されています。

アップロード・ウィジェットの作成

ユーザーがプロフィール写真をアップロードできるように、ユーザーのアバターを作成しましょう。 Angular CLIコマンドng g c avatarAvatarComponentを作成します。

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: `
<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>
`,
})
export class AvatarComponent {
_avatarUrl: SafeResourceUrl | undefined;
uploading = false;

@Input()
set avatarUrl(url: string | undefined) {
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);
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;
}
}
}

新しいウィジェットの追加

そして、このウィジェットをAccountComponent htmlテンプレートの上に追加します。

src/app/account.component.ts
template: `
<app-avatar
[avatarUrl]="this.profile?.avatar_url"
(upload)="updateProfile(username.value, website.value, $event)">
</app-avatar>

<!-- input fields -->
`

次のステップ

この段階で、完全に機能するアプリケーションが完成しました。