# Auth DB Design ## Goal 현재 조직도 업무 데이터와 로그인 데이터를 분리한다. 분리 원칙: - 업무 데이터는 기존 `public.members`, `seat_maps`, `seat_positions` 중심으로 유지 - 인증/권한 데이터는 별도 `auth` 스키마로 분리 - 로그인 사용자는 필요할 때만 `members.id` 와 연결 이 방식이면 비밀번호, 세션, 감사로그를 업무 데이터와 분리해서 관리할 수 있고, 엑셀 임포트로 `members` 가 갱신돼도 인증 체계가 직접 흔들리지 않는다. ## Scope 이번 설계의 대상: - 사용자 계정 - 비밀번호 해시 - 세션 - 역할과 권한 - 로그인 감사로그 - 사용자와 조직 구성원 연결 이번 설계에서 제외: - SSO 연동 - OAuth/OpenID Connect - MFA - 비밀번호 재설정 메일 발송 ## Recommended Structure 권장 구조는 "같은 PostgreSQL, 다른 스키마" 이다. - 업무 스키마: `public` - 인증 스키마: `auth` 초기 운영에서는 DB 인스턴스를 분리하지 않아도 된다. 대신 아래 원칙은 바로 적용한다. - 애플리케이션 계정도 가능하면 읽기/쓰기 범위를 분리 - 인증 관련 쿼리는 `auth.*` 만 접근 - 업무 API 는 `public.*` 중심으로 접근 ## Core Tables ### `auth.users` 로그인 가능한 계정의 기준 테이블. 주요 컬럼: - `id BIGSERIAL PRIMARY KEY` - `username TEXT NOT NULL UNIQUE` - `password_hash TEXT NOT NULL` - `display_name TEXT NOT NULL` - `email TEXT` - `status TEXT NOT NULL DEFAULT 'active'` - `member_id INTEGER NULL REFERENCES public.members(id) ON DELETE SET NULL` - `last_login_at TIMESTAMPTZ` - `password_changed_at TIMESTAMPTZ` - `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` - `updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` 상태값 권장: - `active` - `locked` - `disabled` 원칙: - `username` 는 로그인 식별자 - `member_id` 는 선택 연결 - 구성원이 퇴사하거나 엑셀에서 빠져도 계정 자체는 바로 삭제하지 않음 ### `auth.roles` 권한 묶음 정의. 주요 컬럼: - `id BIGSERIAL PRIMARY KEY` - `code TEXT NOT NULL UNIQUE` - `name TEXT NOT NULL` - `description TEXT` 초기 권장 역할: - `super_admin` - `org_admin` - `viewer` ### `auth.user_roles` 사용자와 역할의 다대다 연결. 주요 컬럼: - `user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE` - `role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE` - `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` - `PRIMARY KEY (user_id, role_id)` ### `auth.permissions` 세분화된 권한 코드 정의. 주요 컬럼: - `id BIGSERIAL PRIMARY KEY` - `code TEXT NOT NULL UNIQUE` - `name TEXT NOT NULL` - `description TEXT` 초기 권장 권한: - `member.read` - `member.write` - `member.import` - `seatmap.read` - `seatmap.write` - `photo.upload` - `admin.user.manage` ### `auth.role_permissions` 역할과 권한의 다대다 연결. 주요 컬럼: - `role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE` - `permission_id BIGINT NOT NULL REFERENCES auth.permissions(id) ON DELETE CASCADE` - `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` - `PRIMARY KEY (role_id, permission_id)` ### `auth.sessions` 서버 세션 저장 테이블. 주요 컬럼: - `id UUID PRIMARY KEY` - `user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE` - `refresh_token_hash TEXT` - `issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` - `expires_at TIMESTAMPTZ NOT NULL` - `revoked_at TIMESTAMPTZ` - `ip_address INET` - `user_agent TEXT` - `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` 원칙: - 브라우저 쿠키에는 세션 식별자만 저장 - 토큰 자체를 평문으로 DB에 저장하지 않음 - 만료와 강제 로그아웃을 DB에서 통제 가능하게 함 ### `auth.login_audit_logs` 로그인 시도와 결과 기록. 주요 컬럼: - `id BIGSERIAL PRIMARY KEY` - `username TEXT NOT NULL` - `user_id BIGINT NULL REFERENCES auth.users(id) ON DELETE SET NULL` - `success BOOLEAN NOT NULL` - `failure_reason TEXT` - `ip_address INET` - `user_agent TEXT` - `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` 용도: - 로그인 실패 추적 - 계정 잠금 기준 판단 - 보안 감사 대응 ## Relationship To Current `members` 핵심은 `auth.users.member_id -> public.members.id` 연결이다. 의미: - 로그인 계정과 조직도 인원을 분리한다 - 로그인하지 않는 구성원은 `members` 에만 있어도 된다 - 외부 관리자 계정은 `member_id` 없이 운영할 수 있다 권장 규칙: - 일반 사내 사용자는 `employee_id` 기준으로 계정-구성원 연결 - 엑셀 동기화 시 `members.id` 유지가 중요하므로 이미 반영한 비교 기반 동기화 방식을 유지 - `member_id` 연결이 끊긴 계정은 자동 삭제하지 말고 관리자 검토 대상으로 둔다 ## Login Flow ### 1. 로그인 요청 입력: - `username` - `password` 처리: - `auth.users` 에서 `username` 조회 - `status != active` 이면 거부 - `password_hash` 검증 - 성공 시 `auth.sessions` 생성 - `auth.login_audit_logs` 기록 응답 권장: - 사용자 기본 정보 - 역할 목록 - 권한 목록 - 세션 만료 시각 ### 2. 인증 확인 각 보호 API 요청 시: - 세션 쿠키 또는 Bearer 토큰 확인 - `auth.sessions` 조회 - 만료/폐기 여부 확인 - 사용자 상태와 역할 재검증 ### 3. 로그아웃 처리: - 현재 세션의 `revoked_at` 업데이트 - 클라이언트 쿠키 제거 ## Authorization Model 초기에는 RBAC 기반으로 충분하다. 권장 역할별 범위: `super_admin` - 사용자 관리 - 권한 관리 - 조직도/사진/자리배치 전체 수정 `org_admin` - 조직도 조회/수정 - 엑셀 임포트 - 사진 업로드 - 자리배치 수정 `viewer` - 조직도 조회 - 자리배치 조회 API 보호 예시: - `GET /api/members`: `member.read` - `POST /api/members/import`: `member.import` - `POST /api/uploads/profile-photo`: `photo.upload` - `PUT /api/seat-maps/{seat_map_id}/layout`: `seatmap.write` - 사용자 관리 API: `admin.user.manage` ## Password Policy 비밀번호는 평문 저장 금지. 권장: - `Argon2id` 우선 - 대안으로 `bcrypt` 추가 원칙: - 첫 구현부터 해시 알고리즘 버전 정보 포함 - 비밀번호 변경 시 `password_changed_at` 갱신 - 실패 횟수 기반 잠금은 앱 로직 또는 별도 컬럼으로 확장 가능 ## Migration Plan ### Phase 1 인증 스키마와 기본 테이블만 추가. 작업: - `CREATE SCHEMA IF NOT EXISTS auth` - `auth.users` - `auth.roles` - `auth.user_roles` - `auth.permissions` - `auth.role_permissions` - `auth.sessions` - `auth.login_audit_logs` 이 단계에서는 기존 `/api/mock-login` 유지 가능. ### Phase 2 관리자 1명 이상을 수동 생성하고 실제 로그인 API 추가. 권장 추가 API: - `POST /api/auth/login` - `POST /api/auth/logout` - `GET /api/auth/me` ### Phase 3 기존 프론트엔드의 mock 로그인 제거. 변경 대상: - [frontend/public/app.js](../frontend/public/app.js) - [backend/app/main.py](../backend/app/main.py) - [backend/app/config.py](../backend/app/config.py) ### Phase 4 권한 기반으로 API 보호 적용. 우선순위: 1. 쓰기 API 보호 2. 업로드 API 보호 3. 읽기 API 권한 정리 ## Recommended SQL Skeleton ```sql CREATE SCHEMA IF NOT EXISTS auth; CREATE TABLE IF NOT EXISTS auth.users ( id BIGSERIAL PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT NOT NULL, email TEXT, status TEXT NOT NULL DEFAULT 'active', member_id INTEGER NULL REFERENCES public.members(id) ON DELETE SET NULL, last_login_at TIMESTAMPTZ, password_changed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS auth.roles ( id BIGSERIAL PRIMARY KEY, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, description TEXT ); CREATE TABLE IF NOT EXISTS auth.user_roles ( user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (user_id, role_id) ); CREATE TABLE IF NOT EXISTS auth.sessions ( id UUID PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, refresh_token_hash TEXT, issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, ip_address INET, user_agent TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ``` ## Operational Notes - 엑셀 임포트는 계속 `public.members` 기준으로 처리 - 로그인 계정 생성은 엑셀 업로드와 분리 - 프로필 사진은 현재처럼 파일 저장 + `members.photo_url` 참조 유지 가능 - 감사로그와 세션은 삭제보다 보존 기간 정책으로 관리 ## Decision 현재 프로젝트의 권장안은 아래 한 줄로 정리된다. "로그인 DB는 `auth` 스키마로 분리하고, 업무 DB는 `public` 에 유지하며, 두 영역은 `auth.users.member_id` 로만 연결한다."