본문으로 건너뛰기
runlot

로그인 화면 직접 만들기

앱의 로그인 화면을 직접 그리고, Runlot ID로 바로 연결하고, 오류를 처리하고, Runlot 화면을 꾸미고, 로컬에서 시험합니다.

앱은 Runlot 화면 앞뒤의 로그인 경험을 직접 만들고, Runlot 화면의 모양도 정할 수 있습니다. Runlot ID 비밀번호는 여전히 Runlot 페이지에서만 입력합니다. 직접 그린 화면은 Runlot으로 연결하거나 프로젝트 전용 방법을 씁니다.

Runlot ID로 바로 연결하기

/__runlot/auth/sign-in/runlot은 선택 매개변수 네 개를 받습니다. 없으면 지금과 똑같이 동작합니다.

매개변수효과
idpgoogle이나 github이면 선택 화면을 건너뛰고 바로 그 제공자로 갑니다. password는 이메일 입력 화면을, passkey는 패스키 창을 엽니다. 꺼진 방법이면 무시하고 선택 화면을 보여 줍니다.
login_hintRunlot 화면의 이메일 칸을 미리 채웁니다. 자동으로 제출하지 않고, 계정에 관한 어떤 정보도 앱으로 돌아오지 않습니다.
error_next오류가 났을 때 next 대신 돌아갈 곳입니다. next와 같은 규칙의 같은 출처 경로입니다.
promptnone이면 이미 Runlot에 로그인해 있고 이 앱을 쓴 적이 있는 사람을 화면 없이 로그인시킵니다. 아니면 login_required로 돌아옵니다.
import { signInUrl } from "@runlot/auth/client";

signInUrl("runlot", { idp: "google", next: "/chat" });
// /__runlot/auth/sign-in/runlot?next=%2Fchat&idp=google
signInUrl("runlot", { idp: "password", loginHint: "[email protected]", errorNext: "/login" });

idp, loginHint, errorNext, prompt는 "runlot"에만 쓸 수 있고, 다른 제공자에 넘기면 타입 오류입니다. 어떤 방법이 켜져 있는지는 config()가 알려 줍니다. runlot 제공자 항목에 ["password", "google", "github"] 같은 methods가 들어 있습니다.

오류: auth_error

로그인이 성공하지 못하면 error_next(없으면 next)로 쿼리 매개변수 두 개와 함께 돌아옵니다.

/login?auth_error=cancelled&auth_request=6f1c…
코드뜻
cancelled뒤로 가거나 제공자 화면을 닫았습니다
signup_closed가입이 꺼져 있고 기존 계정이 없습니다
email_unverified이메일 인증이 필요한데 제공자의 이메일이 인증되지 않았습니다
provider_unavailable로그인 도중 그 방법이 꺼졌습니다
rate_limited시도가 너무 많습니다
expired로그인 요청이 오래되었습니다
consent_denied동의하지 않았습니다
login_requiredprompt=none인데 쓸 수 있는 세션이 없습니다
account_exists이메일로 계정 연결이 꺼져 있고 그 이메일은 다른 계정의 것입니다
server_error그 밖의 경우입니다. 고객 지원은 auth_request로 찾습니다

코드에는 이메일, 제공자의 메시지, 스택이 담기지 않습니다. @runlot/auth는 닫힌 코드 목록과 한국어·영어·일본어 기본 문구를 제공합니다.

import { authErrorMessage, parseAuthError, stripAuthError } from "@runlot/auth/client";

const err = parseAuthError(location.href); // { code, request } | null
if (err) {
  show(authErrorMessage(err.code, "ko"));
  stripAuthError(); // 새로고침해도 다시 보이지 않도록 auth_error/auth_request를 지웁니다
}

모르는 코드는 server_error로 읽습니다.

<runlot-sign-in>

의존성이 없는 사용자 정의 HTML 요소(custom element)입니다. 켜진 Runlot 로그인 방법을 링크로 그리고, auth_error 문구를 보여 준 뒤 주소창에서 지웁니다. gzip으로 약 4 KiB입니다.

<script type="module">import "@runlot/auth/ui";</script>
<runlot-sign-in next="/chat" lang="ko" methods="google github password" guest error-next="/login"></runlot-sign-in>
속성
next, error-next, login-hint모든 링크에 전달합니다
langko(기본), en, ja
methods그릴 버튼을 공백으로 구분합니다. runlot은 일반 선택 화면 버튼입니다. 기본값은 프로젝트가 제공하는 방법이고, 프로젝트가 끈 방법은 그리지 않습니다
guestrunlot-guest 이벤트를 보내는 게스트 버튼을 추가합니다

모든 요소에 part가 있어 밖에서 꾸밀 수 있습니다. runlot-sign-in::part(button-google), ::part(error), ::part(root). 색, 모서리, 폰트는 CSS 사용자 지정 속성으로 바꿉니다. --runlot-brand, --runlot-brand-text, --runlot-bg, --runlot-border, --runlot-radius, --runlot-font, --runlot-gap, --runlot-error. header와 footer 슬롯에 직접 만든 마크업을 넣을 수 있습니다.

React:

import { RunlotSignIn } from "@runlot/auth/react";

<RunlotSignIn next="/chat" lang="ko" methods={["google", "github"]} guest onGuest={() => enterAsGuest()} />

프로젝트 전용 방법의 JSON API

입력 폼을 앱 페이지 안에 두고 싶다면 프로젝트의 비밀번호·이메일 링크 방법을 JSON으로 씁니다. 이 계정은 이 프로젝트에만 속하며 Runlot ID 계정이 아닙니다.

import { signInWithPassword, signUp, requestReset, confirmReset, sendMagicLink } from "@runlot/auth/client";

const r = await signInWithPassword({ email, password });
if (r.ok) location.href = "/";
else show(authErrorMessage(r.error, "ko")); // invalid_credentials, rate_limited, …
경로본문
POST /__runlot/auth/api/password/sign-in{email, password} → 200 {user}와 세션 쿠키
POST /__runlot/auth/api/password/sign-up{email, password, name?}
POST /__runlot/auth/api/password/reset{email} → 항상 202
POST /__runlot/auth/api/password/reset/confirm{token, password}
POST /__runlot/auth/api/magic-link{email, next} → 항상 202

요청은 Content-Type: application/json이어야 하고(아니면 415) 앱의 출처에서 와야 합니다(아니면 403 csrf). 오류 코드는 위의 코드에 invalid_credentials, weak_password, check_email이 더해집니다. 429에는 Retry-After가 붙습니다.

Runlot 화면 꾸미기

Runlot 선택 화면에 앱의 문구, 배치, 색을 입힐 수 있습니다. 모든 항목은 선택이며 기본값은 지금의 화면과 같습니다. 꾸미기는 서버가 검증하는 데이터이며, HTML이나 CSS를 직접 넣을 수는 없습니다.

runlot auth set brand-copy ko.title "세법 원문을 근거로 답합니다"
runlot auth set brand-layout split            # centered | split | minimal
runlot auth set brand-theme dark              # light | dark | system
runlot auth set brand-button pill             # rounded | pill | square
runlot auth set brand-background color "#0b1020"   # none | color | gradient | image
runlot auth set brand-logo-dark https://example.com/logo-dark.svg
runlot auth set brand-link privacy https://example.com/privacy
runlot auth set brand-lang ko                 # ko | en | ja | auto
runlot auth set brand-methods google,github   # all = 쓸 수 있는 방법 전부

문구는 언어마다 제목 80자, 부제목과 하단 문구 200자까지입니다. 링크와 이미지는 https 주소여야 합니다.

전체를 파일로 관리하려면:

runlot auth brand get > branding.json
runlot auth brand apply branding.json

잘못된 파일은 400 invalid_branding과 잘못된 항목의 경로로 거절됩니다. 대시보드의 Sign-in & access 탭에도 같은 입력 폼과 실시간 미리보기가 있습니다.

미리보기

runlot auth preview                       # 꾸민 Runlot 화면을 엽니다
runlot auth preview --lang en --theme dark --state error:cancelled

--state는 password, passkey, error:<code> 중 하나입니다. 실제로 로그인하지 않아도 볼 수 있습니다.

로컬 개발: runlot dev --auth=chooser

runlot dev --auth=chooser

이렇게 실행하면 /__runlot/auth/sign-in/runlot이 꾸민 모양 그대로의 로컬 선택 화면을 보여 줍니다. 테스트 계정을 고르면 로그인된 채로 돌아오고, 오류 버튼을 누르면 해당 auth_error 코드와 함께 돌아옵니다. idp, login_hint, error_next, prompt는 운영 환경과 똑같이 동작합니다. 꾸미기 정보는 API에서 한 번 읽어 .runlot/auth-branding.json에 저장하며, 이 파일을 지우면 다시 읽습니다. 오프라인이면 기본 모양을 씁니다. 이 옵션이 없으면 runlot dev는 지금처럼 고정된 개발용 사용자로 로그인합니다.

계정 연결과 사용자 합치기

기본적으로 Runlot 로그인의 인증된 이메일이 다른 방법으로 가입한 기존 사용자와 같으면 그 사용자에 연결됩니다. 따로 두려면:

runlot auth set link-by-email off

그러면 이런 로그인은 auth_error=account_exists로 돌아옵니다.

나중에 두 사용자를 합치려면(예: 직접 만든 Google 앱에서 Runlot으로 바꾼 뒤):

runlot auth users merge <from-user-id> <into-user-id>

<from>의 모든 로그인 수단을 <into>로 옮기고, <from>을 로그아웃시킨 뒤 삭제합니다. 프로젝트 데이터베이스에서 한 트랜잭션으로 실행합니다. 앱 테이블에서 <from>을 참조하는 행은 직접 고쳐야 합니다. 워커에서는 env.auth.users.merge(from, into)가 같은 일을 합니다.

대시보드의 로그인 탭에는 최근 30일의 로그인 흐름(날짜별 시작, 성공, 오류 코드)도 나오며, runlot auth funnel --days 30은 합계를 출력합니다. 개인 단위 데이터는 저장하지 않습니다.

이 페이지의 목차