SCALE — Build Lab
開発パターン · TYPESCRIPT LIBRARY

パスワード管理パターン

CATEGORY開発パターン TYPETypeScript Library EFFORT90〜240分 DIFFICULTY
PRIMARY CODE
ts · lib/passwords-data.ts
// パスワード管理データ層(D1 + Cloudflare Pages Functions経由)
// 全件取得時にサーバー側で復号して返すため、UI側は通常の文字列として扱える。
// 「みんなで共有」する設計:登録するとscale-base全体に反映される。

export const PW_CATEGORIES = [
  '業務アプリ',
  'インフラ・サーバー',
  'マーケティング',
  '金融・決済',
  'SNS',
  'クライアント連携',
  'その他',
];

export interface PasswordEntry {
  id: string;
  service: string;
  url: string;
  username: string;
  password: string;
  category: string;
  owner: string;
  memo: string;
  tags: string[];
  addedBy?: string;
  addedVia?: 'manual' | 'slack-bot';
  createdAt: string;
  lastUpdated: string;
}

export async function fetchPasswords(): Promise<PasswordEntry[]> {
  try {
    const res = await fetch('/api/passwords', { cache: 'no-store' });
    if (!res.ok) return [];
    const data = await res.json();
    return (data.passwords || []) as PasswordEntry[];
  } catch {
    return [];
  }
}

export async function createPassword(entry: {
  service: string; url?: string; username?: string; password: string;
  category?: string; owner?: string; memo?: string; tags?: string[]; addedBy?: string;
}): Promise<{ ok: boolean; id?: string; error?: string }> {
  try {
    const res = await fetch('/api/passwords', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(entry),
    });
    const data = await res.json();
    if (!res.ok) return { ok: false, error: data.error || `HTTP ${res.status}` };
    return { ok: true, id: data.id };
  } catch (err) {
    return { ok: false, error: String(err) };
  }
}

export async function updatePassword(id: string, patch: {
  service?: string; url?: string; username?: string; password?: string;
  category?: string; owner?: string; memo?: string; tags?: string[];
}): Promise<boolean> {
  try {
    const res = await fetch(`/api/passwords/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(patch),
    });
    return res.ok;
  } catch {
    return false;
  }
}

export async function deletePassword(id: string): Promise<boolean> {
  try {
    const res = await fetch(`/api/passwords/${id}`, { method: 'DELETE' });
    return res.ok;
  } catch {
    return false;
  }
}

export function genId(): string {
  return 'pw-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
}

export function nowISO(): string {
  return new Date().toISOString();
}

export function copyToClipboard(text: string): Promise<boolean> {
  if (typeof navigator === 'undefined' || !navigator.clipboard) return Promise.resolve(false);
  return navigator.clipboard.writeText(text).then(() => true).catch(() => false);
}

// ─── 旧API(localStorageベース)後方互換用。次の改修で削除予定 ───
const LEGACY_KEY = 'scale-passwords';
export function loadPasswords(): PasswordEntry[] {
  if (typeof window === 'undefined') return [];
  try {
    const v = localStorage.getItem(LEGACY_KEY);
    return v ? JSON.parse(v) : [];
  } catch { return []; }
}
export function savePasswords(list: PasswordEntry[]) {
  if (typeof window === 'undefined') return;
  localStorage.setItem(LEGACY_KEY, JSON.stringify(list));
}

前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • 1Password代替
  • APIキー共有
  • クレデンシャル管理

パスワード管理パターン

:LiTarget: 用途

チームで共有するアカウント情報・APIキーの安全な管理パターン。

:LiSparkle: 特徴

  • 暗号化保管
  • チーム共有
  • カテゴリ分類
  • アクセスログ

:LiCode: 実コード(SCALE Base より自動抽出)

:LiInfo: lib/passwords-data.ts の中身そのもの。コピペ即可。

// パスワード管理データ層(D1 + Cloudflare Pages Functions経由)
// 全件取得時にサーバー側で復号して返すため、UI側は通常の文字列として扱える。
// 「みんなで共有」する設計:登録するとscale-base全体に反映される。

export const PW_CATEGORIES = [
  '業務アプリ',
  'インフラ・サーバー',
  'マーケティング',
  '金融・決済',
  'SNS',
  'クライアント連携',
  'その他',
];

export interface PasswordEntry {
  id: string;
  service: string;
  url: string;
  username: string;
  password: string;
  category: string;
  owner: string;
  memo: string;
  tags: string[];
  addedBy?: string;
  addedVia?: 'manual' | 'slack-bot';
  createdAt: string;
  lastUpdated: string;
}

export async function fetchPasswords(): Promise<PasswordEntry[]> {
  try {
    const res = await fetch('/api/passwords', { cache: 'no-store' });
    if (!res.ok) return [];
    const data = await res.json();
    return (data.passwords || []) as PasswordEntry[];
  } catch {
    return [];
  }
}

export async function createPassword(entry: {
  service: string; url?: string; username?: string; password: string;
  category?: string; owner?: string; memo?: string; tags?: string[]; addedBy?: string;
}): Promise<{ ok: boolean; id?: string; error?: string }> {
  try {
    const res = await fetch('/api/passwords', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(entry),
    });
    const data = await res.json();
    if (!res.ok) return { ok: false, error: data.error || `HTTP ${res.status}` };
    return { ok: true, id: data.id };
  } catch (err) {
    return { ok: false, error: String(err) };
  }
}

export async function updatePassword(id: string, patch: {
  service?: string; url?: string; username?: string; password?: string;
  category?: string; owner?: string; memo?: string; tags?: string[];
}): Promise<boolean> {
  try {
    const res = await fetch(`/api/passwords/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(patch),
    });
    return res.ok;
  } catch {
    return false;
  }
}

export async function deletePassword(id: string): Promise<boolean> {
  try {
    const res = await fetch(`/api/passwords/${id}`, { method: 'DELETE' });
    return res.ok;
  } catch {
    return false;
  }
}

export function genId(): string {
  return 'pw-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
}

export function nowISO(): string {
  return new Date().toISOString();
}

export function copyToClipboard(text: string): Promise<boolean> {
  if (typeof navigator === 'undefined' || !navigator.clipboard) return Promise.resolve(false);
  return navigator.clipboard.writeText(text).then(() => true).catch(() => false);
}

// ─── 旧API(localStorageベース)後方互換用。次の改修で削除予定 ───
const LEGACY_KEY = 'scale-passwords';
export function loadPasswords(): PasswordEntry[] {
  if (typeof window === 'undefined') return [];
  try {
    const v = localStorage.getItem(LEGACY_KEY);
    return v ? JSON.parse(v) : [];
  } catch { return []; }
}
export function savePasswords(list: PasswordEntry[]) {
  if (typeof window === 'undefined') return;
  localStorage.setItem(LEGACY_KEY, JSON.stringify(list));
}

:LiFolder: ソースファイルのパス

/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/lib/passwords-data.ts

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加