외부 자동화 도구가 AhaWiki 페이지를 직접 수정할 수 있도록 사용자 개인 API Key 인증을 추가했다.
세션 쿠키와 reCAPTCHA 없이 Authorization: Bearer <key> 헤더로 페이지를 읽고 저장할 수 있는 API를 제공한다.
페이지 히스토리에는 편집자가 기존 사용자로 기록되며, API Key를 통한 편집은 Page.viaApi = TRUE로 표시한다.
Page.viaApi = TRUE로 기록한다.WikiPermission 규칙과 key 소유자 사용자의 권한을 그대로 따른다.UserApiKey 테이블은 사용자별 API Key 메타데이터와 hash를 저장한다.
seq BIGINT AUTO_INCREMENT PRIMARY KEYuser INT NOT NULL — User.seq FKkeyHash VARCHAR(64) NOT NULL UNIQUE — SHA-256 hexkeyPrefix VARCHAR(32) NOT NULL — 목록에서 key를 식별하기 위한 prefixlabel VARCHAR(255) NOT NULLdateInserted DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMPdateLastUsed DATETIME NULLdateRevoked DATETIME NULLINDEX (user, dateRevoked) — 사용자별 key 목록 조회keyHash UNIQUE가 인증 조회 인덱스 역할을 하므로 별도 INDEX (keyHash)는 만들지 않는다.
Page 테이블에 viaApi BOOLEAN NOT NULL DEFAULT FALSE 컬럼을 추가했다.
FALSE다.TRUE로 기록한다.Page, PageWithoutContent, row parser, history 조회, insert SQL에 모두 viaApi를 포함한다.PageLogic.insert는 viaApi: Boolean = false 기본값을 받고, API Key 저장 시 true를 전달한다.SessionLogic.getApiKeyUser(request)가 Authorization: Bearer <key> 헤더를 읽고 raw key를 SHA-256으로 hash한 뒤 UserApiKey에서 활성 key를 조회한다.
인증 성공 시:
dateLastUsed를 갱신한다.User.SessionUser를 만든다.AhaWiki API에서는 RequestWrapper.forUser(user)로 인증 사용자를 ContextWikiPage와 WikiPermission에 전달한다.
이 처리가 없으면 인증은 성공해도 권한 계산이 익명 사용자 기준으로 동작할 수 있다.
외부 사용 설명서는 Api에 둔다.
GET /api/v1/page/*nameEncodedPOST /api/v1/page/*nameEncodedGET /api/v1/pagesPOST /api/v1/pages/metadataGET /api/v1/changesPOST /api/v1/renameDELETE /api/v1/page/*nameEncoded저장 API는 JSON body의 revision, text, comment, minorEdit를 사용한다.
revision이 최신과 다르면 409 Conflict를 반환한다.
현재 API 저장은 PageLogic.insert(..., viaApi = true), cache invalidate, page calculation enqueue까지만 수행한다.
웹 편집의 websocket broadcast와 Telegram 알림은 보내지 않는다.
Telegram 알림은 minorEdit와 viaApi 저장을 제외한다.
AhaWikiDoc sync를 위해 AhaWiki API를 보강했다.
name, revision, dateTime, isMinorEdit, viaApi, contentHash를 반환한다.revision, dateTime, hash를 한 번에 조회한다.afterRevision은 페이지별 revision이므로 name으로 단일 페이지를 지정한 경우에만 허용한다.viaApi = true redirect page를 만든다.confirm: true를 요구하며, 웹 삭제와 같은 정책으로 첨부파일도 삭제 처리한다.lastSyncedAt, page별 revision, dateTime, contentHash를 기록하는 방식을 권장한다.POST /api/v1/rename으로 기존 page history를 보존한다.Account Settings에 API Key 관리 섹션을 추가했다.
목록 조회에서는 plain text key를 반환하지 않고 keyPrefix만 보여준다.
세션 기반 내부 API:
GET /api/account/ApiKeysPOST /api/account/ApiKeysDELETE /api/account/ApiKeys/:seq이 API는 로그인 세션과 CSRF token이 필요하다.
POST 응답에만 plain text key를 포함하고, 이후 조회에서는 keyPrefix만 반환한다.
Admin SPA에 /Admin/ApiKeys 화면을 추가했다.
세션 기반 내부 Admin API:
GET /api/Admin/ApiKeysDELETE /api/Admin/ApiKeys/:seqAdmin 권한과 CSRF token이 필요하다.
viaApi는 사용자가 자동화 편집을 구분할 수 있도록 여러 화면과 API에 노출한다.
Api.change 응답에 viaApi 포함Api.adminRecentChanges 응답에 viaApi 포함Show ViaApi 필터 추가RecentChanges 매크로에 Include via API edits 토글과 minor edit, Via API 이모지 컬럼 추가/api/change에 includeViaApi 파라미터 추가SecureRandom으로 32 bytes를 생성하고 Base64 URL-safe 문자열로 표시한다. 형식은 ahawiki_<token>이다./api/v1/ 경로에서 CSRF filter를 우회한다.ApiV1SpecviaApi = TRUE409 Conflictsince, includeMinorEdit, includeViaApi, invalid since 검증afterRevision을 단일 페이지에만 허용하는 정책 검증ApiV1FilterSpecFilters 체인에서 /api/v1/ POST가 CSRF token 없이 Bearer 인증만으로 저장되는지 검증UnitTestSuiteSpecPage schema에 viaApi 컬럼 추가관련 테스트:
sbt.bat "testOnly com.aha00a.controllers.ApiV1Spec"sbt.bat "testOnly com.aha00a.controllers.ApiV1FilterSpec"마지막 확인 시 ApiV1Spec 18개 테스트가 통과했다.
Similar pages by cosine similarity. Words after page name are term frequency.