Skip to content

Commit 8613e37

Browse files
zdavatzclaude
andcommitted
Replace Yus authentication with Swiyu OID4VP login
Users now authenticate by scanning a QR code with the swiyu Wallet app, which presents a verifiable credential (SD-JWT) containing firstName, lastName, and GLN. A Rack middleware intercepts /swiyu/* routes to handle the verification flow. GLN-to-role mapping via etc/swiyu_roles.yml preserves the existing permission system; unenrolled GLNs default to PowerUser with unlimited queries. The query limit page now redirects to the Swiyu login instead of showing PayPal registration forms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 949ba30 commit 8613e37

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+887
-1092
lines changed

config.ru

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ begin # with a rescue
8686
reentrant: true
8787
use Clogger, logger: $stdout, reentrant: true
8888
use(Rack::Static, urls: ["/doc/"])
89+
require "util/swiyu_middleware"
90+
use ODDB::SwiyuMiddleware
8991
use Rack::ContentLength
9092
SBSM.warn "Starting Rack::Server with log_pattern #{ODDB.config.log_pattern}"
9193

doc/resources/swiyu/login.html

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
<!DOCTYPE html>
2+
<html lang="de">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>ODDB.org - Swiyu Login</title>
7+
<style>
8+
body {
9+
font-family: Arial, Helvetica, sans-serif;
10+
background: #f0f0f0;
11+
margin: 0;
12+
padding: 0;
13+
}
14+
.container {
15+
max-width: 480px;
16+
margin: 60px auto;
17+
background: #fff;
18+
border: 1px solid #ccc;
19+
padding: 2em;
20+
text-align: center;
21+
}
22+
h1 {
23+
color: #003d73;
24+
font-size: 1.4em;
25+
margin-bottom: 0.5em;
26+
}
27+
p {
28+
color: #333;
29+
font-size: 0.95em;
30+
}
31+
#qrcode {
32+
display: inline-block;
33+
margin: 1em 0;
34+
}
35+
#qrcode canvas, #qrcode img {
36+
margin: 0 auto;
37+
}
38+
#status {
39+
margin: 1em 0;
40+
font-size: 1.1em;
41+
font-weight: bold;
42+
}
43+
.error { color: #c00; }
44+
.success { color: #090; }
45+
.pending { color: #666; }
46+
#deeplink {
47+
margin: 1em 0;
48+
}
49+
#deeplink a {
50+
color: #003d73;
51+
text-decoration: underline;
52+
}
53+
#retry-btn {
54+
display: none;
55+
margin: 1em auto;
56+
padding: 0.6em 1.5em;
57+
background: #003d73;
58+
color: #fff;
59+
border: none;
60+
cursor: pointer;
61+
font-size: 1em;
62+
}
63+
#retry-btn:hover {
64+
background: #00507a;
65+
}
66+
.footer {
67+
margin-top: 2em;
68+
font-size: 0.8em;
69+
color: #999;
70+
}
71+
.footer a { color: #003d73; }
72+
</style>
73+
<script src="/doc/resources/javascript/qrcode.js"></script>
74+
</head>
75+
<body>
76+
<div class="container">
77+
<h1>Login via Swiyu Wallet</h1>
78+
<p>Scannen Sie den QR-Code mit Ihrer Swiyu Wallet App.</p>
79+
80+
<div id="qrcode"></div>
81+
<div id="deeplink"></div>
82+
<div id="status" class="pending">Initialisierung...</div>
83+
<button id="retry-btn" onclick="startLogin()">Erneut versuchen</button>
84+
85+
<div class="footer">
86+
<a href="/">Zur&uuml;ck zur Startseite</a>
87+
</div>
88+
</div>
89+
90+
<script>
91+
var pollTimer = null;
92+
var timeoutTimer = null;
93+
94+
function startLogin() {
95+
var statusEl = document.getElementById('status');
96+
var qrEl = document.getElementById('qrcode');
97+
var deepEl = document.getElementById('deeplink');
98+
var retryBtn = document.getElementById('retry-btn');
99+
100+
// Reset
101+
qrEl.innerHTML = '';
102+
deepEl.innerHTML = '';
103+
retryBtn.style.display = 'none';
104+
statusEl.textContent = 'Verifizierung wird erstellt...';
105+
statusEl.className = 'pending';
106+
if (pollTimer) clearInterval(pollTimer);
107+
if (timeoutTimer) clearTimeout(timeoutTimer);
108+
109+
fetch('/swiyu/login')
110+
.then(function(resp) { return resp.json(); })
111+
.then(function(data) {
112+
if (!data.id || !data.verification_deeplink) {
113+
statusEl.textContent = 'Fehler beim Erstellen der Verifizierung.';
114+
statusEl.className = 'error';
115+
retryBtn.style.display = 'block';
116+
return;
117+
}
118+
119+
// Show QR code
120+
new QRCode(qrEl, {
121+
text: data.verification_deeplink,
122+
width: 256,
123+
height: 256,
124+
colorDark: '#003d73',
125+
colorLight: '#ffffff'
126+
});
127+
128+
// Show mobile deeplink
129+
var link = document.createElement('a');
130+
link.href = data.verification_deeplink;
131+
link.textContent = 'In Swiyu Wallet \u00f6ffnen';
132+
deepEl.appendChild(link);
133+
134+
statusEl.textContent = 'Warte auf Wallet-Scan...';
135+
statusEl.className = 'pending';
136+
137+
// Poll for status
138+
var verificationId = data.id;
139+
pollTimer = setInterval(function() {
140+
fetch('/swiyu/status/' + verificationId)
141+
.then(function(resp) { return resp.json(); })
142+
.then(function(pollData) {
143+
if (pollData.state === 'SUCCESS') {
144+
clearInterval(pollTimer);
145+
if (timeoutTimer) clearTimeout(timeoutTimer);
146+
statusEl.textContent = 'Verifiziert! Anmeldung...';
147+
statusEl.className = 'success';
148+
149+
fetch('/swiyu/session', {
150+
method: 'POST',
151+
headers: { 'Content-Type': 'application/json' },
152+
body: JSON.stringify({ verification_id: verificationId })
153+
})
154+
.then(function(resp) { return resp.json(); })
155+
.then(function(sessionData) {
156+
if (sessionData.status === 'ok') {
157+
window.location.href = sessionData.redirect || '/';
158+
} else {
159+
statusEl.textContent = 'Anmeldung fehlgeschlagen: ' + (sessionData.message || 'Unbekannter Fehler');
160+
statusEl.className = 'error';
161+
retryBtn.style.display = 'block';
162+
}
163+
})
164+
.catch(function() {
165+
statusEl.textContent = 'Netzwerkfehler bei der Anmeldung.';
166+
statusEl.className = 'error';
167+
retryBtn.style.display = 'block';
168+
});
169+
} else if (pollData.state === 'FAILED') {
170+
clearInterval(pollTimer);
171+
if (timeoutTimer) clearTimeout(timeoutTimer);
172+
statusEl.textContent = 'Verifizierung fehlgeschlagen. Bitte erneut versuchen.';
173+
statusEl.className = 'error';
174+
retryBtn.style.display = 'block';
175+
} else if (pollData.state === 'EXPIRED') {
176+
clearInterval(pollTimer);
177+
if (timeoutTimer) clearTimeout(timeoutTimer);
178+
statusEl.textContent = 'Verifizierung abgelaufen. Bitte erneut versuchen.';
179+
statusEl.className = 'error';
180+
retryBtn.style.display = 'block';
181+
}
182+
})
183+
.catch(function(e) {
184+
console.error('Polling error:', e);
185+
});
186+
}, 2000);
187+
188+
// Timeout after 5 minutes
189+
timeoutTimer = setTimeout(function() {
190+
clearInterval(pollTimer);
191+
if (statusEl.className === 'pending') {
192+
statusEl.textContent = 'Zeitlimit \u00fcberschritten. Bitte erneut versuchen.';
193+
statusEl.className = 'error';
194+
retryBtn.style.display = 'block';
195+
}
196+
}, 300000);
197+
})
198+
.catch(function(err) {
199+
statusEl.textContent = 'Netzwerkfehler: ' + err.message;
200+
statusEl.className = 'error';
201+
retryBtn.style.display = 'block';
202+
});
203+
}
204+
205+
// Start automatically
206+
startLogin();
207+
</script>
208+
</body>
209+
</html>

etc/swiyu_roles.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# etc/swiyu_roles.yml
2+
# Maps GLN numbers to ODDB role keys and permissions.
3+
#
4+
# Role keys correspond to the existing permission system:
5+
# org.oddb.RootUser, org.oddb.AdminUser, org.oddb.PowerUser,
6+
# org.oddb.CompanyUser, org.oddb.PowerLinkUser
7+
#
8+
# "permissions" lists fine-grained action/key pairs checked by allowed?(action, key).
9+
# "association" links a user to an ODBA-persisted object (e.g., Company) by odba_id.
10+
11+
accepted_issuer_did: "did:tdw:QmeA6Hpod7N85daNqWZD5w8jBCU6oaXcxxQFNZ6ox245ci:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:5b1672d3-2805-4752-b364-2f87013bc5c3"
12+
13+
users:
14+
# Example root user — replace GLN with actual values
15+
# "7601000000001":
16+
# roles:
17+
# - org.oddb.RootUser
18+
# permissions:
19+
# - { action: login, key: org.oddb.RootUser }
20+
# association: null

src/config.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ module ODDB
1919
MEDDATA_URI ||= "druby://127.0.0.1:10006"
2020
SWISSREG_URI ||= "druby://127.0.0.1:10007"
2121
READONLY_URI ||= "druby://127.0.0.1:10013"
22-
YUS_URI ||= "drbssl://127.0.0.1:9997"
2322
MIGEL_URI ||= "druby://127.0.0.1:33000"
24-
YUS_DOMAIN ||= "oddb.org"
2523

2624
oddb_dir = ODDB::PROJECT_ROOT
2725
default_dir = File.expand_path("etc", oddb_dir)

src/custom/lookandfeelbase.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,9 @@ def google_analytics_token
946946
:location => "PLZ/Ort",
947947
:login => "Anmelden",
948948
:login_form => "Anmeldung",
949+
:swiyu_login => "Anmeldung",
950+
:swiyu_login_link => "Bitte melden Sie sich mit Ihrer Swiyu Wallet an, um ODDB.org uneingeschr&auml;nkt zu nutzen. Bei Fragen: zdavatz at ywesee dot com.",
951+
:swiyu_logout => "Abmelden",
949952
:login_welcome => "Willkommen bei oddb.org",
950953
:logo_file => "Logo",
951954
:logo_fr => "Logo Fran&ccedil;ais",

0 commit comments

Comments
 (0)