SCALE — Build Lab
開発パターン · REACT CONTEXT

認証コンテキスト

CATEGORY開発パターン TYPEReact Context EFFORT90〜240分 DIFFICULTY
PRIMARY CODE
tsx · lib/auth-context.tsx
'use client';

import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';

export interface User {
  id: string;
  name: string;
  email: string;
  altEmail?: string;
  role: 'admin' | 'manager' | 'member';
  departmentId: string;
  departmentName: string;
  avatar: string;
  allowedSystems: string[];
}

const SESSION_KEY = 'sb_user';

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<boolean>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

// Demo users for initial setup
const DEMO_USERS: (User & { password: string })[] = [
  {
    id: '1',
    name: '大串', // 2026-05-06 ce-2026-05-06-03: タスクシート担当者表記「大串」と統一(フル表記は法人代表者名等の特別な場面のみ)
    email: 'y-ogushi@scale-group.co.jp',
    password: 'scale2023',
    role: 'admin',
    departmentId: 'all',
    departmentName: '経営',
    avatar: '大',
    allowedSystems: ['command', 'assistant', 'pilot', 'tasks', 'docs', 'admin', 'services', 'system-mgmt', 'design', 'writing', 'x', 'seo', 'hp', 'fs', 'scale-lead', 'pm', 'pipeline', 'accounting', 'hr', 'ai-ops', 'automation', 'datalake', 'goals', 'partners', 'brand', 'rnd', 'insight', 'cmo', 'build'],
  },
  {
    id: '2',
    name: 'ハヤテ',
    email: 'marketing@scale-group.co.jp',
    password: 'marketing',
    role: 'manager',
    departmentId: 'marketing',
    departmentName: 'マーケティング',
    avatar: 'ハ',
    allowedSystems: ['assistant', 'seo', 'x', 'tasks', 'sites', 'goals'],
  },
  {
    id: '3',
    name: 'センリ',
    email: 'sales@scale-group.co.jp',
    password: 'sales',
    role: 'manager',
    departmentId: 'sales',
    departmentName: '営業',
    avatar: 'セ',
    allowedSystems: ['assistant', 'fs', 'scale-lead', 'tasks', 'sites', 'goals'],
  },
  {
    id: '4',
    name: '細川',
    email: 'hosokawa@scale-group.co.jp',
    altEmail: 's.hosokawa03044@gmail.com',
    password: 'scale2023',
    role: 'admin',
    departmentId: 'all',
    departmentName: 'PM',
    avatar: '細',
    allowedSystems: ['command', 'assistant', 'pilot', 'tasks', 'docs', 'admin', 'services', 'system-mgmt', 'design', 'writing', 'x', 'seo', 'hp', 'fs', 'scale-lead', 'pm', 'pipeline', 'hr', 'ai-ops', 'automation', 'datalake', 'goals', 'partners', 'brand', 'rnd', 'insight', 'cmo'],
  },
];

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Migrate legacy key if present
    try {
      const legacy = localStorage.getItem('scale-base-user');
      if (legacy && !sessionStorage.getItem(SESSION_KEY)) {
        sessionStorage.setItem(SESSION_KEY, legacy);
      }
    } catch { /* ignore */ }

    const stored = sessionStorage.getItem(SESSION_KEY);
    if (stored) {
      try {
        const parsed = JSON.parse(stored);
        const master = DEMO_USERS.find(u => u.email === parsed.email || u.name === parsed.name);
        if (master) {
          parsed.allowedSystems = master.allowedSystems;
          parsed.altEmail = master.altEmail;
          parsed.role = master.role;
          sessionStorage.setItem(SESSION_KEY, JSON.stringify(parsed));
        }
        setUser(parsed);
      } catch { /* ignore */ }
    }
    setIsLoading(false);
  }, []);

  const login = useCallback(async (identifier: string, password: string): Promise<boolean> => {
    // Accept email, altEmail, or name (case-insensitive)
    const input = identifier.trim().toLowerCase();
    const found = DEMO_USERS.find(u =>
      u.password === password && (
        u.email.toLowerCase() === input ||
        (u.altEmail && u.altEmail.toLowerCase() === input) ||
        u.name.toLowerCase() === input
      )
    );
    if (!found) return false;
    const { password: _, ...userData } = found;
    setUser(userData);
    sessionStorage.setItem(SESSION_KEY, JSON.stringify(userData));
    return true;
  }, []);

  const logout = useCallback(() => {
    setUser(null);
    try { sessionStorage.removeItem(SESSION_KEY); } catch { /* ignore */ }
    try { localStorage.removeItem('scale-base-user'); } catch { /* ignore */ }
  }, []);

  return (
    <AuthContext.Provider value={{ user, isLoading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}

前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • 認証必須のダッシュボード

認証コンテキスト

:LiTarget: 用途

ユーザー認証状態をアプリ全体で共有するReact Context。

:LiSparkle: 特徴

  • ログイン/ログアウト
  • ユーザー情報共有
  • 権限チェック

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

:LiInfo: lib/auth-context.tsx の中身そのもの。コピペ即可。

'use client';

import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';

export interface User {
  id: string;
  name: string;
  email: string;
  altEmail?: string;
  role: 'admin' | 'manager' | 'member';
  departmentId: string;
  departmentName: string;
  avatar: string;
  allowedSystems: string[];
}

const SESSION_KEY = 'sb_user';

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<boolean>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

// Demo users for initial setup
const DEMO_USERS: (User & { password: string })[] = [
  {
    id: '1',
    name: '大串', // 2026-05-06 ce-2026-05-06-03: タスクシート担当者表記「大串」と統一(フル表記は法人代表者名等の特別な場面のみ)
    email: 'y-ogushi@scale-group.co.jp',
    password: 'scale2023',
    role: 'admin',
    departmentId: 'all',
    departmentName: '経営',
    avatar: '大',
    allowedSystems: ['command', 'assistant', 'pilot', 'tasks', 'docs', 'admin', 'services', 'system-mgmt', 'design', 'writing', 'x', 'seo', 'hp', 'fs', 'scale-lead', 'pm', 'pipeline', 'accounting', 'hr', 'ai-ops', 'automation', 'datalake', 'goals', 'partners', 'brand', 'rnd', 'insight', 'cmo', 'build'],
  },
  {
    id: '2',
    name: 'ハヤテ',
    email: 'marketing@scale-group.co.jp',
    password: 'marketing',
    role: 'manager',
    departmentId: 'marketing',
    departmentName: 'マーケティング',
    avatar: 'ハ',
    allowedSystems: ['assistant', 'seo', 'x', 'tasks', 'sites', 'goals'],
  },
  {
    id: '3',
    name: 'センリ',
    email: 'sales@scale-group.co.jp',
    password: 'sales',
    role: 'manager',
    departmentId: 'sales',
    departmentName: '営業',
    avatar: 'セ',
    allowedSystems: ['assistant', 'fs', 'scale-lead', 'tasks', 'sites', 'goals'],
  },
  {
    id: '4',
    name: '細川',
    email: 'hosokawa@scale-group.co.jp',
    altEmail: 's.hosokawa03044@gmail.com',
    password: 'scale2023',
    role: 'admin',
    departmentId: 'all',
    departmentName: 'PM',
    avatar: '細',
    allowedSystems: ['command', 'assistant', 'pilot', 'tasks', 'docs', 'admin', 'services', 'system-mgmt', 'design', 'writing', 'x', 'seo', 'hp', 'fs', 'scale-lead', 'pm', 'pipeline', 'hr', 'ai-ops', 'automation', 'datalake', 'goals', 'partners', 'brand', 'rnd', 'insight', 'cmo'],
  },
];

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Migrate legacy key if present
    try {
      const legacy = localStorage.getItem('scale-base-user');
      if (legacy && !sessionStorage.getItem(SESSION_KEY)) {
        sessionStorage.setItem(SESSION_KEY, legacy);
      }
    } catch { /* ignore */ }

    const stored = sessionStorage.getItem(SESSION_KEY);
    if (stored) {
      try {
        const parsed = JSON.parse(stored);
        const master = DEMO_USERS.find(u => u.email === parsed.email || u.name === parsed.name);
        if (master) {
          parsed.allowedSystems = master.allowedSystems;
          parsed.altEmail = master.altEmail;
          parsed.role = master.role;
          sessionStorage.setItem(SESSION_KEY, JSON.stringify(parsed));
        }
        setUser(parsed);
      } catch { /* ignore */ }
    }
    setIsLoading(false);
  }, []);

  const login = useCallback(async (identifier: string, password: string): Promise<boolean> => {
    // Accept email, altEmail, or name (case-insensitive)
    const input = identifier.trim().toLowerCase();
    const found = DEMO_USERS.find(u =>
      u.password === password && (
        u.email.toLowerCase() === input ||
        (u.altEmail && u.altEmail.toLowerCase() === input) ||
        u.name.toLowerCase() === input
      )
    );
    if (!found) return false;
    const { password: _, ...userData } = found;
    setUser(userData);
    sessionStorage.setItem(SESSION_KEY, JSON.stringify(userData));
    return true;
  }, []);

  const logout = useCallback(() => {
    setUser(null);
    try { sessionStorage.removeItem(SESSION_KEY); } catch { /* ignore */ }
    try { localStorage.removeItem('scale-base-user'); } catch { /* ignore */ }
  }, []);

  return (
    <AuthContext.Provider value={{ user, isLoading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}

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

/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/lib/auth-context.tsx

:LiHandPointer: 使い方

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

:LiAlertCircle: 注意事項

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