Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
nodeLinker: node-modules

# @rwdocs/{core,viewer} 0.1.27 was just published; preapprove it past yarn's
# npmMinimalAgeGate (24h supply-chain quarantine). These are first-party
# packages we publish. Removable once 0.1.27 is >24h old.
npmPreapprovedPackages:
- "@rwdocs/core@^0.1.27"
- "@rwdocs/core-darwin-arm64@^0.1.27"
- "@rwdocs/core-linux-x64-gnu@^0.1.27"
- "@rwdocs/core-linux-x64-musl@^0.1.27"
- "@rwdocs/viewer@^0.1.27"
9 changes: 9 additions & 0 deletions app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ backend:
auth:
providers:
guest: {}
alice: {}
bob: {}

catalog:
rules:
- allow: [Component, Domain, System, Location, User, Group]
locations:
- type: file
target: ../../examples/entities.yaml

rw:
projectDir: ${RW_PROJECT_DIR}
24 changes: 24 additions & 0 deletions examples/entities.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
name: alice
namespace: default
spec:
profile:
displayName: Alice Anderson
email: alice@example.com
picture: https://api.dicebear.com/9.x/identicon/svg?seed=alice
memberOf: []
---
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
name: bob
namespace: default
spec:
profile:
displayName: Bob Brown
email: bob@example.com
picture: https://api.dicebear.com/9.x/identicon/svg?seed=bob
memberOf: []
4 changes: 4 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
},
"dependencies": {
"@backstage/cli": "^0.36.0",
"@backstage/core-components": "^0.18.11",
"@backstage/core-plugin-api": "^1.12.7",
"@backstage/frontend-defaults": "^0.5.0",
"@backstage/frontend-plugin-api": "^0.17.2",
"@backstage/plugin-app-react": "^0.2.4",
"@backstage/plugin-catalog": "^2.0.0",
"@backstage/plugin-search": "^1.7.0",
"@backstage/plugin-user-settings": "^0.9.0",
Expand Down
5 changes: 4 additions & 1 deletion packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import catalogPlugin from '@backstage/plugin-catalog/alpha';
import searchPlugin from '@backstage/plugin-search/alpha';
import userSettingsPlugin from '@backstage/plugin-user-settings/alpha';
import rwPlugin from '@rwdocs/backstage-plugin-rw';
import { devSignInPage } from './devSignInPage';

const app = createApp({
features: [catalogPlugin, searchPlugin, userSettingsPlugin, rwPlugin],
// devSignInPage (Alice/Bob picker) is always included: this instance is a
// demo/test stand for the plugins, never a production build.
features: [catalogPlugin, searchPlugin, userSettingsPlugin, rwPlugin, devSignInPage],
});

export default app.createRoot();
64 changes: 64 additions & 0 deletions packages/app/src/DevUsersSignInPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useState } from 'react';
import { ProxiedSignInPage } from '@backstage/core-components';
import type { IdentityApi, SignInPageProps } from '@backstage/core-plugin-api';

const STORAGE_KEY = 'rw-dev-user';

const USERS = [
{ id: 'alice', label: 'Sign in as Alice' },
{ id: 'bob', label: 'Sign in as Bob' },
] as const;

// Remember the chosen dev user so a page reload silently re-signs-in (via
// ProxiedSignInPage) instead of re-showing the picker — matching how the guest
// provider auto-resumes. Ignores an unknown stored value (e.g. a removed user).
function readStoredProvider(): string | null {
try {
const value = localStorage.getItem(STORAGE_KEY);
return USERS.some(u => u.id === value) ? value : null;
} catch {
return null;
}
}

export function DevUsersSignInPage(props: SignInPageProps) {
const [provider, setProvider] = useState<string | null>(readStoredProvider);

// Persist the choice on success, and wrap signOut so it forgets the choice —
// that way the picker returns after sign-out and a different user can be
// selected, but a plain reload resumes the current one.
const handleSignInSuccess = (identity: IdentityApi) => {
try {
localStorage.setItem(STORAGE_KEY, provider!);
} catch {
// localStorage unavailable (private mode etc.) — resume just won't persist.
}
const signOut = identity.signOut.bind(identity);
identity.signOut = async () => {
try {
localStorage.removeItem(STORAGE_KEY);
} catch {
// ignore
}
await signOut();
};
props.onSignInSuccess(identity);
};

if (provider) {
return (
<ProxiedSignInPage {...props} provider={provider} onSignInSuccess={handleSignInSuccess} />
);
}

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 48, maxWidth: 280 }}>
<h1>Dev sign-in</h1>
{USERS.map(u => (
<button key={u.id} type="button" onClick={() => setProvider(u.id)}>
{u.label}
</button>
))}
</div>
);
}
13 changes: 13 additions & 0 deletions packages/app/src/devSignInPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { SignInPageBlueprint } from '@backstage/plugin-app-react';

export const devSignInPage = createFrontendModule({
pluginId: 'app',
extensions: [
SignInPageBlueprint.make({
params: {
loader: async () => (await import('./DevUsersSignInPage')).DevUsersSignInPage,
},
}),
],
});
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@backstage/plugin-app-backend": "^0.5.11",
"@backstage/plugin-auth-backend": "^0.29.0",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.2.16",
"@backstage/plugin-auth-node": "^0.7.2",
"@backstage/plugin-catalog-backend": "^3.4.0",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.2.17",
"@backstage/plugin-permission-backend": "^0.7.9",
Expand Down
61 changes: 61 additions & 0 deletions packages/backend/src/devUsersAuthModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createBackendModule } from '@backstage/backend-plugin-api';
import {
authProvidersExtensionPoint,
createProxyAuthProviderFactory,
createProxyAuthenticator,
} from '@backstage/plugin-auth-node';
import type { SignInResolver } from '@backstage/plugin-auth-node';

// Dev-only fixed identities. The provider id IS the user.
const DEV_USERS = ['alice', 'bob'] as const;

// A no-op proxy authenticator: there is nothing to authenticate — the chosen
// provider determines the user.
function fixedUserAuthenticator() {
return createProxyAuthenticator({
defaultProfileTransform: async () => ({ profile: {} }),
initialize(_ctx) {
return {};
},
async authenticate(_options, _ctx) {
return { result: {} };
},
});
}

// Signs the caller in as a fixed catalog user (real profile + ownership);
// falls back to a bare token if the entity isn't in the catalog yet.
function signInAsUser(userEntityRef: string): SignInResolver<{}> {
return async (_info, ctx) => {
try {
return await ctx.signInWithCatalogUser({ entityRef: userEntityRef });
} catch {
return ctx.issueToken({
claims: { sub: userEntityRef, ent: [userEntityRef] },
});
}
};
}

export default createBackendModule({
pluginId: 'auth',
moduleId: 'dev-users-provider',
register(reg) {
reg.registerInit({
deps: { providers: authProvidersExtensionPoint },
async init({ providers }) {
// The authenticator is stateless and identical for every provider.
const authenticator = fixedUserAuthenticator();
for (const name of DEV_USERS) {
providers.registerProvider({
providerId: name,
factory: createProxyAuthProviderFactory({
authenticator,
signInResolver: signInAsUser(`user:default/${name}`),
}),
});
}
},
});
},
});
3 changes: 3 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const backend = createBackend();
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
// Dev Alice/Bob auth providers — this instance is a demo/test stand, never a
// production build.
backend.add(import('./devUsersAuthModule'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'));
backend.add(import('@backstage/plugin-permission-backend'));
Expand Down
31 changes: 31 additions & 0 deletions plugins/rw-backend/migrations/20260621000000_init_comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @ts-check
/** @param {import('knex').Knex} knex */
exports.up = async function up(knex) {
await knex.schema.createTable('comments', table => {
table.uuid('id').primary(); // uuid v7
table.text('site_ref').notNullable();
table.text('document_id').notNullable();
table.text('entity_ref').notNullable();
table.uuid('parent_id').nullable();
table.text('author_ref').notNullable();
table.text('author_profile').nullable(); // JSON {displayName, picture?}
table.text('body').notNullable();
table.text('body_html').notNullable();
table.text('selectors').notNullable(); // JSON Selector[]
table.text('status').notNullable(); // 'open' | 'resolved'
table.dateTime('created_at').notNullable();
table.dateTime('updated_at').notNullable();
table.dateTime('resolved_at').nullable();
table.text('resolved_by').nullable();
table.dateTime('deleted_at').nullable();
table.index(['site_ref', 'document_id'], 'comments_site_doc_idx');
table.index(['site_ref', 'document_id', 'status'], 'comments_site_doc_status_idx');
table.index(['parent_id'], 'comments_parent_idx');
table.index(['entity_ref'], 'comments_entity_idx');
});
};

/** @param {import('knex').Knex} knex */
exports.down = async function down(knex) {
await knex.schema.dropTableIfExists('comments');
};
12 changes: 10 additions & 2 deletions plugins/rw-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"files": [
"dist",
"migrations",
"LICENSE-MIT",
"LICENSE-APACHE"
],
Expand All @@ -47,16 +48,23 @@
"@backstage/catalog-model": "^1.7.6",
"@backstage/config": "^1.3.6",
"@backstage/errors": "^1.2.7",
"@backstage/plugin-catalog-node": "^2.2.2",
"@backstage/plugin-permission-common": "^0.9.9",
"@backstage/plugin-permission-node": "^0.11.1",
"@backstage/types": "^1.2.2",
"@rwdocs/backstage-plugin-rw-common": "workspace:^",
"@rwdocs/core": "^0.1.23",
"@rwdocs/core": "^0.1.27",
"express": "^4.21.0",
"express-promise-router": "^4.1.0"
"express-promise-router": "^4.1.0",
"luxon": "^3.7.2",
"uuid": "^11.0.0",
"zod": "^3.25.76 || ^4.0.0"
},
"peerDependencies": {
"@backstage/backend-plugin-api": "^1.0.0"
},
"devDependencies": {
"@backstage/backend-defaults": "^0.17.3",
"@backstage/backend-plugin-api": "^1.0.0",
"@backstage/backend-test-utils": "^1.11.0",
"@backstage/cli": "^0.36.0",
Expand Down
Loading