UNPKG

verdaccio-openid

Version:

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

226 lines (215 loc) 7 kB
#!/usr/bin/env node import process from 'node:process'; import express from 'express'; import open from 'open'; import colors from 'picocolors'; import { execSync } from 'node:child_process'; import { URL } from 'node:url'; import minimist from 'minimist'; var name = "verdaccio-openid"; var version = "0.14.1"; 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, logCommand = true) { if (logCommand) { logger.info("Running command:", colors.blackBright(typeof logCommand === "string" ? logCommand : command)); } return execSync(command).toString(); } 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 getNpmSaveCommand(registry, token) { const url = new URL(registry); let baseUrl = `${url.host}${url.pathname}`; if (!baseUrl.endsWith("/")) { baseUrl = `${baseUrl}/`; } return `npm config set //${baseUrl}:_authToken "${token}"`; } function saveNpmToken(token) { const registry = getRegistryUrl(); const command = getNpmSaveCommand(registry, token); runCommand(command, command.replace(/".*"/, '"<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%); } a { color: #3f51b5; } .img { filter: drop-shadow(0 0.5rem 0.5rem #24292F80); width: 114px; height: 98px; } `; function buildStatusPage(body, withBack = false) { 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} ${withBack ? `<p><a href="${typeof withBack === "object" ? withBack.backUrl : "javascript:history.back()"}">Go back</a></p>` : ""} </div> </body> </html>`; } function buildErrorPage(error, withBack = false) { return buildStatusPage(`<h1>Sorry :(</h1> <p>${error?.message ?? error}</p>`, withBack); } function buildAccessDeniedPage(withBack = false) { return buildStatusPage(`<h1>Access Denied</h1> <p>${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(buildStatusPage(`<h1>Success!</h1> <p>${messageSuccess}</p> <p><code>${getNpmConfigFile()}</code></p> <p>${messageLoggedAndCloseWindow}</p>`)); 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); }); });