Skip to content

Commit

Permalink
feat: allow to use passwd login along with OpenID login
Browse files Browse the repository at this point in the history
  • Loading branch information
kuoruan committed Jan 14, 2025
1 parent 42f95d6 commit 9ba22d6
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 25 deletions.
5 changes: 5 additions & 0 deletions src/client/plugin/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import type { TemplateUIOptions } from "@verdaccio/types";

export {};

interface OpenIDOptions {
keepPasswdLogin: boolean;
}

declare global {
interface MouseEvent {
// IE and Edge have a `path` property instead of `composedPath()`.
Expand All @@ -12,5 +16,6 @@ declare global {
interface Window {
VERDACCIO_API_URL?: string;
__VERDACCIO_BASENAME_UI_OPTIONS?: TemplateUIOptions;
__VERDACCIO_OPENID_OPTIONS?: OpenIDOptions;
}
}
57 changes: 44 additions & 13 deletions src/client/plugin/init.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { loginHref, logoutHref, replacedAttrKey, replacedAttrValue } from "@/constants";
import { loginHref, logoutHref, updatedAttrKey, updatedAttrValue } from "@/constants";
import { parseQueryParams } from "@/query-params";

import {
Expand Down Expand Up @@ -68,7 +68,7 @@ function removeInvalidCommands(commands: HTMLElement[]): void {

function updateUsageTabs(usageTabsSelector: string): void {
const tabs = [...document.querySelectorAll(usageTabsSelector)].filter(
(node) => node.getAttribute(replacedAttrKey) !== replacedAttrValue,
(node) => node.getAttribute(updatedAttrKey) !== updatedAttrValue,
);

if (tabs.length === 0) return;
Expand All @@ -90,21 +90,46 @@ function updateUsageTabs(usageTabsSelector: string): void {

removeInvalidCommands(commands);

tab.setAttribute(replacedAttrKey, replacedAttrValue);
tab.setAttribute(updatedAttrKey, updatedAttrValue);
}
}

function addOpenIDLoginButton(loginDialogSelector: string, loginButtonSelector: string, callback: () => void): void {
const loginDialog = document.querySelector(loginDialogSelector);

if (!loginDialog || loginDialog.getAttribute(updatedAttrKey) === updatedAttrValue) return;

const loginButton = document.querySelector(loginButtonSelector)!;

const loginWithOpenIDButton = loginButton.cloneNode(false) as HTMLButtonElement;

loginWithOpenIDButton.textContent = "Login with OpenID Connect";
loginWithOpenIDButton.dataset.testid = "dialogOpenIDLogin";

loginWithOpenIDButton.addEventListener("click", callback);

loginDialog.append(loginWithOpenIDButton);

loginDialog.setAttribute(updatedAttrKey, updatedAttrValue);
}

export interface InitOptions {
loginButton: string;
logoutButton: string;
usageTabs: string;
loginButtonSelector: string;
loginDialogSelector: string;
logoutButtonSelector: string;
usageTabsSelector: string;
}

//
// By default the login button opens a form that asks the user to submit credentials.
// We replace this behaviour and instead redirect to the route that handles OAuth.
//
export function init({ loginButton, logoutButton, usageTabs }: InitOptions): void {
export function init({
loginButtonSelector,
logoutButtonSelector,
usageTabsSelector,
loginDialogSelector,
}: InitOptions): void {
if (parseAndSaveCredentials()) {
// If we are new logged in, reload the page to remove the query params
reloadToPathname();
Expand All @@ -117,19 +142,25 @@ export function init({ loginButton, logoutButton, usageTabs }: InitOptions): voi

const baseUrl = getBaseUrl(true);

interruptClick(loginButton, () => {
const gotoOpenIDLoginUrl = () => {
location.href = baseUrl + loginHref;
});
};

if (window.__VERDACCIO_OPENID_OPTIONS?.keepPasswdLogin) {
const updateLoginDialog = () => addOpenIDLoginButton(loginDialogSelector, loginButtonSelector, gotoOpenIDLoginUrl);

interruptClick(logoutButton, () => {
document.addEventListener("click", () => retry(updateLoginDialog, 2));
} else {
interruptClick(loginButtonSelector, gotoOpenIDLoginUrl);
}

interruptClick(logoutButtonSelector, () => {
clearCredentials();

location.href = baseUrl + logoutHref;
});

const updateUsageInfo = () => updateUsageTabs(usageTabs);
const updateUsageInfo = () => updateUsageTabs(usageTabsSelector);

document.addEventListener("click", () => retry(updateUsageInfo, 2));

retry(updateUsageInfo);
}
8 changes: 5 additions & 3 deletions src/client/verdaccio.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { init } from "./plugin";

const loginButtonSelector = `[data-testid="header--button-login"]`;
const loginDialogSelector = `[data-testid="dialogContentLogin"]`;
const logoutButtonSelector = `[data-testid="header--button-logout"],[data-testid="logOutDialogIcon"]`;
const usageTabsSelector = `[data-testid="tab-content"]`;

init({
loginButton: loginButtonSelector,
logoutButton: logoutButtonSelector,
usageTabs: usageTabsSelector,
loginButtonSelector,
loginDialogSelector,
logoutButtonSelector,
usageTabsSelector,
});
4 changes: 2 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export const plugin = {

export const pluginKey = name.replace("verdaccio-", "");

export const replacedAttrKey = `data-${pluginKey}`;
export const replacedAttrValue = "1";
export const updatedAttrKey = `data-${pluginKey}`;
export const updatedAttrValue = "1";

export const authorizePath = "/-/oauth/authorize";
export const callbackPath = "/-/oauth/callback";
Expand Down
18 changes: 14 additions & 4 deletions src/server/config/Config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defaultSecurity } from "@verdaccio/config";
import type { Config, PackageAccess, PackageList, Security } from "@verdaccio/types";
import type { Config, PackageList, Security } from "@verdaccio/types";
import merge from "deepmerge";
import { mixed, object, Schema, string } from "yup";
import { boolean, mixed, object, Schema, string } from "yup";

import { plugin, pluginKey } from "@/constants";
import { CONFIG_ENV_NAME_REGEX } from "@/server/constants";
Expand Down Expand Up @@ -29,11 +29,12 @@ export interface ConfigHolder {
groupUsers?: Record<string, string[]>;
storeType: StoreType;

urlPrefix: string;
secret: string;
security: Security;
packages: Record<string, PackageAccess>;
urlPrefix: string;
packages: PackageList;

keepPasswdLogin: boolean;
getStoreConfig(storeType: StoreType): any;
}

Expand All @@ -53,6 +54,7 @@ export interface OpenIDConfig {
"groups-claim"?: string;
"store-type"?: StoreType;
"store-config"?: Record<string, unknown> | string;
"keep-passwd-login"?: boolean;
"authorized-groups"?: string | string[] | boolean;
"group-users"?: string | Record<string, string[]>;
}
Expand All @@ -74,6 +76,7 @@ export default class ParsedPluginConfig implements ConfigHolder {
public get packages(): PackageList {
return this.verdaccioConfig.packages ?? {};
}

public get urlPrefix(): string {
return this.verdaccioConfig.url_prefix ?? "";
}
Expand Down Expand Up @@ -303,4 +306,11 @@ export default class ParsedPluginConfig implements ConfigHolder {
}
}
}

public get keepPasswdLogin(): boolean {
return (
this.getConfigValue<boolean | undefined>("keep-passwd-login", boolean().optional()) ??
this.verdaccioConfig.auth?.htpasswd?.file !== undefined
);
}
}
4 changes: 2 additions & 2 deletions src/server/plugin/AuthCore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Auth, buildUser, isAESLegacy, verifyJWTPayload } from "@verdaccio/auth";
import { defaultLoggedUserRoles, defaultNonLoggedUserRoles } from "@verdaccio/config";
import type { JWTSignOptions, PackageAccess, RemoteUser, Security } from "@verdaccio/types";
import type { JWTSignOptions, PackageList, RemoteUser, Security } from "@verdaccio/types";

import type { ConfigHolder } from "@/server/config/Config";
import { debug } from "@/server/debugger";
Expand Down Expand Up @@ -88,7 +88,7 @@ export class AuthCore {
/**
* Returns all permission groups used in the Verdacio config.
*/
private initConfiguredGroups(packages: Record<string, PackageAccess> = {}): string[] {
private initConfiguredGroups(packages: PackageList = {}): string[] {
for (const packageConfig of Object.values(packages)) {
const groups = (["access", "publish", "unpublish"] as const)
.flatMap((key) => packageConfig[key])
Expand Down
5 changes: 4 additions & 1 deletion src/server/plugin/PatchHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export class PatchHtml implements PluginMiddleware {

const scriptSrc = `${baseUrl}${staticPath}/${scriptName}`;

return `<script defer="defer" src="${scriptSrc}"></script>`;
return [
`<script>window.__VERDACCIO_OPENID_OPTIONS={keepLoginDialog:${this.config.keepPasswdLogin}}</script>`,
`<script defer="defer" src="${scriptSrc}"></script>`,
].join("\n");
}
}

0 comments on commit 9ba22d6

Please sign in to comment.