113 lines
2.7 KiB
TypeScript
113 lines
2.7 KiB
TypeScript
import crypto from 'crypto';
|
|
import { env } from '../config/env.js';
|
|
|
|
interface GiteaUser {
|
|
id: number;
|
|
login: string;
|
|
email: string;
|
|
full_name: string;
|
|
avatar_url: string;
|
|
}
|
|
|
|
interface OAuthTokenResponse {
|
|
access_token: string;
|
|
token_type: string;
|
|
expires_in: number;
|
|
refresh_token?: string;
|
|
}
|
|
|
|
export class GiteaService {
|
|
private giteaUrl: string;
|
|
private clientId: string;
|
|
private clientSecret: string;
|
|
private redirectUri: string;
|
|
private allowedUsers: Set<string>;
|
|
|
|
constructor() {
|
|
this.giteaUrl = env.GITEA_URL;
|
|
this.clientId = env.GITEA_CLIENT_ID;
|
|
this.clientSecret = env.GITEA_CLIENT_SECRET;
|
|
this.redirectUri = env.GITEA_REDIRECT_URI;
|
|
|
|
const allowed = env.GITEA_ALLOWED_USERS;
|
|
this.allowedUsers = new Set(allowed.split(',').map(u => u.trim()).filter(Boolean));
|
|
}
|
|
|
|
/**
|
|
* Generate OAuth authorization URL
|
|
*/
|
|
getAuthorizationUrl(state: string): string {
|
|
const params = new URLSearchParams({
|
|
client_id: this.clientId,
|
|
redirect_uri: this.redirectUri,
|
|
response_type: 'code',
|
|
state,
|
|
scope: 'read:user',
|
|
});
|
|
|
|
return `${this.giteaUrl}/login/oauth/authorize?${params.toString()}`;
|
|
}
|
|
|
|
/**
|
|
* Exchange authorization code for access token
|
|
*/
|
|
async exchangeCodeForToken(code: string): Promise<OAuthTokenResponse> {
|
|
const response = await fetch(`${this.giteaUrl}/login/oauth/access_token`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
client_id: this.clientId,
|
|
client_secret: this.clientSecret,
|
|
code,
|
|
redirect_uri: this.redirectUri,
|
|
grant_type: 'authorization_code',
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to exchange code: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
/**
|
|
* Fetch user info from Gitea using access token
|
|
*/
|
|
async getUserInfo(accessToken: string): Promise<GiteaUser> {
|
|
const response = await fetch(`${this.giteaUrl}/api/v1/user`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
'Accept': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch user info: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
/**
|
|
* Check if user is allowed to access the CMS
|
|
*/
|
|
isUserAllowed(username: string): boolean {
|
|
// If no allowed users specified, allow all
|
|
if (this.allowedUsers.size === 0) {
|
|
return true;
|
|
}
|
|
return this.allowedUsers.has(username);
|
|
}
|
|
|
|
/**
|
|
* Generate random state for CSRF protection
|
|
*/
|
|
generateState(): string {
|
|
return crypto.randomBytes(32).toString('hex');
|
|
}
|
|
}
|