Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ BETTER_AUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# GitHub OAuth2 app credentials (github.com → Settings → Developer settings → OAuth Apps)
# Callback URL: {BETTER_AUTH_URL}/v1/auth/callback/github
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

# Must exactly match an authorized redirect URI in your Google OAuth app
# Better Auth callback (for user login): {BETTER_AUTH_URL}/v1/auth/callback/google
# Add this to Google Cloud Console authorized redirect URIs:
Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,24 @@ docker compose --build -f docker-compose.yml -f docker-compose.standalone.yml -f

### Cloud mode

Full production-like stack. Users authenticate via Google OAuth (via Better Auth); deck data
stored in their Google Drive. Requires all OAuth credentials in `.env`.
Full production-like stack. Users authenticate via Google or GitHub OAuth (via Better Auth); deck data
stored in their Google Drive. Requires OAuth credentials in `.env`.

Before running, add these URIs to your Google OAuth app in
[Google Cloud Console](https://console.cloud.google.com) → APIs & Services → Credentials:
Before running, register these callback URIs in your OAuth apps:

**Google** ([Cloud Console](https://console.cloud.google.com) → APIs & Services → Credentials):

```
{BETTER_AUTH_URL}/v1/auth/callback/google # sign-in callback
{FRONTEND_URL}/v1/me/storage/connect/google/callback # Google Drive callback
```

**GitHub** (optional — [Settings → Developer settings → OAuth Apps](https://github.com/settings/developers)):

```
{BETTER_AUTH_URL}/v1/auth/callback/github # sign-in callback
```

Set `TRUSTED_ORIGINS` in `.env` to your frontend URL(s) (comma-separated) so Better Auth accepts
requests from the web UI. Defaults to `FRONTEND_URL` if unset.

Expand Down Expand Up @@ -80,7 +87,7 @@ Authorization: Bearer ak_<your-key>

Generate a key in the web UI under **Account → API Keys**, or via `POST /v1/me/api-keys`.

Account management endpoints (`/v1/me/*`) use the session cookie set by [Better Auth](https://better-auth.com) after Google OAuth login. The auth handler is mounted at `/v1/auth/*`.
Account management endpoints (`/v1/me/*`) use the session cookie set by [Better Auth](https://better-auth.com) after Google or GitHub OAuth login. The auth handler is mounted at `/v1/auth/*`.

---

Expand Down
4 changes: 4 additions & 0 deletions api/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@ export const auth = betterAuth({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
});
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ services:
BETTER_AUTH_URL: ${BETTER_AUTH_URL}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
GOOGLE_DRIVE_REDIRECT_URI: ${GOOGLE_DRIVE_REDIRECT_URI}
FRONTEND_URL: ${FRONTEND_URL}
TRUSTED_ORIGINS: ${TRUSTED_ORIGINS:-}
Expand Down
32 changes: 26 additions & 6 deletions docs/SELF_HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ Get your own sync server running in under 10 minutes. All you need is Docker and

- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (or Docker Engine + Compose)
- A Google account (for OAuth login and Google Drive storage)
- A GitHub account (optional — if you want GitHub sign-in)

No other tools required.

---

## Step 1 — Create a Google OAuth app
## Step 1 — Create OAuth app(s)

### Google (required — for sign-in and Google Drive storage)

You need a Google OAuth 2.0 app to handle sign-in and Google Drive access.

Expand All @@ -34,6 +37,16 @@ You need a Google OAuth 2.0 app to handle sign-in and Google Drive access.
```
5. Copy the **Client ID** and **Client Secret** — you'll need them in the next step.

### GitHub (optional — for GitHub sign-in)

1. Go to [GitHub Settings → Developer settings → OAuth Apps](https://github.com/settings/developers) and click **New OAuth App**.
2. Fill in **Application name** (e.g. `anki-cloud`), **Homepage URL** (e.g. `http://localhost:3000`), and set the **Authorization callback URL** to:
```
http://localhost:3000/v1/auth/callback/github
```
3. Click **Register application**, then click **Generate a new client secret**.
4. Copy the **Client ID** and **Client Secret** — you'll need them in the next step.

---

## Step 2 — Clone and configure
Expand All @@ -56,9 +69,13 @@ TOKEN_ENCRYPTION_KEY=<generated>
# Public base URL of the API server — used by Better Auth for OAuth callbacks
BETTER_AUTH_URL=http://localhost:3000

# From Step 1
GOOGLE_CLIENT_ID=<your-client-id>
GOOGLE_CLIENT_SECRET=<your-client-secret>
# From Step 1 — Google
GOOGLE_CLIENT_ID=<your-google-client-id>
GOOGLE_CLIENT_SECRET=<your-google-client-secret>

# From Step 1 — GitHub (optional)
GITHUB_CLIENT_ID=<your-github-client-id>
GITHUB_CLIENT_SECRET=<your-github-client-secret>

# Must match the redirect URI you registered in Google Cloud Console
GOOGLE_DRIVE_REDIRECT_URI=http://localhost:5173/v1/me/storage/connect/google/callback
Expand Down Expand Up @@ -92,7 +109,7 @@ Once running:
## Step 4 — Sign in and connect Google Drive

1. Open `http://localhost:5173` in your browser.
2. Click **Sign in with Google** and complete the OAuth flow.
2. Click **Continue with Google** or **Continue with GitHub** and complete the OAuth flow.
3. Once signed in, click **Connect Google Drive** under Storage.
4. Authorize the Drive access — scope is `drive.file` (only files this app creates).
5. A `/AnkiCloudSync` folder is created in your Drive. Your deck data will live here.
Expand Down Expand Up @@ -135,10 +152,13 @@ docker compose -f docker-compose.yml -f docker-compose.cloud.yml up

## Troubleshooting

**OAuth redirect mismatch error**
**OAuth redirect mismatch error (Google)**
Verify the redirect URIs in Google Cloud Console exactly match those derived from `BETTER_AUTH_URL` and `GOOGLE_DRIVE_REDIRECT_URI` in your `.env`. For local dev: `http://localhost:3000/v1/auth/callback/google` (sign-in)
and `http://localhost:5173/v1/me/storage/connect/google/callback` (Drive). Trailing slashes and `http` vs `https` matter.

**OAuth redirect mismatch error (GitHub)**
Verify the Authorization callback URL in your GitHub OAuth App exactly matches `{BETTER_AUTH_URL}/v1/auth/callback/github`. For local dev: `http://localhost:3000/v1/auth/callback/github`. Leave `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` empty in `.env` to disable the GitHub button entirely.

**Anki says "sync server not configured"**
Ensure the sync URL in Anki is `http://localhost:8080` (no trailing slash) and the stack is running.

Expand Down
21 changes: 21 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ function LoginPage() {
await authClient.signIn.social({provider: "google", callbackURL: window.location.origin});
};

const handleGitHubLogin = async () => {
await authClient.signIn.social({provider: "github", callbackURL: window.location.origin});
};

return (
<div className="login-page">
<div className="login-card">
Expand All @@ -145,6 +149,10 @@ function LoginPage() {
<GoogleIcon/>
Continue with Google
</button>
<button onClick={handleGitHubLogin} className="btn-github">
<GitHubIcon/>
Continue with GitHub
</button>
</div>
</div>
);
Expand Down Expand Up @@ -503,6 +511,19 @@ function SyncPasswordSection({
);
}

// ── GitHub Icon ──────────────────────────────────────────────────────────────

function GitHubIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 1C5.923 1 1 5.923 1 12c0 4.867 3.149 8.979 7.521 10.436.55.096.756-.234.756-.521 0-.258-.009-.943-.014-1.85-3.06.664-3.706-1.475-3.706-1.475-.501-1.271-1.221-1.609-1.221-1.609-.999-.682.075-.668.075-.668 1.104.077 1.685 1.133 1.685 1.133.982 1.682 2.576 1.196 3.202.914.1-.71.384-1.196.699-1.47-2.443-.277-5.012-1.221-5.012-5.437 0-1.202.429-2.183 1.133-2.954-.113-.278-.491-1.397.108-2.91 0 0 .924-.296 3.028 1.129A10.563 10.563 0 0 1 12 6.792c.937.004 1.881.127 2.762.373 2.102-1.425 3.025-1.129 3.025-1.129.6 1.513.222 2.632.109 2.91.705.771 1.132 1.752 1.132 2.954 0 4.226-2.573 5.157-5.023 5.428.394.341.746 1.011.746 2.037 0 1.47-.014 2.657-.014 3.018 0 .289.203.622.761.517C19.854 20.975 23 16.866 23 12c0-6.077-4.923-11-11-11z"
fill="#ffffff"
/>
</svg>
);
}

// ── Google Icon ───────────────────────────────────────────────────────────────

function GoogleIcon() {
Expand Down
23 changes: 23 additions & 0 deletions web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,29 @@ body {
background: #f9fafb;
}

.btn-github {
display: flex;
align-items: center;
gap: 10px;
justify-content: center;
width: 100%;
padding: 11px 20px;
background: #24292f;
border: 1px solid #24292f;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: #ffffff;
cursor: pointer;
text-decoration: none;
transition: background 0.1s;
margin-top: 10px;
}

.btn-github:hover {
background: #32383f;
}

/* ── Storage Section ─────────────────────────────────────────────────────── */

.storage-row {
Expand Down
Loading