13 Commits
Author SHA1 Message Date
Enes Yakıştır cea68cad17 feat(auth): preserve and restore last visited page URL after YakNet SSO login (v1.0.12) 2026-10-03 12:13:26 +03:00
Enes Yakıştır 717573aa9d v1.0.11: Standalone multi-instance OAuth callback routing, DB user pre-sync, and externalAuthToken redirect loop fix (no bridge.js needed) 2026-10-03 11:00:14 +03:00
Enes Yakıştır 8e8170a8dc fix(sso): migrate to developer-console.yakhub.com.tr and follow redirects 2026-09-22 18:54:34 +03:00
Enes Yakıştır 4327c510e9 fix(sso): update YakNet Auth URL to developer-console.yakhub.com.tr (v1.0.9) 2026-09-22 06:15:25 +03:00
Enes Yakıştır f40f5ca905 chore(release): v1.0.8 2026-09-06 11:38:42 +03:00
Enes Yakıştır f11c9bdeee fix(client): use correct router path /plugins/auth-yaknet/router/ 2026-09-06 11:37:57 +03:00
Enes Yakıştır 8c1eb882a3 feat: add auto-redirect-login setting with dynamic config synchronization (v1.0.7) 2026-09-06 11:35:45 +03:00
Enes Yakıştır 0b4a25f684 Release v1.0.6 (formatted codebase) 2026-09-04 12:56:38 +03:00
Enes Yakıştır d890d60dad Format codebase with Prettier 2026-09-04 12:53:55 +03:00
Enes Yakıştır 9d12320035 Auto-verify email in PeerTube DB upon YakNet SSO login 2026-09-04 08:39:02 +03:00
Enes Yakıştır a23a151618 chore(release): v1.0.4 2026-09-04 07:10:38 +03:00
Enes Yakıştır c141d4232e fix(client): registerClient function without module.exports in browser 2026-09-04 07:07:55 +03:00
Enes Yakıştır 688530d8e5 style: format package.json with Prettier 2026-08-31 14:35:51 +03:00
6 changed files with 346 additions and 29 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
package-lock.json
pnpm-lock.yaml
+9
View File
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 120,
"arrowParens": "avoid",
"endOfLine": "auto"
}
+3 -2
View File
@@ -2,7 +2,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![PeerTube Engine](https://img.shields.io/badge/PeerTube-%3E%3D5.0.0-orange.svg)](https://joinpeertube.org)
[![YakNet](https://img.shields.io/badge/YakNet-Federated%20Identity-blue.svg)](https://auth.yakhub.com.tr)
[![YakNet](https://img.shields.io/badge/YakNet-Federated%20Identity-blue.svg)](https://developer-console.yakhub.com.tr)
**English** | [Türkçe](#türkçe)
@@ -43,7 +43,8 @@ In your PeerTube admin panel (**Administration > Plugins / Themes > Settings** f
- **Client ID:** Your registered YakNet OAuth2 Application Client ID.
- **Client Secret:** Your YakNet OAuth2 Client Secret.
- **Auth Base URL:** Default is `https://auth.yakhub.com.tr`.
- **Auth Base URL:** Default is `https://developer-console.yakhub.com.tr`.
- **Auto-Redirect on Login:** Automatically bypass the standard login form and redirect users directly to YakNet SSO (also synchronizes `redirect_on_single_external_auth` in PeerTube configuration).
---
+107 -3
View File
@@ -1,4 +1,108 @@
function register() {
console.log('[YakNet Auth] Client script loaded');
async function register({ registerHook, peertubeHelpers }) {
try {
function saveReturnUrlBeforeLogin() {
try {
var currentPath = window.location.pathname + window.location.search + window.location.hash;
if (currentPath && !window.location.pathname.includes('/login')) {
localStorage.setItem('yaktube_return_url', currentPath);
localStorage.setItem('yaktube_return_url_time', String(Date.now()));
sessionStorage.setItem('yaktube_return_url', currentPath);
sessionStorage.setItem('redirect-url-after-login', currentPath);
}
} catch (e) {}
}
function checkPendingPostLoginRedirect() {
try {
if (window.location.pathname.includes('/login')) return;
var token = localStorage.getItem('access_token');
if (!token) return;
var savedUrl =
localStorage.getItem('yaktube_return_url') ||
sessionStorage.getItem('yaktube_return_url') ||
sessionStorage.getItem('redirect-url-after-login');
if (!savedUrl) return;
var savedTime = parseInt(localStorage.getItem('yaktube_return_url_time') || '0', 10);
localStorage.removeItem('yaktube_return_url');
localStorage.removeItem('yaktube_return_url_time');
sessionStorage.removeItem('yaktube_return_url');
sessionStorage.removeItem('redirect-url-after-login');
if (savedTime && Date.now() - savedTime > 15 * 60 * 1000) return;
var currentFull = window.location.pathname + window.location.search + window.location.hash;
if (
typeof savedUrl === 'string' &&
savedUrl.startsWith('/') &&
!savedUrl.startsWith('//') &&
!savedUrl.startsWith('/login') &&
savedUrl !== currentFull
) {
window.location.replace(savedUrl);
}
} catch (e) {}
}
checkPendingPostLoginRedirect();
window.addEventListener('popstate', checkPendingPostLoginRedirect);
setInterval(checkPendingPostLoginRedirect, 800);
fetch('/plugins/auth-yaknet/router/status')
.then(function (r) {
return r.json();
})
.then(function (status) {
if (!status || status.autoRedirectLogin === false) {
return; // Auto redirect is disabled by administrator
}
var isLoginUrl = window.location.pathname.includes('/login');
var isLocal = window.location.search.includes('local=true');
var isError = window.location.search.includes('externalAuthError');
var isExternalToken = window.location.search.includes('externalAuthToken');
// CRITICAL: Never intercept /login when PeerTube is completing the externalAuthToken handshake!
if (isLoginUrl && !isLocal && !isError && !isExternalToken) {
window.location.replace('/plugins/auth-yaknet/router/auth');
return;
}
// Intercept clicks on login links/buttons
document.addEventListener(
'click',
function (e) {
var el = e.target;
if (el && el.closest) {
var isLoginBtn = el.closest(
'my-login-link, .login-button, a[href*="/login"]:not(#yaktube-show-admin-login), a[aria-label*="Giriş"], a[title*="Giriş"], a[title*="login"], a[aria-label*="login"]'
);
if (
isLoginBtn &&
!window.location.search.includes('local=true') &&
!window.location.search.includes('externalAuthToken')
) {
e.preventDefault();
e.stopPropagation();
saveReturnUrlBeforeLogin();
window.location.href = '/plugins/auth-yaknet/router/auth';
}
}
},
true
);
})
.catch(function () {});
// Alt+A shortcut for local admin login bypass
document.addEventListener('keydown', function (e) {
if (e.altKey && (e.key === 'a' || e.key === 'A')) {
if (!window.location.pathname.includes('/login')) {
window.location.href = '/login?local=true';
}
}
});
} catch (err) {
console.warn('[YakNet SSO] Client init error:', err);
}
}
module.exports = { register };
export { register, register as registerClient };
export default register;
+221 -22
View File
@@ -4,11 +4,23 @@ const http = require('http');
let clientId = '';
let clientSecret = '';
let authBaseUrl = 'https://auth.yakhub.com.tr';
let authBaseUrl = 'https://developer-console.yakhub.com.tr';
let customCallbackUrl = '';
function postRequest(urlStr, data) {
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) => {
const url = new URL(urlStr);
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;
@@ -26,6 +38,11 @@ function postRequest(urlStr, data) {
}
},
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', () => {
@@ -44,9 +61,13 @@ function postRequest(urlStr, data) {
});
}
function getRequest(urlStr, token) {
function getRequest(urlStr, token, maxRedirects = 5) {
return new Promise((resolve, reject) => {
const url = new URL(urlStr);
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(
@@ -62,6 +83,11 @@ function getRequest(urlStr, token) {
}
},
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', () => {
@@ -81,6 +107,12 @@ function getRequest(urlStr, token) {
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',
@@ -104,44 +136,160 @@ async function register({ registerExternalAuth, registerSetting, settingsManager
name: 'auth-base-url',
label: 'YakNet Auth URL',
type: 'input',
description: 'YakNet SSO Server URL (Default: https://auth.yakhub.com.tr)',
description: 'YakNet SSO Server URL (Default: https://developer-console.yakhub.com.tr)',
private: false,
default: 'https://auth.yakhub.com.tr'
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');
if (cid) clientId = cid;
if (csec) clientSecret = csec;
if (burl) authBaseUrl = burl.replace(/\/+$/, '');
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);
const webserverUrl = peertubeHelpers.config.getWebserverUrl();
const callbackUrl = `${webserverUrl}/plugins/peertube-plugin-auth-yaknet/router/auth-callback`;
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 authUrl = `${authBaseUrl}/oauth/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(callbackUrl)}&response_type=code&scope=&state=${state}`;
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 authUrl = `${authBaseUrl}/oauth/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(callbackUrl)}&response_type=code&scope=&state=${state}`;
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);
});
router.get('/auth-callback', async (req, res) => {
async function handleOAuthCallback(req, res) {
await loadSettings();
const code = req.query.code;
const error = req.query.error;
@@ -155,23 +303,25 @@ async function register({ registerExternalAuth, registerSetting, settingsManager
}
try {
const tokenRes = await postRequest(`${authBaseUrl}/oauth/token`, {
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: callbackUrl,
redirect_uri: cbUrl,
code: code
});
if (!tokenRes.data || !tokenRes.data.access_token) {
logger.error('Failed to get access token from YakNet:', tokenRes);
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(`${authBaseUrl}/api/user`, accessToken);
const userRes = await getRequest(`${targetAuthUrl}/api/user`, accessToken);
if (!userRes.data || !userRes.data.email) {
logger.error('Failed to get user profile from YakNet:', userRes);
logger.error('Failed to get user profile from YakNet: ' + JSON.stringify(userRes));
return res.redirect('/login?externalAuthError=true');
}
@@ -197,6 +347,30 @@ async function register({ registerExternalAuth, registerSetting, settingsManager
? 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({
@@ -207,13 +381,38 @@ async function register({ registerExternalAuth, registerSetting, settingsManager
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');
}
});
}
logger.info('YakNet SSO Plugin initialized with Callback URL: ' + callbackUrl);
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() {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "peertube-plugin-auth-yaknet",
"version": "1.0.3",
"version": "1.0.12",
"description": "YakNet SSO & OAuth2 Single Sign-On Authentication Plugin for PeerTube",
"main": "main.js",
"library": "./main.js",
@@ -40,4 +40,4 @@
"homepage": "https://yaktube.yakhub.com.tr",
"bugs": "https://github.com/yakistir98/peertube-plugin-auth-yaknet/issues",
"license": "MIT"
}
}