【第4回】パスワードの登録・更新機能を実装する

こんにちは。今回も引き続き、ローカル環境で開発している個人向けパスワード管理アプリについて、開発の記録を綴っていきます。
前回までで、パスワードの一覧表示ができるようになりました。今回は、いよいよユーザーが新しい情報を登録したり、既存の情報を編集したりできるようにしていきます。



目次
「登録」と「編集」ができるようになると、アプリは“使える”ものになる
ただ情報を見るだけのアプリと、実際に情報を追加・変更できるアプリ。この違いはとても大きいです。
使いながら自分のデータを蓄積していけることで、日々の生活の中に自然と組み込まれていきます。
今回実装したのは、以下の2つの画面です:
- パスワードの新規登録フォーム
- パスワードの更新(編集)フォーム
パスワード登録画面(app/passwords/new/page.tsx)
ユーザーが入力できる項目は以下のとおりです:
- カテゴリ
- サイト名
- サイトURL
- ログインID
- パスワード
- メールアドレス
- メモ
Next.js のクライアントコンポーネントとして、useState
で状態を管理します。
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
const AddPassword: React.FC = () => {
const router = useRouter();
const [formData, setFormData] = useState({
category: "",
site_name: "",
site_url: "",
login_id: "",
password: "",
email: "",
memo: "",
});
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
): void => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
const res = await fetch("/api/passwords", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
});
if (res.ok) {
alert("登録成功!");
setFormData({ category: "", site_name: "", site_url: "", login_id: "", password: "", email: "", memo: "" });
router.push('/');
} else {
alert("登録失敗");
}
};
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">新しいサイトを作成</h1>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div>
<label>カテゴリ</label>
<input
type="text"
name="category"
value={formData.category}
className="w-full p-2 border rounded"
onChange={handleChange}
/>
</div>
<div>
<label>サイト名</label>
<input
type="text"
name="site_name"
value={formData.site_name}
className="w-full p-2 border rounded"
onChange={handleChange}
required
/>
</div>
<div>
<label>サイトURL</label>
<input
type="text"
name="site_url"
value={formData.site_url}
className="w-full p-2 border rounded"
onChange={handleChange}
required
/>
</div>
<div>
<label>ログインID</label>
<input
type="text"
name="login_id"
value={formData.login_id}
className="w-full p-2 border rounded"
onChange={handleChange}
/>
</div>
<div>
<label>パスワード</label>
<input
type="text"
name="password"
value={formData.password}
className="w-full p-2 border rounded"
onChange={handleChange}
required
/>
</div>
<div>
<label>メールアドレス</label>
<input
type="email"
name="email"
value={formData.email}
className="w-full p-2 border rounded"
onChange={handleChange}
/>
</div>
<div>
<label>メモ</label>
<textarea
name="memo"
value={formData.memo}
className="w-full p-2 border rounded"
onChange={handleChange}
></textarea>
</div>
<button type="submit" className="bg-blue-500 text-white py-2 px-4 rounded hover:bg--600">登録</button>
</form>
</div>
);
};
export default AddPassword;
パスワード更新画面(app/passwords/edit/[id]/page.tsx)
既存の情報を後から修正できることで、安心してデータを活用できるようになります。
この画面もクライアントコンポーネントで構成し、[id]
付きのルーティングにより、特定のレコードを取得・編集できるようにしました。
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
type Password = {
id: number;
category: string | null;
site_name: string;
site_url: string;
login_id: string | null;
password: string;
email: string | null;
memo: string | null;
};
const UpdatePasswordPage = ({ params }: { params: Promise<{ id: string }> }) => {
const router = useRouter();
const [id, setId] = useState<string | null>(null);
const [siteName, setSiteName] = useState('');
const [category, setCategory] = useState('');
const [siteUrl, setSiteUrl] = useState('');
const [loginId, setLoginId] = useState('');
const [password, setPassword] = useState<Password | null>(null);
const [pass, setPass] = useState('');
const [email, setEmail] = useState('');
const [memo, setMemo] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
// paramsをアンラップしてIDを取得
useEffect(() => {
const fetchParams = async () => {
const resolvedParams = await params;
setId(resolvedParams.id);
};
fetchParams();
}, [params]);
// IDが取得できたらパスワードデータを取得
useEffect(() => {
if (!id) return;
const fetchPassword = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/passwords/${id}`);
if (!response.ok) throw new Error('Failed to fetch password data');
const data: Password = await response.json();
setPassword(data);
setCategory(data.category || '');
setSiteName(data.site_name);
setSiteUrl(data.site_url);
setLoginId(data.login_id || '');
setPass(data.password);
setEmail(data.email || '');
setMemo(data.memo || '');
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
console.error('Error fetching password:', err);
}
} finally {
setLoading(false);
}
};
fetchPassword();
}, [id]);
// 更新処理
const handleUpdate = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
try {
const response = await fetch(`/api/passwords/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category, siteName, siteUrl, loginId, pass, email, memo }),
});
if (!response.ok) throw new Error('Failed to update password');
alert('パスワードが更新されました!');
router.push('/'); // 更新後にメインページへ遷移
} catch (error) {
console.error('Error updating password:', error);
}
};
if (loading) {
return <div>読み込み中...</div>;
}
if (error) {
return <p className="text-red-500">{error}</p>;
}
if (!password) {
return <div>データが見つかりません</div>;
}
return (
<div className="p-4">
<h1 className="text-xl font-bold mb-4">パスワード更新</h1>
<form onSubmit={handleUpdate} className="space-y-4">
<div>
<label className="block text-gray-700 font-bold mb-2" htmlFor="siteName">カテゴリ</label>
<input
id="category"
type="text"
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-2" htmlFor="siteName">サイト名</label>
<input
id="siteName"
type="text"
value={siteName}
onChange={(e) => setSiteName(e.target.value)}
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-2" htmlFor="siteUrl">サイトURL</label>
<input
id="siteUrl"
type="text"
value={siteUrl}
onChange={(e) => setSiteUrl(e.target.value)}
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-2" htmlFor="loginId">ログインID</label>
<input
id="loginId"
type="text"
value={loginId}
onChange={(e) => setLoginId(e.target.value)}
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-2" htmlFor="password">パスワード</label>
<input
id="password"
type="text"
value={pass}
onChange={(e) => setPass(e.target.value)}
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-2" htmlFor="email">メールアドレス</label>
<input
id="email"
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-2" htmlFor="memo">メモ</label>
<textarea
id="content"
value={memo}
onChange={(e) => setMemo(e.target.value)}
className="w-full p-2 border rounded"
rows={6}
/>
</div>
<button
type="submit"
className="bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
>
更新
</button>
</form>
</div>
);
};
export default UpdatePasswordPage;
バックエンド:APIエンドポイントの拡張
登録(POST):app/api/passwords/route.ts
必須項目が入力されていない場合には 400 エラーを返すようにしています。
なお前回記述したソースコードの下に記載してください。
export async function POST(req: Request) {
try {
const body = await req.json();
const { category, site_name, site_url, login_id, password, email, memo } = body;
if (!site_name || !site_url || !password) {
return NextResponse.json({ error: '必須項目不足' }, { status: 400 });
}
runExecute(
'INSERT INTO password_manager (category, site_name, site_url, login_id, password, email, memo) VALUES (?, ?, ?, ?, ?, ?, ?)',
[category, site_name, site_url, login_id, password, email, memo]
);
return NextResponse.json({ message: '登録成功' });
} catch (error) {
return NextResponse.json({ error: '登録失敗' }, { status: 500 });
}
}
更新(PUT):app/api/passwords/[id]/route.ts
対象データが存在しない場合や、更新が行われなかった場合には 404 エラーを返すことで、フロント側でのエラーハンドリングが容易になります。
import { NextResponse } from 'next/server';
import { runGet, runExecute } from '../../../../lib/db';
import type { Password } from '../../../../types/password';
// GET: 特定のパスワード情報を取得
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
if (!id || isNaN(Number(id))) {
return NextResponse.json(
{ error: 'Invalid password_manager ID.' },
{ status: 400 }
);
}
try {
const result = runGet<Password>(
'SELECT * FROM password_manager WHERE id = ?',
[Number(id)]
);
if (!result) {
return NextResponse.json(
{ error: 'password_manager entry not found.' },
{ status: 404 }
);
}
return NextResponse.json(result);
} catch (error) {
console.error('Error fetching password_manager entry:', error);
return NextResponse.json(
{ error: 'Failed to fetch password_manager entry.' },
{ status: 500 }
);
}
}
// PUT: 特定のパスワード情報を更新
export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
try {
const body = await request.json();
const {
category,
siteName,
siteUrl,
loginId,
pass,
email,
memo,
} = body;
if (!siteName || !siteUrl || !pass) {
return NextResponse.json(
{ error: 'site_name, site_url, and password are required.' },
{ status: 400 }
);
}
const result = runExecute(
'UPDATE password_manager SET category = ?, site_name = ?, site_url = ?, login_id = ?, password = ?, email = ?, memo = ? WHERE id = ?',
[category, siteName, siteUrl, loginId, pass, email, memo, Number(id)]
);
return NextResponse.json({
message: 'password_manager entry updated successfully.',
});
} catch (error) {
console.error('Error updating password_manager entry:', error);
return NextResponse.json(
{ error: 'Failed to update password_manager entry.' },
{ status: 500 }
);
}
}
今回の実装で感じたこと
アプリというのは、“見る”だけから“書ける”ようになると、一気に実用性が増します。
とはいえ、入力ミスや操作ミスを防ぐ工夫、わかりやすい導線づくりがより重要になってきます。
たとえば……
- 「更新ボタンを押したつもりが反応しない」
- 「どこを修正したのか忘れてしまった」
こうした“小さな不便”を減らすには、バリデーションやUXの改善が鍵になりそうです。
今後はそのあたりも丁寧に取り組んでいきたいと考えています。
次回予告
次回は GitHub へのコミットやバージョン管理のポイントについてご紹介します。
開発履歴をきちんと残すことで、メンテナンス性も大きく向上します。
おわりに
自分の生活に合わせて、自分のために作るアプリ。
完璧ではなくても、日々少しずつ使いやすくなっていく感覚が、40代になってからの開発の面白さだと感じています。
同じように「もう一度コードを書いてみたい」と思っている方にとって、少しでもヒントや励みになれば嬉しいです。
0 件のコメント:
コメントを投稿
コメントをお待ちしています。