Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: manage gears #15

Merged
merged 7 commits into from
Oct 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"firestore": { "indexes": "firestore.indexes.json", "rules": "firestore.rules" },
"hosting": {
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"public": "web/build",
Expand Down
19 changes: 19 additions & 0 deletions firestore.indexes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"indexes": [
{
"collectionGroup": "gears",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "maker",
"order": "ASCENDING"
},
{
"fieldPath": "name",
"order": "ASCENDING"
}
]
}
],
"fieldOverrides": []
}
13 changes: 13 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
rules_version = '2';

service cloud.firestore {
match /databases/{database}/documents {
function authenticated() {
return request.auth != null;
}

match /gears/{gearId} {
allow list, write: if authenticated();
}
}
}
49 changes: 28 additions & 21 deletions web/src/adapters/infrastructure/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
import Firebase from '../firebase';

export interface IAuthenticationDriver {
sendPasswordResetEmail(email: string): Promise<Error | void>;
signIn(email: string, password: string): Promise<UserCredential | Error>;
signOut(): Promise<Error | void>;
sendPasswordResetEmail(email: string): Promise<void>;
signIn(email: string, password: string): Promise<UserCredential>;
signOut(): Promise<void>;
subscribe(nextOrObserver: (user: User | null) => void): Unsubscribe;
}

Expand All @@ -23,45 +23,52 @@ export default function authenticationDriver(): IAuthenticationDriver {
switch (error.code) {
case 'auth/email-already-in-use':
case 'auth/provider-already-linked':
throw new Error('このメールアドレスは既に登録されています');
return new Error('このメールアドレスは既に登録されています');
case 'auth/invalid-credential':
throw new Error('エラーが発生しました');
return new Error('エラーが発生しました');
case 'auth/invalid-email':
throw new Error('メールアドレスを確認してください');
return new Error('メールアドレスを確認してください');
case 'auth/invalid-user-token':
throw new Error('再ログインしてください');
return new Error('再ログインしてください');
case 'auth/invalid-verification-code':
throw new Error('認証コードを確認してください');
return new Error('認証コードを確認してください');
case 'auth/invalid-verification-id':
throw new Error('エラーが発生しました');
return new Error('エラーが発生しました');
case 'auth/operation-not-allowed':
throw new Error('エラーが発生しました');
return new Error('エラーが発生しました');
case 'auth/requires-recent-login':
throw new Error('再認証が必要です');
return new Error('再認証が必要です');
case 'auth/user-disabled':
throw new Error('このアカウントは無効化されています');
return new Error('このアカウントは無効化されています');
case 'auth/user-mismatch':
throw new Error('パスワードを確認してください');
return new Error('パスワードを確認してください');
case 'auth/user-not-found':
throw new Error('このメールアドレスは登録されていません');
return new Error('このメールアドレスは登録されていません');
case 'auth/user-token-expired':
throw new Error('エラーが発生しました');
return new Error('エラーが発生しました');
case 'auth/weak-password':
throw new Error('より安全なパスワードを設定してください');
return new Error('より安全なパスワードを設定してください');
case 'auth/wrong-password':
throw new Error('パスワードを確認してください');
return new Error('パスワードを確認してください');
default:
throw new Error('エラーが発生しました');
return new Error('エラーが発生しました');
}
};

const sendPasswordResetEmail = async (email: string) =>
FirebaseSendPasswordResetEmail(Firebase.instance.auth, email).catch(handleError);
FirebaseSendPasswordResetEmail(Firebase.instance.auth, email).catch((error) => {
throw handleError(error);
});

const signIn = async (email: string, password: string) =>
signInWithEmailAndPassword(Firebase.instance.auth, email, password).catch(handleError);
signInWithEmailAndPassword(Firebase.instance.auth, email, password).catch((error) => {
throw handleError(error);
});

const signOut = async () => FirebaseSignOut(Firebase.instance.auth).catch(handleError);
const signOut = async () =>
FirebaseSignOut(Firebase.instance.auth).catch((error) => {
throw handleError(error);
});

const subscribe = (nextOrObserver: (user: User | null) => void) =>
onAuthStateChanged(Firebase.instance.auth, nextOrObserver);
Expand Down
9 changes: 9 additions & 0 deletions web/src/adapters/infrastructure/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { FirebaseApp, initializeApp } from 'firebase/app';
import { Auth, getAuth } from 'firebase/auth';
import { Firestore, getFirestore } from 'firebase/firestore';

export default class Firebase {
private _app: FirebaseApp;
private _auth: Auth;
private _firetore: Firestore;
private static _instance: Firebase; // eslint-disable-line no-use-before-define

private constructor() {
Expand All @@ -19,6 +21,7 @@ export default class Firebase {

this._auth = getAuth(this._app);
this._auth.languageCode = 'ja';
this._firetore = getFirestore(this._app);
}

public static get instance(): Firebase {
Expand All @@ -35,4 +38,10 @@ export default class Firebase {
this._auth = getAuth(this._app);
return this._auth;
}

public get firetore(): Firestore {
if (this._firetore) return this._firetore;
this._firetore = getFirestore(this._app);
return this._firetore;
}
}
78 changes: 78 additions & 0 deletions web/src/adapters/infrastructure/gear/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
addDoc,
collection,
deleteDoc,
doc,
DocumentReference,
FirestoreError,
getDocs,
orderBy,
query,
QuerySnapshot,
setDoc,
serverTimestamp,
} from 'firebase/firestore';

import { Gear } from '../../../domains/gear';
import Firebase from '../firebase';

export interface IGearDriver {
create(data: Gear): Promise<DocumentReference>;
destroy(data: Gear): Promise<void>;
list(): Promise<QuerySnapshot>;
update(data: Gear): Promise<void>;
}

export default function gearDriver(): IGearDriver {
const gearsRef = collection(Firebase.instance.firetore, 'gears');

const handleError = (error: FirestoreError): Error => {
switch (error.code) {
case 'already-exists':
return new Error('既に同じデータが存在しています');
case 'not-found':
return new Error('データが見つかりませんでした');
case 'permission-denied':
return new Error('権限が不足しています');
default:
return new Error('エラーが発生しました');
}
};

const create = async (data: Gear) =>
addDoc(gearsRef, {
maker: data.maker,
name: data.name,
type: data.type,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
}).catch((error) => {
throw handleError(error);
});

const destroy = async (data: Gear) =>
deleteDoc(doc(gearsRef, data.id)).catch((error) => {
throw handleError(error);
});

const list = async () =>
getDocs(query(gearsRef, orderBy('maker'), orderBy('name'))).catch((error) => {
throw handleError(error);
});

const update = async (data: Gear) =>
setDoc(
doc(gearsRef, data.id),
{
maker: data.maker,
name: data.name,
type: data.type,
updatedAt: serverTimestamp(),
},
{ merge: true }
).catch((error) => {
throw handleError(error);
});

return { create, destroy, list, update };
}
29 changes: 29 additions & 0 deletions web/src/adapters/repositories/gear/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Gear } from '../../../domains/gear';
import { IGearRepository } from '../../../interface/repository/gear';
import gearDriver from '../../infrastructure/gear';

export default function gearRepository(): IGearRepository {
const create = async (data: Gear) => {
await gearDriver().create(data);
};

const destroy = async (data: Gear) => {
await gearDriver().destroy(data);
};

const list = async () =>
gearDriver()
.list()
.then((querySnapshot) =>
querySnapshot.docs.map((doc) => {
const data = doc.data();
return new Gear({ id: doc.id, maker: data.maker, name: data.name, type: data.type });
})
);

const update = async (data: Gear) => {
await gearDriver().update(data);
};

return { create, destroy, list, update };
}
4 changes: 2 additions & 2 deletions web/src/adapters/repositories/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TUser } from '../../../domains/user';
import { User } from '../../../domains/user';
import { IUserUseCase } from '../../../interface/useCase/user';
import authenticationDriver from '../../infrastructure/authentication';

Expand All @@ -15,7 +15,7 @@ export default function userRepository(): IUserUseCase {
await authenticationDriver().signOut();
};

const subscribe = (nextOrObserver: (authid: TUser['authid']) => void) =>
const subscribe = (nextOrObserver: (authid: User['authid']) => void) =>
authenticationDriver().subscribe((user) => nextOrObserver(user?.uid ?? null));

return { sendPasswordResetEmail, signIn, signOut, subscribe };
Expand Down
4 changes: 2 additions & 2 deletions web/src/adapters/stores/authentication/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable import/prefer-default-export */
import create from 'zustand';

import { TUser } from '../../../domains/user';
import { User } from '../../../domains/user';

type AuthState = Pick<TUser, 'authid'> & { initializing: boolean };
type AuthState = Pick<User, 'authid'> & { initializing: boolean };

export const useAuthStore = create<AuthState>(() => ({ authid: null, initializing: true }));
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

import { Edit } from '@mui/icons-material';
import { Box, Card, CardContent, IconButton, Typography } from '@mui/material';
import { Link } from 'react-router-dom';

import { Gear } from '../../../../../../domains/gear';

type Props = { gear: Gear };

export default function GearCard({ gear }: Props) {
return (
<Card>
<CardContent>
<Box display="flex" justifyContent="space-between">
<Typography color="text.secondary" gutterBottom variant="body2">
{gear.maker}
</Typography>
<IconButton component={Link} size="small" to={`/gears/${gear.id}`}>
<Edit fontSize="small" />
</IconButton>
</Box>
<Typography fontWeight="bold" gutterBottom variant="body1">
{gear.name}
</Typography>
<Typography color="text.secondary" variant="caption">
{gear.typeJP}
</Typography>
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';

import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';

type Props = { onClose: () => void; onSubmit: () => void; open: boolean };

export default function GearDeleteDialog({ onClose, onSubmit, open }: Props) {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle variant="h2">この機材を削除しますか?</DialogTitle>
<DialogContent>
<DialogContentText variant="body1">
この操作は元に戻すことができません。本当によろしいですか?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>キャンセルする</Button>
<Button color="error" onClick={onSubmit}>
削除する
</Button>
</DialogActions>
</Dialog>
);
}
Loading