verdaccio-openid
Version:
A UI for OIDC authentication for Verdaccio, a fork of verdaccio-github-oauth-ui
292 lines (281 loc) • 8.49 kB
JavaScript
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);
});
});