UNPKG

verdaccio-openid

Version:

A UI for OIDC authentication for Verdaccio, a fork of verdaccio-github-oauth-ui

292 lines (281 loc) 8.49 kB
#!/usr/bin/env node import process from 'node:process'; import express from 'express'; import open from 'open'; import colors from 'picocolors'; import { execFileSync } from 'node:child_process'; import { URL } from 'node:url'; import minimist from 'minimist'; var name = "verdaccio-openid"; var version = "0.17.2"; const plugin = { name, version }; const pluginKey = name.replace("verdaccio-", ""); const authorizePath = "/-/oauth/authorize"; const cliPort = 8239; const cliProviderId = "cli"; const messageGroupRequired = "You are not a member of the required access group."; const messageLoggedAndCloseWindow = "You have logged in successfully and may close this window."; function getAuthorizePath(id) { return `${authorizePath}${`/${id}` }`; } const prefix = colors.blue(`[${pluginKey}]`); const logger = { info: console.log.bind(console, prefix), success: (...args) => console.log(prefix, colors.green(args.join(" "))), warn: (...args) => console.warn(prefix, colors.yellow(args.join(" "))), error: (...args) => console.error(prefix, colors.red(args.join(" "))) }; logger.info(`Version: ${plugin.name}@${plugin.version}`); const PUBLIC_REGISTRIES = ["registry.npmjs.org", "registry.npmmirror.com", "registry.npm.taobao.org"]; let npmConfig; function parseCliArgs() { return minimist(process.argv.slice(2)); } function runCommand(command, args = [], logCommand = true) { if (logCommand) { const displayCommand = typeof logCommand === "string" ? logCommand : [command, ...args].join(" "); logger.info("Running command:", colors.blackBright(displayCommand)); } return execFileSync(command, args, { encoding: "utf8" }); } function getNpmConfig() { if (!npmConfig) { const npmConfigJson = runCommand("npm", ["config", "list", "--json"], false); npmConfig = JSON.parse(npmConfigJson); } return npmConfig; } function removeTrailingSlash(input) { return input.trim().replace(/\/?$/, ""); } function getRegistryUrl() { const cliArgs = parseCliArgs(); const registry = cliArgs.registry ?? getNpmConfig().registry; if (!registry) { return PUBLIC_REGISTRIES[0]; } return removeTrailingSlash(registry); } function getNpmConfigFile() { return getNpmConfig().userconfig; } function saveNpmToken(token) { const registry = getRegistryUrl(); const url = new URL(registry); let baseUrl = `${url.host}${url.pathname}`; if (!baseUrl.endsWith("/")) { baseUrl = `${baseUrl}/`; } const key = `//${baseUrl}:_authToken`; runCommand("npm", ["config", "set", key, token], `npm config set ${key} "<token>"`); } var img = "data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 57 49' width='57' height='49'%3e %3cg fill='none' stroke-width='2.4'%3e %3cpath fill='%23405236' stroke='%23405236' d='M46.06 18.8H33.54L28.4 29.08 14.86 2H2.34l22.4 44.8h7.32l14-28Z'/%3e %3cpath fill='%234A5E3F' stroke='%23405236' d='m32.06 46.8 2.58-5.11L14.86 2H2.34l22.4 44.8h7.32Z'/%3e %3cpath fill='%23CD4000' stroke='%23CD4000' d='m50.06 10.8 4.4-8.8H41.94l-4.4 8.8h12.52Z'/%3e %3cg stroke='%23CD4000' stroke-linecap='square'%3e %3cpath d='M37.6 2h15.22M33.6 6h15.22M29.6 10.8h15.22'/%3e %3c/g%3e %3c/g%3e%3c/svg%3e"; const styles = ` html, body { padding: 0; margin: 0; height: 100%; background-color: #e0e0e0; color: #24292F; font-family: Helvetica, sans-serif; position: relative; text-align: center; } .wrap { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .img { filter: drop-shadow(0 0.5rem 0.5rem #24292F80); width: 114px; height: 98px; } h1 { margin: 20px 0 16px 0; font-size: 28px; font-weight: 600; line-height: 1.2; } h1.success { color: #27ae60; } h1.error { color: #e74c3c; } h1.warning { color: #f39c12; } p { font-size: 16px; line-height: 1.5; } .message { background: rgba(255, 255, 255, 0.8); border-radius: 12px; padding: 16px 20px; margin-top: 20px; border-left: 4px solid transparent; } .message.success { border-left-color: #27ae60; background: rgba(39, 174, 96, 0.1); } .message.error { border-left-color: #e74c3c; background: rgba(231, 76, 60, 0.1); } .message.warning { border-left-color: #f39c12; background: rgba(243, 156, 18, 0.1); } .buttons { margin-top: 30px; } .back { color: #3498db; text-decoration: none; font-weight: 500; padding: 10px 16px; background: rgba(52, 152, 219, 0.1); border-radius: 6px; display: inline-block; transition: all 0.3s ease; border: 1px solid rgba(52, 152, 219, 0.2); } .back:hover { background: rgba(52, 152, 219, 0.2); transform: translateY(-2px); box-shadow: 0 4px 8px rgba(52, 152, 219, 0.3); } `; const defaultBackUrl = "javascript:history.back()"; function buildStatusPage(body, withBack = false) { let backUrl; if (typeof withBack === "object") { backUrl = withBack.backUrl || defaultBackUrl; } else if (withBack) { backUrl = defaultBackUrl; } return `<!DOCTYPE html> <html lang="en"> <head> <title>${plugin.name} - ${plugin.version}</title> <style>${styles}</style> </head> <body> <div class="wrap"> <img src="${img}" class="img" alt="logo" /> ${body} ${backUrl ? `<div class="buttons"> <a class="back" href="${backUrl}">Go back</a> </div>` : ""} </div> </body> </html>`; } function buildErrorPage(error, withBack = false) { return buildStatusPage(`<h1 class="error">Sorry :(</h1> <p class="message error">${error?.message ?? error}</p>`, withBack); } function buildSuccessPage(message, withBack = false) { return buildStatusPage(`<h1 class="success">Success!</h1> <p class="message success">${message}</p>`, withBack); } function buildAccessDeniedPage(withBack = false) { return buildStatusPage(`<h1 class="warning">Access Denied.</h1> <p class="message warning">${messageGroupRequired}</p>`, withBack); } const messageSuccess = "All done! We've updated your npm configuration."; function respondWithCliMessage(status, message) { switch (status) { case "success": { logger.success(messageSuccess); logger.info("Path:", colors.blackBright(getNpmConfigFile())); break; } case "denied": { logger.error(messageGroupRequired); break; } default: { logger.warn(message); break; } } } function respondWithWebPage(status, message, res) { res.setHeader("Content-Type", "text/html"); switch (status) { case "success": { res.status(200).send(buildSuccessPage(`${messageSuccess}<br> <code>${getNpmConfigFile()}</code><br> ${messageLoggedAndCloseWindow}`)); break; } case "denied": { res.status(401).send(buildAccessDeniedPage()); break; } default: { res.status(500).send(buildErrorPage(message)); break; } } } function getUsageInfo() { return ["========================= Usage =========================", "It seems you are using the default npm registry.", "Please update it to your Verdaccio URL by either running:", "", "npm config set registry <URL>", "", "Or by using the registry argument:", "", `npx ${plugin.name} --registry <URL>`, "========================================================"]; } function printUsage() { for (const line of getUsageInfo()) { logger.info(line); } } function validateRegistry() { const registry = getRegistryUrl(); if (PUBLIC_REGISTRIES.some(item => registry.includes(item))) { printUsage(); process.exit(1); } return registry; } const registry = validateRegistry(); const authorizeUrl = registry + getAuthorizePath(cliProviderId); const server = express().get("/", (req, res) => { let status = String(req.query.status); let message = String(req.query.message); const token = String(req.query.token); if (status === "success") { try { saveNpmToken(token); } catch (error) { status = "error"; message = error.message; } } respondWithWebPage(status, message, res); respondWithCliMessage(status, message); server.close(); process.exit(status === "success" ? 0 : 1); }).listen(cliPort, () => { logger.info(`Listening on port ${cliPort}...`); logger.info(`Opening ${authorizeUrl} in your browser...`); open(authorizeUrl).catch(() => { logger.error("Failed to open browser window."); logger.warn("Please visit the following URL manually:"); logger.info(authorizeUrl); }); });