概要
Firebase Authenticationを使用した認証機能と、Firestoreでのユーザー情報管理システムを完全実装する。
前提条件
タスク一覧
1. 認証サービスの実装
2. ユーザー管理サービスの実装
3. 認証ガードの実装
4. 認証関連コンポーネントの実装
5. 状態管理の実装
実装仕様
AuthService の完全実装
// src/app/core/auth/auth.service.ts
import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/compat/auth';
import { GoogleAuthProvider } from 'firebase/auth';
import { Observable, map, switchMap, of } from 'rxjs';
import { UserService } from '../services/user.service';
import { User } from '../models/user.interface';
@Injectable({
providedIn: 'root'
})
export class AuthService {
// 認証状態の監視
user$: Observable<User | null> = this.afAuth.authState.pipe(
switchMap(authUser => {
if (authUser) {
return this.userService.getUser(authUser.uid);
} else {
return of(null);
}
})
);
// 認証済みかどうかの確認
isAuthenticated$: Observable<boolean> = this.user$.pipe(
map(user => \!\!user)
);
constructor(
private afAuth: AngularFireAuth,
private userService: UserService,
private router: Router,
private snackBar: MatSnackBar
) {}
/**
* Google サインイン
*/
async signInWithGoogle(): Promise<void> {
try {
const provider = new GoogleAuthProvider();
const result = await this.afAuth.signInWithPopup(provider);
if (result.user?.email && this.validateKeioEmail(result.user.email)) {
// @keio.jp ドメインの場合、ユーザー情報を作成/更新
await this.userService.createOrUpdateUser({
uid: result.user.uid,
email: result.user.email,
displayName: result.user.displayName || '',
createdAt: new Date(),
updatedAt: new Date()
});
this.snackBar.open('ログインしました', '閉じる', { duration: 3000 });
this.router.navigate(['/dashboard']);
} else {
// @keio.jp 以外の場合はサインアウト
await this.signOut();
throw new Error('慶應義塾大学のメールアドレス (@keio.jp) でログインしてください');
}
} catch (error) {
console.error('Login error:', error);
this.snackBar.open(
error instanceof Error ? error.message : 'ログインに失敗しました',
'閉じる',
{ duration: 5000 }
);
}
}
/**
* サインアウト
*/
async signOut(): Promise<void> {
try {
await this.afAuth.signOut();
this.snackBar.open('ログアウトしました', '閉じる', { duration: 3000 });
this.router.navigate(['/login']);
} catch (error) {
console.error('Logout error:', error);
this.snackBar.open('ログアウトに失敗しました', '閉じる', { duration: 3000 });
}
}
/**
* 現在のユーザーを取得
*/
getCurrentUser(): Observable<User | null> {
return this.user$;
}
/**
* @keio.jpドメインの検証
*/
private validateKeioEmail(email: string): boolean {
return /^[a-zA-Z0-9._%+-]+@keio\.jp$/.test(email);
}
}
UserService の実装
// src/app/core/services/user.service.ts
import { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/compat/firestore';
import { Observable } from 'rxjs';
import { User } from '../models/user.interface';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(private firestore: AngularFirestore) {}
/**
* ユーザー情報を取得
*/
getUser(uid: string): Observable<User | undefined> {
return this.firestore.collection('users').doc<User>(uid).valueChanges();
}
/**
* ユーザー情報を作成または更新
*/
async createOrUpdateUser(userData: Partial<User>): Promise<void> {
if (\!userData.uid) {
throw new Error('UID is required');
}
const userRef = this.firestore.collection('users').doc(userData.uid);
const userDoc = await userRef.get().toPromise();
if (userDoc?.exists) {
// 既存ユーザーの更新
await userRef.update({
...userData,
updatedAt: new Date()
});
} else {
// 新規ユーザーの作成
await userRef.set({
uid: userData.uid,
email: userData.email,
displayName: userData.displayName || '',
facultyType: '',
bio: '',
createdAt: new Date(),
updatedAt: new Date()
});
}
}
/**
* プロフィール更新
*/
async updateProfile(uid: string, profileData: Partial<User>): Promise<void> {
await this.firestore.collection('users').doc(uid).update({
...profileData,
updatedAt: new Date()
});
}
}
AuthGuard の実装
// src/app/core/guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, map, take } from 'rxjs';
import { AuthService } from '../auth/auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.authService.isAuthenticated$.pipe(
take(1),
map(isAuthenticated => {
if (isAuthenticated) {
return true;
} else {
// 未認証の場合はログインページにリダイレクト
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
})
);
}
}
ログインコンポーネント
// src/app/features/auth/login/login.component.ts
import { Component } from '@angular/core';
import { AuthService } from '../../../core/auth/auth.service';
@Component({
selector: 'app-login',
template: `
<div class="login-container">
<mat-card class="login-card">
<mat-card-header>
<mat-card-title>KCC Syllabus</mat-card-title>
<mat-card-subtitle>慶應義塾大学通信教育課程</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="login-description">
<p>科目情報共有プラットフォームにログイン</p>
<p class="login-note">
※ @keio.jp のメールアドレスでのみログイン可能です
</p>
</div>
</mat-card-content>
<mat-card-actions>
<button
mat-raised-button
color="primary"
(click)="signInWithGoogle()"
[disabled]="isLoading"
class="google-signin-button">
<mat-icon>account_circle</mat-icon>
Googleでログイン
</button>
</mat-card-actions>
</mat-card>
</div>
`,
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
isLoading = false;
constructor(private authService: AuthService) {}
async signInWithGoogle() {
this.isLoading = true;
try {
await this.authService.signInWithGoogle();
} finally {
this.isLoading = false;
}
}
}
ユーザーメニューコンポーネント
// src/app/shared/components/user-menu/user-menu.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { AuthService } from '../../../core/auth/auth.service';
import { User } from '../../../core/models/user.interface';
@Component({
selector: 'app-user-menu',
template: `
<ng-container *ngIf="user$ | async as user; else loginButton">
<button mat-button [matMenuTriggerFor]="userMenu" class="user-button">
<mat-icon>account_circle</mat-icon>
<span>{{ user.displayName || user.email }}</span>
<mat-icon>arrow_drop_down</mat-icon>
</button>
<mat-menu #userMenu="matMenu">
<button mat-menu-item routerLink="/profile">
<mat-icon>person</mat-icon>
<span>プロフィール</span>
</button>
<button mat-menu-item routerLink="/my-posts">
<mat-icon>post_add</mat-icon>
<span>自分の投稿</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="signOut()">
<mat-icon>logout</mat-icon>
<span>ログアウト</span>
</button>
</mat-menu>
</ng-container>
<ng-template #loginButton>
<button mat-button routerLink="/login" color="accent">
<mat-icon>login</mat-icon>
ログイン
</button>
</ng-template>
`
})
export class UserMenuComponent {
user$: Observable<User | null> = this.authService.getCurrentUser();
constructor(private authService: AuthService) {}
signOut() {
this.authService.signOut();
}
}
データモデル
User インターフェース
// src/app/core/models/user.interface.ts
export interface User {
uid: string;
email: string;
displayName?: string;
facultyType?: string;
bio?: string;
createdAt: Date;
updatedAt: Date;
}
ルーティング設定
// src/app/app-routing.module.ts
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuard]
},
{
path: 'profile',
component: ProfileComponent,
canActivate: [AuthGuard]
},
{ path: '**', redirectTo: '/dashboard' }
];
Acceptance Criteria
テスト項目
参照
次のステップ
フェーズ2の科目関連機能とコミュニティ機能の実装へ
概要
Firebase Authenticationを使用した認証機能と、Firestoreでのユーザー情報管理システムを完全実装する。
前提条件
タスク一覧
1. 認証サービスの実装
src/app/core/auth/auth.service.tsの完全実装2. ユーザー管理サービスの実装
src/app/core/services/user.service.tsの実装3. 認証ガードの実装
src/app/core/guards/auth.guard.tsの実装4. 認証関連コンポーネントの実装
5. 状態管理の実装
実装仕様
AuthService の完全実装
UserService の実装
AuthGuard の実装
ログインコンポーネント
ユーザーメニューコンポーネント
データモデル
User インターフェース
ルーティング設定
Acceptance Criteria
テスト項目
参照
次のステップ
フェーズ2の科目関連機能とコミュニティ機能の実装へ