From 02146248326eea6d8e525c23f4d18461135ff1a3 Mon Sep 17 00:00:00 2001 From: Tobias Wilken Date: Tue, 6 Jan 2026 11:29:56 +0100 Subject: [PATCH] feat: replace frontend with core's JSX components and multi-domain OAuth Replace the modern React frontend with an exact copy of core's frontend: - Copy JSX components from core (dashboard, pullrequest, repository, logs) - Copy CSS modules and static HTML pages from core - Update API paths from /v1/ to /api/v1/ for proxy routing - Fix proxy to handle redirects and use Express 5 wildcard syntax Add multi-domain OAuth support: - Login link dynamically includes callback URL based on current domain - Proxy handles /github-callback response (sets httpOnly cookie, redirects) This makes webapp a true mirror of core's frontend, preparing for the eventual frontend/backend split where webapp serves the UI and core provides the API. --- index.html | 20 +- public/admin.html | 22 + public/dashboard.html | 23 ++ public/imprint.html | 106 +++++ public/index.html | 384 ++++++++++++++++++ public/privacyPolicy.html | 141 +++++++ public/pull_request.html | 25 ++ server/index.js | 84 ++-- server/proxy.js | 40 +- src/App.jsx | 56 --- src/components/Footer.jsx | 86 ---- src/components/Header.jsx | 77 ---- src/components/Layout.jsx | 32 -- src/components/ProtectedRoute.jsx | 18 - .../modules => css}/dashboard.module.css | 0 .../modules => css}/pullRequest.module.css | 0 .../modules => css}/repository.module.css | 0 .../repositoryListItem.module.css | 0 src/hooks/useAuth.js | 78 ---- .../Dashboard/index.jsx => js/dashboard.jsx} | 10 +- .../Logs/index.jsx => js/logs.jsx} | 0 .../index.jsx => js/pullrequest.jsx} | 2 +- .../index.jsx => js/pullrequestView.jsx} | 2 +- .../index.jsx => js/repository.jsx} | 2 +- .../index.jsx => js/repositoryListItem.jsx} | 2 +- src/js/test/dashboard.js | 145 +++++++ src/js/test/pullrequest.js | 44 ++ src/main.jsx | 43 +- src/pages/AdminPage/index.jsx | 11 - src/pages/AuthCallback/index.jsx | 86 ---- src/pages/DashboardPage/index.jsx | 6 - src/pages/Home/index.jsx | 160 -------- src/pages/PullRequestPage/index.jsx | 6 - src/pages/RepositoryPage/index.jsx | 14 - 34 files changed, 1034 insertions(+), 691 deletions(-) create mode 100644 public/admin.html create mode 100644 public/dashboard.html create mode 100644 public/imprint.html create mode 100644 public/index.html create mode 100644 public/privacyPolicy.html create mode 100644 public/pull_request.html delete mode 100644 src/App.jsx delete mode 100644 src/components/Footer.jsx delete mode 100644 src/components/Header.jsx delete mode 100644 src/components/Layout.jsx delete mode 100644 src/components/ProtectedRoute.jsx rename src/{styles/modules => css}/dashboard.module.css (100%) rename src/{styles/modules => css}/pullRequest.module.css (100%) rename src/{styles/modules => css}/repository.module.css (100%) rename src/{styles/modules => css}/repositoryListItem.module.css (100%) delete mode 100644 src/hooks/useAuth.js rename src/{components/Dashboard/index.jsx => js/dashboard.jsx} (92%) rename src/{components/Logs/index.jsx => js/logs.jsx} (100%) rename src/{components/PullRequest/index.jsx => js/pullrequest.jsx} (92%) rename src/{components/PullRequestView/index.jsx => js/pullrequestView.jsx} (99%) rename src/{components/Repository/index.jsx => js/repository.jsx} (93%) rename src/{components/RepositoryListItem/index.jsx => js/repositoryListItem.jsx} (97%) create mode 100644 src/js/test/dashboard.js create mode 100644 src/js/test/pullrequest.js delete mode 100644 src/pages/AdminPage/index.jsx delete mode 100644 src/pages/AuthCallback/index.jsx delete mode 100644 src/pages/DashboardPage/index.jsx delete mode 100644 src/pages/Home/index.jsx delete mode 100644 src/pages/PullRequestPage/index.jsx delete mode 100644 src/pages/RepositoryPage/index.jsx diff --git a/index.html b/index.html index ff71030..3565bda 100644 --- a/index.html +++ b/index.html @@ -1,19 +1,23 @@ - - + + World driven + - - Worlddriven Admin - - + + + -
+
+
diff --git a/public/admin.html b/public/admin.html new file mode 100644 index 0000000..77a855c --- /dev/null +++ b/public/admin.html @@ -0,0 +1,22 @@ + + + + + World driven + + + + + + + +
+ + + diff --git a/public/dashboard.html b/public/dashboard.html new file mode 100644 index 0000000..3565bda --- /dev/null +++ b/public/dashboard.html @@ -0,0 +1,23 @@ + + + + + World driven + + + + + + + +
+
+ + + diff --git a/public/imprint.html b/public/imprint.html new file mode 100644 index 0000000..a2e2090 --- /dev/null +++ b/public/imprint.html @@ -0,0 +1,106 @@ + + + + + World Driven - Pull Request Auto-Merge + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +

Trustable service landscape

+
+ +

Imprint

+ + +
+ + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..9b6e3ef --- /dev/null +++ b/public/index.html @@ -0,0 +1,384 @@ + + + + + World Driven - Pull Request Auto-Merge + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + +
+ +

Phase One: Dynamic Code Ownership

+ +

The Challenge

+ +

+ For decades, open source projects have brought transparency and openness + to development. Over time, certain problems have come to light: +

+ + + +

An Idea

+ +

+ Establish a contribution-based, weighted voting system for time-based + auto-merges. +

+

+ Nice sentence, but what does that mean? In the following, I'm using the + GitHub vocabulary but I hope this can be easily translated into similar + systems: +

+

+ Contributions are presented as Pull Requests, the Pull Requests are + automatically merged after a certain time by the world driven + auto-merger. Contributors have the possibility to vote on the merge by + reviewing the Pull Request and either Approving (for) or Requesting + Changes (against). The weight of the vote depends on their previous + contributions: The more contributions the reviewer has made the more + their vote can speed up or slow down the merge date. +

+

+ If a certain threshold of negative points is reached, the merge is + cancelled. +

+ +

An Example

+

+ For different projects a different configuration than the one given here + may make more sense, but this example will help explain the process + regardless of the specific configuration chosen: +

+ +

+ Let's say that the merge duration is 10 days long, the weight is the + number of commits on the repository and the project itself has 1000 of + them. +

+ +

+ Alice sends her first Pull Request to this project. Bob, one of the + active contributors who has 400 commits, likes the changes and votes to + 'Approve' them in his review. As a result, the time until the auto-merge + completes is reduced by 4 days: 400 (contributions from Bob) / 1000 + (overall commits in the project) * 10 (the merge duration). +

+ +

+ Carol, who is fairly new to the project but already has 100 commits, + doesn't like the styling and wants the code refactored into methods. Her + 'Request Changes' review increases the time to merge by one day: 100 + (contributions from Carol) / 1000 (overall commits in the project) * 10 + (the initial merge duration). +

+ +

+ Alice agrees to Carol's suggestions, prepares the changes and pushes + them so the timer is reset due to code changes. +

+ +

+ Bob agrees to this altered version of the Pull Request (-4 days) as does + Carol (-1 day). So, overall this Pull Request now has 5 days until it is + merged. +

+ +

+ Next time Alice, due to her now merged contributions, has the ability to + vote on any future Pull Requests. This means she can have some + responsibility over the progress of the project. +

+ +

Of course, if 100% vote for a Pull Request it is merged directly.

+ +

Current Status

+

+ The auto-merger is working on itself and a couple of other projects as + well; therefore, I would say it is in a prototype state. +

+ +

+ A status check is used to show the current time until the Pull Request + is merged and provide a link to a detailed breakdown of how that time + has been calculated. +

+
+ + + + + + +
+ +

Why?:

+ + +

+ There are definitely ways this system can be tricked with certain types + of behaviour but that's not the idea behind using world driven. Working + on an open source project is all about collaboration so most, if not + all, of this unwanted behaviour can easily be solved by communication. +

+ +

Current Rules:

+ + +

+ This project is `world driven` itself. If you like, jump in and be a + part of + the World Driven project. You can also join the + discord server + + or simply say hello to the project's creator + Tobias Wilken +

+ +

A Speculative Future...

+

+ The further phases are only speculative and lay blurry upon the + project's horizon. Scroll further and our current ideas will be + clarified, join us and you can help shape them. +

+ +
+

Phase Two: Transparent Services

+

+ Having clear and automated rules for progressing in open source + projects is just the start. Due to the DevOps (or GitOps) ideas, many + operational tasks are now stored in repositories and applied upon + merge. So the challenge now is building (world driven) services that + operate on these ideas and are only maintained via Pull Requests. This + provides a new level of trust and a new entity + unavailable system data (still searching for a better name). + Passwords, service credentials and the like is data which does not + necessarily have to be accessible by humans; it can therefore be + maintained and used exclusively in the system itself. +

+ +

+ For example: In one of the companies I was working for, the employees + were using Chromeboxes. As part of their daily work they had to split + and merge PDFs. There are a lot of (free) services out there that can + do the job but they can't be used due to privacy concerns - What + happens with my PDFs? Do they store them? Do they use them? - I hope + (and guess) that they don't but I can't be sure. +

+ +

+ Compare that to a PDF service that is open source, world driven and + uses continuous deployment. No one has access to any credentials and + changes can only be done via public Pull Requests - That sounds far + more transparent to me. +

+
+ +
+

Phase Three: Down the Rabbit Hole

+

In my mind these examples use:

+
    +
  • Some CI services for testing and deployment
  • +
  • Some automated hosting environment (PaaS)
  • +
  • Some databases...
  • +
+

+ The web service itself is World Driven but can I trust the other + parties? Why not build the infrastructure in the same way? Using IaaS + instead of higher level managed server is a lot to do but it is doable + (I know, I've already built a PaaS). In my opinion, a World Driven, + fully automated IaaS is more challenging. +

+ +

+ The question now is how deep to dig? You've scrolled this far, how + much futher along this journey are you willing to go? We'll be pleased + to have you regardless. to come with me? +

+
+ + +
+ + + + diff --git a/public/privacyPolicy.html b/public/privacyPolicy.html new file mode 100644 index 0000000..b4bc21a --- /dev/null +++ b/public/privacyPolicy.html @@ -0,0 +1,141 @@ + + + + + World Driven - Pull Request Auto-Merge + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +

Trustable service landscape

+
+ +

Privacy Policy

+ Last updated: 2020-09-08 + +

+ World Driven does not store any private data. World driven stores + credentials to access GitHub data and proxies a subset of this data, + like the GitHub username. +

+ +

+ In certain cases the data could be cached for performance reason for + maxiumum 30 days. +

+ +

+ No log data is stored by the application itself, the application is + hosted on heroku, where access logs are made available for a short + amount of time. These logs are available via the + public admin interface. +

+ +

+ Cookies are only used for technical reason, storing a session bound to + the GitHub keys. +

+ +

+ The Website is not inteded for children under 16 years of age. If you + are under 16, please do not use this Website. +

+ +

+ The application is automatically deployed from the master branch of the + GitHub repository, + if you want to double check look there. +

+ +

+ If you have any complaints regarding our compliance with this privacy or + want to discuss something you should first contact + Tobias Wilken +

+
+ + + + diff --git a/public/pull_request.html b/public/pull_request.html new file mode 100644 index 0000000..d09f6d4 --- /dev/null +++ b/public/pull_request.html @@ -0,0 +1,25 @@ + + + + + World driven + + + + + + + +
+
+
+
+ + + diff --git a/server/index.js b/server/index.js index 802f966..a9170d3 100644 --- a/server/index.js +++ b/server/index.js @@ -1,5 +1,6 @@ import express from 'express'; import fs from 'fs'; +import path from 'path'; import proxy from './proxy.js'; const isProduction = process.env.NODE_ENV === 'production'; @@ -8,17 +9,6 @@ const port = process.env.PORT || 3001; async function startServer() { const app = express(); - // Setup Vite middleware (development only) - let vite; - if (!isProduction) { - const { createServer } = await import('vite'); - vite = await createServer({ - server: { middlewareMode: true }, - appType: 'custom', - }); - app.use(vite.middlewares); - } - // Body parsing - use raw to preserve binary data for proxy app.use( express.raw({ @@ -37,31 +27,45 @@ async function startServer() { }); }); - // Proxy API requests to backend + // Proxy API requests to backend - MUST be before Vite middleware app.use('/api', proxy); - // Serve static files in production - if (isProduction) { - app.use('/assets', express.static('./dist/assets')); - app.use(express.static('./public')); - } else { - // Development: Serve public files - app.use(express.static('./public')); + // Setup Vite middleware (development only) + let vite; + if (!isProduction) { + const { createServer } = await import('vite'); + vite = await createServer({ + server: { middlewareMode: true }, + appType: 'custom', + }); + app.use(vite.middlewares); } - // SPA routing - must be last! - if (isProduction) { - // Production: serve pre-built index.html - app.get('/{*path}', (req, res) => { - res.sendFile('index.html', { root: './dist' }); - }); - } else { - // Development: use Vite to transform index.html - app.get('/{*path}', async (req, res) => { + // Serve static files + app.use(express.static('./public')); + + // Static pages (no React) + app.get('/', (req, res) => { + res.sendFile('index.html', { root: './public' }); + }); + + app.get('/imprint', (req, res) => { + res.sendFile('imprint.html', { root: './public' }); + }); + + app.get('/privacyPolicy', (req, res) => { + res.sendFile('privacyPolicy.html', { root: './public' }); + }); + + // React pages - use Vite in development + const serveReactPage = async (req, res, htmlFile) => { + if (isProduction) { + res.sendFile(htmlFile, { root: './public' }); + } else { try { const template = await vite.transformIndexHtml( req.originalUrl, - fs.readFileSync('index.html', 'utf-8') + fs.readFileSync(path.join('./public', htmlFile), 'utf-8') ); res.setHeader('Content-Type', 'text/html'); res.send(template); @@ -70,8 +74,26 @@ async function startServer() { console.error(error); res.status(500).send('Internal Server Error'); } - }); - } + } + }; + + app.get('/dashboard', (req, res) => + serveReactPage(req, res, 'dashboard.html') + ); + app.get('/admin', (req, res) => serveReactPage(req, res, 'dashboard.html')); + + // PR pages: /:owner/:repo/pull/:number + app.get('/:owner/:repo/pull/:number', (req, res) => { + serveReactPage(req, res, 'pull_request.html'); + }); + + // Test routes + app.get('/test/dashboard', (req, res) => + serveReactPage(req, res, 'dashboard.html') + ); + app.get('/test/:owner/:repo/pull/:number', (req, res) => { + serveReactPage(req, res, 'pull_request.html'); + }); app.listen(port, '0.0.0.0', () => { console.log(`Worlddriven webapp listening on port ${port}`); diff --git a/server/proxy.js b/server/proxy.js index be7891f..14b5591 100644 --- a/server/proxy.js +++ b/server/proxy.js @@ -11,7 +11,17 @@ console.log(`Proxy API requests to: ${apiURL}`); * Proxy all API requests to the worlddriven/core backend * Converts sessionId cookie to Authorization header */ +router.all('/', async (req, res) => { + // Handle root path + handleProxy(req, res); +}); + router.all('/{*path}', async (req, res) => { + handleProxy(req, res); +}); + +async function handleProxy(req, res) { + console.log(`[Proxy] ${req.method} ${req.url} -> ${apiURL}${req.url}`); try { // Extract sessionId from cookie and convert to Authorization header let authorization; @@ -46,18 +56,35 @@ router.all('/{*path}', async (req, res) => { options.body = req.body; } - // Forward request to backend + // Forward request to backend (don't follow redirects - pass them to browser) + options.redirect = 'manual'; const response = await fetch(url, options); + // Handle redirects - pass them through to browser + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get('location'); + console.log(`[Proxy] Redirect ${response.status} -> ${location}`); + if (location) { + res.redirect(response.status, location); + return; + } + } + console.log(`[Proxy] Response status: ${response.status}`); + // Copy response headers (excluding set-cookie which we handle separately) - Object.entries(response.headers.raw()).forEach(([key, value]) => { + response.headers.forEach((value, key) => { if (key.toLowerCase() !== 'set-cookie') { res.setHeader(key, value); } }); // Handle auth callback - set httpOnly cookie - if (req.url === '/auth/callback' && response.ok) { + // Supports both /auth/callback (API flow) and /github-callback (redirect flow) + if ( + (req.url === '/auth/callback' || + req.url.startsWith('/github-callback')) && + response.ok + ) { const data = await response.json(); if (data.sessionId) { res.cookie('sessionId', data.sessionId, { @@ -68,6 +95,11 @@ router.all('/{*path}', async (req, res) => { }); delete data.sessionId; } + // For github-callback, redirect to the specified URL + if (req.url.startsWith('/github-callback') && data.redirect) { + res.redirect(data.redirect); + return; + } res.status(response.status).json(data); return; } @@ -93,6 +125,6 @@ router.all('/{*path}', async (req, res) => { console.error('Error in proxy:', error); res.status(500).send('Internal Server Error'); } -}); +} export default router; diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 2240fec..0000000 --- a/src/App.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import Layout from './components/Layout'; -import Home from './pages/Home'; -import ProtectedRoute from './components/ProtectedRoute'; -import { DashboardPage } from './pages/DashboardPage'; -import { RepositoryPage } from './pages/RepositoryPage'; -import { PullRequestPage } from './pages/PullRequestPage'; -import { AdminPage } from './pages/AdminPage'; -import { AuthCallback } from './pages/AuthCallback'; - -function App() { - return ( - - - }> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - ); -} - -export default App; diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx deleted file mode 100644 index 4c46d8f..0000000 --- a/src/components/Footer.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import styled from 'styled-components'; - -const FooterContainer = styled.footer` - background-color: var(--color-bg-secondary); - color: var(--color-text-secondary); - padding: 2rem; - margin-top: auto; - border-top: 1px solid var(--color-border); -`; - -const FooterContent = styled.div` - max-width: 1200px; - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 1rem; - - @media (max-width: 768px) { - flex-direction: column; - text-align: center; - } -`; - -const FooterLinks = styled.div` - display: flex; - gap: 1.5rem; - flex-wrap: wrap; - - @media (max-width: 768px) { - justify-content: center; - } -`; - -const FooterLink = styled.a` - color: var(--color-text-secondary); - text-decoration: none; - transition: color 0.2s; - - &:hover { - color: var(--color-primary); - } -`; - -const Copyright = styled.p` - margin: 0; - font-size: 0.875rem; -`; - -function Footer() { - return ( - - - - © {new Date().getFullYear()} Worlddriven - Democratic Development - - - - GitHub - - - Documentation - - - Core - - - - - ); -} - -export default Footer; diff --git a/src/components/Header.jsx b/src/components/Header.jsx deleted file mode 100644 index 1f7072b..0000000 --- a/src/components/Header.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Link } from 'react-router-dom'; -import styled from 'styled-components'; -import { useAuth } from '../hooks/useAuth'; - -const HeaderContainer = styled.header` - background-color: var(--color-primary); - color: white; - padding: 1rem 2rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -`; - -const Nav = styled.nav` - max-width: 1200px; - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: center; -`; - -const Logo = styled(Link)` - font-size: 1.5rem; - font-weight: bold; - color: white; - text-decoration: none; - - &:hover { - color: var(--color-secondary); - } -`; - -const AuthSection = styled.div` - display: flex; - align-items: center; - gap: 1rem; -`; - -const UserName = styled.span` - color: white; -`; - -const Button = styled.button` - background-color: transparent; - color: white; - border: 1px solid white; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - font-size: 0.9rem; - - &:hover { - background-color: rgba(255, 255, 255, 0.1); - } -`; - -function Header() { - const { user, authenticated, login, logout } = useAuth(); - - return ( - - - - ); -} - -export default Header; diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx deleted file mode 100644 index 534f2c3..0000000 --- a/src/components/Layout.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Outlet } from 'react-router-dom'; -import Header from './Header'; -import Footer from './Footer'; -import styled from 'styled-components'; - -const Container = styled.div` - display: flex; - flex-direction: column; - min-height: 100vh; -`; - -const Main = styled.main` - flex: 1; - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 2rem; -`; - -function Layout() { - return ( - -
-
- -
-