Files
peertube-plugin-auth-yaknet/main.js
T

426 lines
15 KiB
JavaScript

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const crypto = require('crypto');
const https = require('https');
const http = require('http');
let clientId = '';
let clientSecret = '';
let authBaseUrl = 'https://developer-console.yakhub.com.tr';
let customCallbackUrl = '';
function normalizeAuthUrl(urlStr) {
if (typeof urlStr === 'string' && urlStr.includes('auth.yakhub.com.tr')) {
return urlStr.replace('auth.yakhub.com.tr', 'developer-console.yakhub.com.tr');
}
return urlStr;
}
function postRequest(urlStr, data, maxRedirects = 5) {
return new Promise((resolve, reject) => {
if (maxRedirects <= 0) {
return reject(new Error('Too many redirects while calling YakNet auth server'));
}
const safeUrlStr = normalizeAuthUrl(urlStr);
const url = new URL(safeUrlStr);
const postData = typeof data === 'string' ? data : new URLSearchParams(data).toString();
const lib = url.protocol === 'https:' ? https : http;
const req = lib.request(
{
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + (url.search || ''),
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
'Content-Length': Buffer.byteLength(postData),
'User-Agent': 'YakTube-SSO/1.0'
}
},
res => {
if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
const nextUrl = new URL(res.headers.location, safeUrlStr).toString();
return resolve(postRequest(nextUrl, data, maxRedirects - 1));
}
let body = '';
res.on('data', chunk => (body += chunk));
res.on('end', () => {
try {
resolve({ statusCode: res.statusCode, data: JSON.parse(body) });
} catch (e) {
resolve({ statusCode: res.statusCode, raw: body });
}
});
}
);
req.on('error', reject);
req.write(postData);
req.end();
});
}
function getRequest(urlStr, token, maxRedirects = 5) {
return new Promise((resolve, reject) => {
if (maxRedirects <= 0) {
return reject(new Error('Too many redirects while calling YakNet auth server'));
}
const safeUrlStr = normalizeAuthUrl(urlStr);
const url = new URL(safeUrlStr);
const lib = url.protocol === 'https:' ? https : http;
const req = lib.request(
{
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + (url.search || ''),
method: 'GET',
headers: {
Authorization: 'Bearer ' + token,
Accept: 'application/json',
'User-Agent': 'YakTube-SSO/1.0'
}
},
res => {
if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
const nextUrl = new URL(res.headers.location, safeUrlStr).toString();
return resolve(getRequest(nextUrl, token, maxRedirects - 1));
}
let body = '';
res.on('data', chunk => (body += chunk));
res.on('end', () => {
try {
resolve({ statusCode: res.statusCode, data: JSON.parse(body) });
} catch (e) {
resolve({ statusCode: res.statusCode, raw: body });
}
});
}
);
req.on('error', reject);
req.end();
});
}
async function register({ registerExternalAuth, registerSetting, settingsManager, getRouter, peertubeHelpers }) {
const logger = peertubeHelpers.logger;
const webserverUrl = (peertubeHelpers.config.getWebserverUrl() || '').replace(/\/+$/, '');
const isOfficialYakTube = webserverUrl.includes('yaktube.yakhub.com.tr');
// Standard PeerTube plugin router path uses short name (/plugins/auth-yaknet/router/auth-callback)
const defaultCallbackUrl = isOfficialYakTube
? `${webserverUrl}/plugins/peertube-plugin-auth-yaknet/router/auth-callback`
: `${webserverUrl}/plugins/auth-yaknet/router/auth-callback`;
registerSetting({
name: 'client-id',
label: 'YakNet Client ID',
type: 'input',
description: 'Your YakNet OAuth2 Client ID',
private: false,
default: ''
});
registerSetting({
name: 'client-secret',
label: 'YakNet Client Secret',
type: 'input-password',
description: 'Your YakNet OAuth2 Client Secret',
private: true,
default: ''
});
registerSetting({
name: 'auth-base-url',
label: 'YakNet Auth URL',
type: 'input',
description: 'YakNet SSO Server URL (Default: https://developer-console.yakhub.com.tr)',
private: false,
default: 'https://developer-console.yakhub.com.tr'
});
registerSetting({
name: 'callback-url',
label: 'OAuth Callback (Redirect) URL (Opsiyonel)',
type: 'input',
description: `Boş bırakılırsa otomatik olarak "${defaultCallbackUrl}" kullanılır. YakNet Geliştirici Konsolu'nda tanımladığınız Redirect URI adresini buraya yazabilirsiniz.`,
private: false,
default: ''
});
registerSetting({
name: 'auto-redirect-login',
label: "Giriş Sayfasında Doğrudan YakNet SSO'ya Yönlendir",
type: 'input-checkbox',
description:
'Aktif olduğunda, kullanıcılar giriş butonuna veya /login sayfasına gittiğinde standart PeerTube şifre formu yerine doğrudan YakNet SSO sunucusuna yönlendirilir.',
private: false,
default: true
});
let autoRedirectLogin = true;
function syncConfigFiles(enable) {
const fs = require('fs');
const path = require('path');
// Update in-memory PeerTube ServerConfig if accessible
try {
const srvCfg = peertubeHelpers.config.getServerConfig();
if (srvCfg && srvCfg.client && srvCfg.client.menu && srvCfg.client.menu.login) {
srvCfg.client.menu.login.redirectOnSingleExternalAuth = enable;
}
} catch (e) {}
// 1. Update local-production.json if present
const jsonCandidates = [
path.resolve(process.cwd(), 'config', 'local-production.json'),
path.resolve(__dirname, '..', '..', 'config', 'local-production.json'),
path.resolve(__dirname, '..', '..', '..', 'config', 'local-production.json'),
'c:/laragon/www/yaktube.yakhub.com.tr/config/local-production.json'
];
for (const jPath of jsonCandidates) {
if (fs.existsSync(jPath)) {
try {
const cfg = JSON.parse(fs.readFileSync(jPath, 'utf8'));
if (!cfg.menu) cfg.menu = {};
if (!cfg.menu.login) cfg.menu.login = {};
if (cfg.menu.login.redirect_on_single_external_auth !== enable) {
cfg.menu.login.redirect_on_single_external_auth = enable;
fs.writeFileSync(jPath, JSON.stringify(cfg, null, 2), 'utf8');
logger.info(`[YakNet SSO] Synced local-production.json redirect_on_single_external_auth = ${enable}`);
}
} catch (e) {
logger.warn('[YakNet SSO] Error updating local-production.json:', e.message);
}
break;
}
}
// 2. Update production.yaml if present
const yamlCandidates = [
path.resolve(process.cwd(), 'config', 'production.yaml'),
path.resolve(__dirname, '..', '..', 'config', 'production.yaml'),
path.resolve(__dirname, '..', '..', '..', 'config', 'production.yaml'),
'c:/laragon/www/yaktube.yakhub.com.tr/config/production.yaml'
];
for (const yPath of yamlCandidates) {
if (fs.existsSync(yPath)) {
try {
let yContent = fs.readFileSync(yPath, 'utf8');
const pattern = /(redirect_on_single_external_auth:\s*)(true|false)/;
if (pattern.test(yContent)) {
const updated = yContent.replace(pattern, `$1${enable}`);
if (updated !== yContent) {
fs.writeFileSync(yPath, updated, 'utf8');
logger.info(`[YakNet SSO] Synced production.yaml redirect_on_single_external_auth = ${enable}`);
}
}
} catch (e) {
logger.warn('[YakNet SSO] Error updating production.yaml:', e.message);
}
break;
}
}
}
async function loadSettings() {
const cid = await settingsManager.getSetting('client-id');
const csec = await settingsManager.getSetting('client-secret');
const burl = await settingsManager.getSetting('auth-base-url');
const cburl = await settingsManager.getSetting('callback-url');
const autoRedir = await settingsManager.getSetting('auto-redirect-login');
if (cid) clientId = String(cid).trim();
if (csec) clientSecret = String(csec).trim();
if (burl && String(burl).trim()) {
authBaseUrl = normalizeAuthUrl(String(burl).trim().replace(/\/+$/, ''));
}
customCallbackUrl = cburl ? String(cburl).trim() : '';
if (autoRedir !== undefined && autoRedir !== null) {
autoRedirectLogin = autoRedir === true || autoRedir === 'true';
} else {
autoRedirectLogin = true;
}
syncConfigFiles(autoRedirectLogin);
}
await loadSettings();
settingsManager.onSettingsChange(loadSettings);
function getActiveCallbackUrl() {
return customCallbackUrl || defaultCallbackUrl;
}
async function dbQuery(sql, bindParams = []) {
if (!peertubeHelpers || !peertubeHelpers.database || typeof peertubeHelpers.database.query !== 'function') {
return [];
}
const res = await peertubeHelpers.database.query(sql, { bind: bindParams });
if (Array.isArray(res) && Array.isArray(res[0])) return res[0];
return Array.isArray(res) ? res : [];
}
const externalAuth = registerExternalAuth({
authName: 'yaknet',
authDisplayName: () => 'YakNet ile Giriş Yap',
onAuthRequest: (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
const cbUrl = getActiveCallbackUrl();
const authUrl = `${normalizeAuthUrl(authBaseUrl)}/oauth/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(cbUrl)}&response_type=code&scope=&state=${state}`;
return res.redirect(authUrl);
}
});
const router = getRouter();
router.get('/status', (req, res) => {
return res.json({
autoRedirectLogin: autoRedirectLogin !== false,
callbackUrl: getActiveCallbackUrl()
});
});
router.get('/auth', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
const cbUrl = getActiveCallbackUrl();
const authUrl = `${normalizeAuthUrl(authBaseUrl)}/oauth/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(cbUrl)}&response_type=code&scope=&state=${state}`;
return res.redirect(authUrl);
});
async function handleOAuthCallback(req, res) {
await loadSettings();
const code = req.query.code;
const error = req.query.error;
if (error) {
logger.error('YakNet OAuth Error: ' + error);
return res.redirect('/login?externalAuthError=true&error=' + encodeURIComponent(error));
}
if (!code) {
return res.redirect('/login?externalAuthError=true');
}
try {
const targetAuthUrl = normalizeAuthUrl(authBaseUrl);
const cbUrl = getActiveCallbackUrl();
const tokenRes = await postRequest(`${targetAuthUrl}/oauth/token`, {
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
redirect_uri: cbUrl,
code: code
});
if (!tokenRes.data || !tokenRes.data.access_token) {
logger.error('Failed to get access token from YakNet: ' + JSON.stringify(tokenRes));
return res.redirect('/login?externalAuthError=true');
}
const accessToken = tokenRes.data.access_token;
const userRes = await getRequest(`${targetAuthUrl}/api/user`, accessToken);
if (!userRes.data || !userRes.data.email) {
logger.error('Failed to get user profile from YakNet: ' + JSON.stringify(userRes));
return res.redirect('/login?externalAuthError=true');
}
const rawUser = userRes.data;
let username = (rawUser.username || rawUser.email.split('@')[0])
.toLowerCase()
.replace(/[^a-z0-9_.]/g, '_')
.substring(0, 50);
if (username.length < 3) {
username = username + '_yak';
}
const displayName = rawUser.name || rawUser.username || username;
const email = rawUser.email;
const role =
rawUser.is_admin === 1 ||
rawUser.is_admin === true ||
email === '[email protected]' ||
username === 'enesyakistir' ||
username === 'yaknet' ||
username === 'yaktube'
? 0
: 2;
// Pre-sync with PeerTube DB so existing accounts or username collisions never fail without bridge.js
try {
const existingByEmail = await dbQuery(
`SELECT id, username FROM "user" WHERE LOWER(email) = LOWER($1) LIMIT 1`,
[email]
);
if (existingByEmail.length > 0) {
username = existingByEmail[0].username;
await dbQuery(
`UPDATE "user" SET "pluginAuth" = 'peertube-plugin-auth-yaknet', "emailVerified" = true WHERE id = $1`,
[existingByEmail[0].id]
);
} else {
const existingByUsername = await dbQuery(`SELECT id FROM "user" WHERE LOWER(username) = LOWER($1) LIMIT 1`, [
username
]);
if (existingByUsername.length > 0) {
username = (username.substring(0, 42) + '_' + crypto.randomBytes(2).toString('hex')).toLowerCase();
}
}
} catch (dbErr) {
logger.warn('[YakNet SSO] Pre-sync DB warning: ' + dbErr.message);
}
logger.info(`YakNet Authenticated user: ${username} (${email})`);
externalAuth.userAuthenticated({
req,
res,
username,
email,
displayName,
role
});
setTimeout(async () => {
try {
await dbQuery(
`UPDATE "user" SET "emailVerified" = true, "pluginAuth" = 'peertube-plugin-auth-yaknet' WHERE LOWER(email) = LOWER($1)`,
[email]
);
} catch (e) {}
}, 1000);
} catch (err) {
logger.error('Error processing YakNet auth callback:', err);
return res.redirect('/login?externalAuthError=true');
}
}
router.get('/auth-callback', handleOAuthCallback);
// Also support legacy /plugins/peertube-plugin-auth-yaknet/router/auth-callback on any PeerTube server without bridge.js
try {
if (peertubeHelpers && peertubeHelpers.server && typeof peertubeHelpers.server.getHTTPServer === 'function') {
const httpServer = peertubeHelpers.server.getHTTPServer();
const listeners = httpServer ? httpServer.listeners('request') : [];
const expressApp = listeners && listeners[0];
if (expressApp && typeof expressApp.get === 'function') {
expressApp.get('/plugins/peertube-plugin-auth-yaknet/router/auth-callback', (req, res) => {
return handleOAuthCallback(req, res);
});
}
}
} catch (e) {}
logger.info('YakNet SSO Plugin initialized with Callback URL: ' + getActiveCallbackUrl());
}
async function unregister() {
return true;
}
module.exports = {
register,
unregister
};