# 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](/home/hyunho/projects/mh-dashboard-organization/frontend/public/app.js)
- [backend/app/main.py](/home/hyunho/projects/mh-dashboard-organization/backend/app/main.py)
- [backend/app/config.py](/home/hyunho/projects/mh-dashboard-organization/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` 로만 연결한다."
