mirror of
https://github.com/jnsahaj/tweakcn.git
synced 2026-08-28 23:02:07 +08:00
bb8df47a8c
* OAuth 2.0 API for external app integrations Implements the Authorization Code flow so external apps can authenticate tweakcn users and access their themes/profile via a REST API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: simplify OAuth API routes and improve efficiency - Extract `requireAuth` helper to eliminate duplicated auth+scope boilerplate across all v1 API routes - Narrow SELECT queries to only fetch needed columns in hot paths (resolveUserFromBearerToken, authenticateClient, authorize, token) - Reuse `generateSecureToken`/`hashSecret` in create-oauth-app script instead of duplicating crypto logic - Unify sign-in handlers and loading state in OAuth authorize page - Use `oauthError` consistently for 404 responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * replace demo app with OAuth API documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * add /api/oauth/userinfo endpoint for genericOAuth compatibility Returns flat OIDC-style fields (sub, name, email, picture) so Better Auth's genericOAuth plugin and similar clients work out of the box without custom getUserInfo mapping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
207 lines
4.5 KiB
Markdown
207 lines
4.5 KiB
Markdown
# OAuth 2.0 API
|
|
|
|
tweakcn exposes an OAuth 2.0 API so external apps can authenticate users and access their data.
|
|
|
|
## Registering an app
|
|
|
|
Register an OAuth app via the CLI script:
|
|
|
|
```bash
|
|
npx tsx scripts/create-oauth-app.ts \
|
|
--name "My App" \
|
|
--redirect-uris "https://myapp.com/callback" \
|
|
--scopes "themes:read,profile:read" \
|
|
--description "Optional description"
|
|
```
|
|
|
|
This outputs a `client_id` and `client_secret`. The secret is shown once and cannot be retrieved later.
|
|
|
|
## Authorization flow
|
|
|
|
Standard OAuth 2.0 Authorization Code flow. PKCE is supported but optional.
|
|
|
|
### 1. Redirect the user to authorize
|
|
|
|
```
|
|
GET https://tweakcn.com/api/oauth/authorize
|
|
?client_id=CLIENT_ID
|
|
&redirect_uri=https://myapp.com/callback
|
|
&response_type=code
|
|
&scope=themes:read profile:read
|
|
&state=RANDOM_STRING
|
|
```
|
|
|
|
If the user is not signed in, they'll be shown a sign-in page. After signing in, they're redirected to your `redirect_uri` with an authorization code:
|
|
|
|
```
|
|
https://myapp.com/callback?code=AUTH_CODE&state=RANDOM_STRING
|
|
```
|
|
|
|
### 2. Exchange the code for tokens
|
|
|
|
```bash
|
|
curl -X POST https://tweakcn.com/api/oauth/token \
|
|
-d grant_type=authorization_code \
|
|
-d client_id=CLIENT_ID \
|
|
-d client_secret=CLIENT_SECRET \
|
|
-d code=AUTH_CODE \
|
|
-d redirect_uri=https://myapp.com/callback
|
|
```
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"access_token": "...",
|
|
"refresh_token": "...",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
"scope": "themes:read profile:read"
|
|
}
|
|
```
|
|
|
|
### 3. Call the API
|
|
|
|
Pass the access token as a Bearer token:
|
|
|
|
```bash
|
|
curl https://tweakcn.com/api/v1/themes \
|
|
-H "Authorization: Bearer ACCESS_TOKEN"
|
|
```
|
|
|
|
### 4. Refresh tokens
|
|
|
|
Access tokens expire after 1 hour. Use the refresh token to get a new pair:
|
|
|
|
```bash
|
|
curl -X POST https://tweakcn.com/api/oauth/token \
|
|
-d grant_type=refresh_token \
|
|
-d client_id=CLIENT_ID \
|
|
-d client_secret=CLIENT_SECRET \
|
|
-d refresh_token=REFRESH_TOKEN
|
|
```
|
|
|
|
### 5. Revoke tokens
|
|
|
|
```bash
|
|
curl -X POST https://tweakcn.com/api/oauth/revoke \
|
|
-d token=ACCESS_OR_REFRESH_TOKEN
|
|
```
|
|
|
|
## Using with Better Auth's genericOAuth
|
|
|
|
tweakcn works as a provider with Better Auth's `genericOAuth` plugin:
|
|
|
|
```typescript
|
|
// server
|
|
import { genericOAuth } from "better-auth/plugins";
|
|
|
|
export const auth = betterAuth({
|
|
plugins: [
|
|
genericOAuth({
|
|
config: [
|
|
{
|
|
providerId: "tweakcn",
|
|
clientId: process.env.TWEAKCN_CLIENT_ID,
|
|
clientSecret: process.env.TWEAKCN_CLIENT_SECRET,
|
|
authorizationUrl: "https://tweakcn.com/api/oauth/authorize",
|
|
tokenUrl: "https://tweakcn.com/api/oauth/token",
|
|
userInfoUrl: "https://tweakcn.com/api/oauth/userinfo",
|
|
scopes: ["themes:read", "profile:read"],
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
});
|
|
```
|
|
|
|
```typescript
|
|
// client
|
|
import { genericOAuthClient } from "better-auth/client/plugins";
|
|
|
|
const authClient = createAuthClient({
|
|
plugins: [genericOAuthClient()],
|
|
});
|
|
|
|
await authClient.signIn.oauth2({
|
|
providerId: "tweakcn",
|
|
callbackURL: "/dashboard",
|
|
});
|
|
```
|
|
|
|
## API endpoints
|
|
|
|
All endpoints require `Authorization: Bearer <access_token>`.
|
|
|
|
### `GET /api/oauth/userinfo`
|
|
|
|
OIDC-compatible userinfo endpoint. Returns flat user fields. Requires `profile:read` scope.
|
|
|
|
```json
|
|
{
|
|
"sub": "user_123",
|
|
"name": "Jane Doe",
|
|
"email": "jane@example.com",
|
|
"picture": "https://..."
|
|
}
|
|
```
|
|
|
|
### `GET /api/v1/me`
|
|
|
|
Returns the authenticated user's profile. Requires `profile:read` scope.
|
|
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "...",
|
|
"name": "Jane Doe",
|
|
"email": "jane@example.com",
|
|
"image": "https://..."
|
|
}
|
|
}
|
|
```
|
|
|
|
### `GET /api/v1/themes`
|
|
|
|
Returns all themes owned by the authenticated user. Requires `themes:read` scope.
|
|
|
|
```json
|
|
{
|
|
"data": [
|
|
{
|
|
"id": "...",
|
|
"name": "My Theme",
|
|
"styles": { ... },
|
|
"createdAt": "2025-01-01T00:00:00.000Z",
|
|
"updatedAt": "2025-01-01T00:00:00.000Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### `GET /api/v1/themes/:themeId`
|
|
|
|
Returns a single theme by ID. Only returns themes owned by the authenticated user. Requires `themes:read` scope.
|
|
|
|
## Scopes
|
|
|
|
| Scope | Description |
|
|
|-------|-------------|
|
|
| `themes:read` | Read the user's saved themes |
|
|
| `profile:read` | Read the user's profile (name, email, avatar) |
|
|
|
|
## PKCE support
|
|
|
|
For public clients (e.g. SPAs, mobile apps), use PKCE by adding `code_challenge` and `code_challenge_method=S256` to the authorize request, then `code_verifier` when exchanging the code.
|
|
|
|
## Error responses
|
|
|
|
All error responses follow the OAuth 2.0 spec:
|
|
|
|
```json
|
|
{
|
|
"error": "invalid_token",
|
|
"error_description": "Invalid or expired access token"
|
|
}
|
|
```
|