• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanodash / 29344362295

14 Jul 2026 03:13PM UTC coverage: 28.485% (-0.8%) from 29.306%
29344362295

Pull #557

github

web-flow
Merge df950e1a3 into e89f67f90
Pull Request #557: feat: chat with local Claude Code via MCP endpoint

1986 of 7698 branches covered (25.8%)

Branch coverage included in aggregate %.

3930 of 13071 relevant lines covered (30.07%)

4.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

20.72
src/main/java/com/knowledgepixels/nanodash/NanodashSession.java
1
package com.knowledgepixels.nanodash;
2

3
import com.knowledgepixels.nanodash.component.NanopubResults;
4
import com.knowledgepixels.nanodash.domain.User;
5
import com.knowledgepixels.nanodash.page.OrcidLoginPage;
6
import com.knowledgepixels.nanodash.page.ProfilePage;
7
import jakarta.servlet.http.HttpServletRequest;
8
import jakarta.servlet.http.HttpSession;
9
import jakarta.xml.bind.DatatypeConverter;
10
import org.apache.commons.io.FileUtils;
11
import org.apache.wicket.PageReference;
12
import org.apache.wicket.Session;
13
import org.apache.wicket.protocol.http.WebSession;
14
import org.apache.wicket.request.Request;
15
import org.apache.wicket.request.flow.RedirectToUrlException;
16
import org.apache.wicket.request.mapper.parameter.PageParameters;
17
import org.eclipse.rdf4j.model.IRI;
18
import org.eclipse.rdf4j.model.ValueFactory;
19
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
20
import org.nanopub.extra.security.*;
21
import org.nanopub.extra.setting.IntroNanopub;
22
import org.slf4j.Logger;
23
import org.slf4j.LoggerFactory;
24

25
import java.io.File;
26
import java.io.IOException;
27
import java.io.Serializable;
28
import java.nio.charset.StandardCharsets;
29
import java.security.KeyPair;
30
import java.util.Date;
31
import java.util.List;
32
import java.util.Locale;
33
import java.util.Map;
34
import java.util.concurrent.ConcurrentHashMap;
35
import java.util.concurrent.ConcurrentMap;
36

37
import org.apache.wicket.markup.html.WebPage;
38
import org.nanopub.Nanopub;
39

40
/**
41
 * Represents a session in the Nanodash application.
42
 */
43
public class NanodashSession extends WebSession {
44

45
    private transient HttpSession httpSession;
46
    private static final Logger logger = LoggerFactory.getLogger(NanodashSession.class);
9✔
47

48
    /**
49
     * Retrieves the current Nanodash session.
50
     *
51
     * @return The current NanodashSession instance.
52
     */
53
    public static NanodashSession get() {
54
        return (NanodashSession) Session.get();
9✔
55
    }
56

57
    /**
58
     * Returns the current user's agent IRI, or null when no session is bound to
59
     * the current thread (e.g. a background or non-request context) or no user is
60
     * logged in. Safe to call outside a request cycle, unlike {@link #get()}.
61
     *
62
     * @return the current user IRI, or null
63
     */
64
    public static IRI getCurrentUserIriOrNull() {
65
        return Session.exists() ? get().getUserIri() : null;
×
66
    }
67

68
    /**
69
     * Constructs a new NanodashSession for the given request.
70
     * Initializes the HTTP session and loads profile information.
71
     *
72
     * @param request The HTTP request.
73
     */
74
    public NanodashSession(Request request) {
75
        super(request);
9✔
76
        setLocale(Locale.US);
12✔
77
        httpSession = ((HttpServletRequest) request.getContainerRequest()).getSession();
18✔
78
        bind();
6✔
79
        loadProfileInfo();
6✔
80
    }
3✔
81

82
    @Override
83
    public Session setLocale(Locale locale) {
84
        return super.setLocale(Locale.US);
12✔
85
    }
86

87
    private static ValueFactory vf = SimpleValueFactory.getInstance();
9✔
88

89
//        private IntroExtractor introExtractor;
90

91
    private String userDir = System.getProperty("user.home") + "/.nanopub/";
15✔
92
    private NanopubResults.ViewMode nanopubResultsViewMode = NanopubResults.ViewMode.LIST;
9✔
93
    private boolean claudeChatDockExpanded = false;
9✔
94

95
    private KeyPair keyPair;
96
    private IRI userIri;
97
    private ConcurrentMap<IRI, IntroNanopub> introNps;
98
//        private Boolean isOrcidLinked;
99
//        private String orcidLinkError;
100

101
    private Integer localIntroCount = null;
9✔
102
    private IntroNanopub localIntro = null;
9✔
103

104
    private Date lastTimeIntroPublished = null;
9✔
105

106
    /**
107
     * Loads profile information for the user.
108
     * Initializes user-related data such as keys and introductions.
109
     */
110
    public void loadProfileInfo() {
111
        localIntroCount = null;
9✔
112
        localIntro = null;
9✔
113
        NanodashPreferences prefs = NanodashPreferences.get();
6✔
114
        if (prefs.isOrcidLoginMode()) {
9!
115
            File usersDir = new File(System.getProperty("user.home") + "/.nanopub/nanodash-users/");
×
116
            if (!usersDir.exists()) usersDir.mkdir();
×
117
        }
118
        if (userIri == null && !prefs.isReadOnlyMode() && !prefs.isOrcidLoginMode()) {
27!
119
            if (getOrcidFile().exists()) {
12!
120
                try {
121
                    String orcid = FileUtils.readFileToString(getOrcidFile(), StandardCharsets.UTF_8).trim();
×
122
                    //String orcid = Files.readString(orcidFile.toPath(), StandardCharsets.UTF_8).trim();
123
                    if (orcid.matches(ProfilePage.ORCID_PATTERN)) {
×
124
                        userIri = vf.createIRI("https://orcid.org/" + orcid);
×
125
                        if (httpSession != null) httpSession.setMaxInactiveInterval(24 * 60 * 60);  // 24h
×
126
                    }
127
                } catch (IOException ex) {
×
128
                    logger.error("Couldn't read ORCID file", ex);
×
129
                }
×
130
            }
131
        }
132
        if (userIri != null && keyPair == null) {
9!
133
            File keyFile = getKeyFile();
×
134
            if (keyFile.exists()) {
×
135
                try {
136
                    keyPair = SignNanopub.loadKey(keyFile.getPath(), SignatureAlgorithm.RSA);
×
137
                } catch (Exception ex) {
×
138
                    logger.error("Couldn't load key pair", ex);
×
139
                }
×
140
            } else {
141
                // Automatically generate new keys
142
                makeKeys();
×
143
            }
144
        }
145
        if (userIri != null && keyPair != null && introNps == null) {
9!
146
            introNps = new ConcurrentHashMap<>(User.getIntroNanopubs(getPubkeyString()));
×
147
        }
148
//                checkOrcidLink();
149
    }
3✔
150

151
    /**
152
     * Checks if the user's profile is complete.
153
     *
154
     * @return True if the profile is complete, false otherwise.
155
     */
156
    public boolean isProfileComplete() {
157
        return userIri != null && keyPair != null && introNps != null;
×
158
    }
159

160
    /**
161
     * Redirects the user to the login page if their profile is incomplete.
162
     *
163
     * @param path       The path to redirect to after login.
164
     * @param parameters The page parameters for the redirect.
165
     */
166
    public void redirectToLoginIfNeeded(String path, PageParameters parameters) {
167
        String loginUrl = getLoginUrl(path, parameters);
×
168
        if (loginUrl == null) return;
×
169
        throw new RedirectToUrlException(loginUrl);
×
170
    }
171

172
    /**
173
     * Retrieves the login URL for the user.
174
     *
175
     * @param path       The path to redirect to after login.
176
     * @param parameters The page parameters for the redirect.
177
     * @return The login URL, or null if the user is already logged in.
178
     */
179
    public String getLoginUrl(String path, PageParameters parameters) {
180
        if (isProfileComplete()) return null;
×
181
        if (NanodashPreferences.get().isOrcidLoginMode()) {
×
182
            return OrcidLoginPage.getOrcidLoginUrl(path, parameters);
×
183
        } else {
184
            return ProfilePage.MOUNT_PATH;
×
185
        }
186
    }
187

188
    /**
189
     * Retrieves the public key as a Base64-encoded string.
190
     *
191
     * @return The public key string, or null if the key pair is not set.
192
     */
193
    public String getPubkeyString() {
194
        if (keyPair == null) return null;
×
195
        return DatatypeConverter.printBase64Binary(keyPair.getPublic().getEncoded()).replaceAll("\\s", "");
×
196
    }
197

198
    /**
199
     * Retrieves the public key hash for the user.
200
     *
201
     * @return The SHA-256 hash of the public key, or null if the public key is not set.
202
     */
203
    public String getPubkeyhash() {
204
        String pubkey = getPubkeyString();
×
205
        if (pubkey == null) return null;
×
206
        return Utils.createSha256HexHash(pubkey);
×
207
    }
208

209
    /**
210
     * Checks if the user's public key is approved.
211
     *
212
     * @return True if the public key is approved, false otherwise.
213
     */
214
    public boolean isPubkeyApproved() {
215
        if (keyPair == null || userIri == null) return false;
×
216
        return User.isApprovedPubkeyhashForUser(getPubkeyhash(), userIri);
×
217
    }
218

219
    /**
220
     * Retrieves the user's key pair.
221
     *
222
     * @return The key pair.
223
     */
224
    public KeyPair getKeyPair() {
225
        return keyPair;
×
226
    }
227

228
    /**
229
     * Generates a new key pair for the user.
230
     */
231
    public void makeKeys() {
232
        try {
233
            MakeKeys.make(getKeyFile().getAbsolutePath().replaceFirst("_rsa$", ""), SignatureAlgorithm.RSA);
×
234
            keyPair = SignNanopub.loadKey(getKeyFile().getPath(), SignatureAlgorithm.RSA);
×
235
        } catch (Exception ex) {
×
236
            logger.error("Couldn't create key pair", ex);
×
237
        }
×
238
    }
×
239

240
    /**
241
     * Retrieves the user's IRI.
242
     *
243
     * @return The user's IRI, or null if not set.
244
     */
245
    public IRI getUserIri() {
246
        return userIri;
9✔
247
    }
248

249
    /**
250
     * Retrieves the user's introduction nanopublications.
251
     *
252
     * @return A list of user's introduction nanopublications.
253
     */
254
    public List<IntroNanopub> getUserIntroNanopubs() {
255
        return User.getIntroNanopubs(userIri);
12✔
256
    }
257

258
    /**
259
     * Counts the number of local introduction nanopublications.
260
     *
261
     * @return The count of local introduction nanopublications.
262
     */
263
    public int getLocalIntroCount() {
264
        if (localIntroCount == null) {
9!
265
            localIntroCount = 0;
12✔
266
            for (IntroNanopub inp : getUserIntroNanopubs()) {
21!
267
                if (isIntroWithLocalKey(inp)) {
×
268
                    localIntroCount++;
×
269
                    localIntro = inp;
×
270
                }
271
            }
×
272
            if (localIntroCount > 1) localIntro = null;
15!
273
        }
274
        return localIntroCount;
12✔
275
    }
276

277
    /**
278
     * Retrieves the local introduction nanopublication.
279
     *
280
     * @return The local introduction nanopublication, or null if not found.
281
     */
282
    public IntroNanopub getLocalIntro() {
283
        getLocalIntroCount();
×
284
        return localIntro;
×
285
    }
286

287
    /**
288
     * Checks if the given introduction nanopublication is associated with the local key.
289
     *
290
     * @param inp The introduction nanopublication.
291
     * @return True if associated with the local key, false otherwise.
292
     */
293
    public boolean isIntroWithLocalKey(IntroNanopub inp) {
294
        IRI location = Utils.getLocation(inp);
×
295
        NanopubSignatureElement el = Utils.getNanopubSignatureElement(inp);
×
296
        String siteUrl = NanodashPreferences.get().getWebsiteUrl();
×
297
        if (location != null && siteUrl != null) {
×
298
            String l = location.stringValue();
×
299
            // TODO: Solve the name change recognition in a better way:
300
            if (!l.equals(siteUrl) && !l.replace("nanobench", "nanodash").equals(siteUrl)) return false;
×
301
        }
302
        if (!getPubkeyString().equals(el.getPublicKeyString())) return false;
×
303
        for (KeyDeclaration kd : inp.getKeyDeclarations()) {
×
304
            if (getPubkeyString().equals(kd.getPublicKeyString())) return true;
×
305
        }
×
306
        return false;
×
307
    }
308

309
    /**
310
     * Sets the user's ORCID identifier.
311
     *
312
     * @param orcid The ORCID identifier.
313
     */
314
    public void setOrcid(String orcid) {
315
        if (!orcid.matches(ProfilePage.ORCID_PATTERN)) {
×
316
            throw new RuntimeException("Illegal ORCID identifier: " + orcid);
×
317
        }
318
        if (NanodashPreferences.get().isOrcidLoginMode()) {
×
319
            userDir = System.getProperty("user.home") + "/.nanopub/nanodash-users/" + orcid + "/";
×
320
            File f = new File(userDir);
×
321
            if (!f.exists()) f.mkdir();
×
322
        } else {
×
323
            try {
324
                FileUtils.writeStringToFile(getOrcidFile(), orcid + "\n", StandardCharsets.UTF_8);
×
325
                //                        Files.writeString(orcidFile.toPath(), orcid + "\n");
326
            } catch (IOException ex) {
×
327
                logger.error("Couldn't write ORCID file", ex);
×
328
            }
×
329
        }
330
        userIri = vf.createIRI("https://orcid.org/" + orcid);
×
331
        loadProfileInfo();
×
332
        if (httpSession != null) httpSession.setMaxInactiveInterval(24 * 60 * 60);  // 24h
×
333
    }
×
334

335
    /**
336
     * Logs out the user and invalidates the session.
337
     */
338
    public void logout() {
339
        userIri = null;
×
340
        invalidateNow();
×
341
    }
×
342

343
    /**
344
     * Retrieves the user's introduction nanopublications as a map.
345
     *
346
     * @return A map of introduction nanopublications.
347
     */
348
    public Map<IRI, IntroNanopub> getIntroNanopubs() {
349
        return introNps;
×
350
    }
351

352
//        public void checkOrcidLink() {
353
//                if (isOrcidLinked == null && userIri != null) {
354
//                        orcidLinkError = "";
355
//                        introExtractor = null;
356
//                        try {
357
//                                introExtractor = IntroNanopub.extract(userIri.stringValue(), null);
358
//                                if (introExtractor.getIntroNanopub() == null) {
359
//                                        orcidLinkError = "ORCID account is not linked.";
360
//                                        isOrcidLinked = false;
361
//                                } else {
362
//                                        IntroNanopub inp = IntroNanopub.get(userIri.stringValue(), introExtractor);
363
//                                        if (introNps != null && introNps.containsKey(inp.getNanopub().getUri())) {
364
//                                                // TODO: also check whether introduction contains local key
365
//                                                isOrcidLinked = true;
366
//                                        } else {
367
//                                                isOrcidLinked = false;
368
//                                                orcidLinkError = "Error: ORCID is linked to another introduction nanopublication.";
369
//                                        }
370
//                                }
371
//                        } catch (Exception ex) {
372
//                                logger.error("ORCID check failed");
373
//                                orcidLinkError = "ORCID check failed.";
374
//                        }
375
//                }
376
//        }
377
//
378
//        public void resetOrcidLinked() {
379
//                isOrcidLinked = null;
380
//        }
381
//
382
//        public boolean isOrcidLinked() {
383
//                checkOrcidLink();
384
//                return isOrcidLinked != null && isOrcidLinked == true;
385
//        }
386
//
387
//        public String getOrcidLinkError() {
388
//                return orcidLinkError;
389
//        }
390
//
391
//        public String getOrcidName() {
392
//                if (introExtractor == null || introExtractor.getName() == null) return null;
393
//                if (introExtractor.getName().trim().isEmpty()) return null;
394
//                return introExtractor.getName();
395
//        }
396

397
    /**
398
     * Retrieves the file for storing the user's ORCID identifier.
399
     *
400
     * @return The ORCID file.
401
     */
402
    private File getOrcidFile() {
403
        return new File(userDir + "orcid");
21✔
404
    }
405

406
    /**
407
     * Retrieves the file for storing the user's private key.
408
     *
409
     * @return The key file.
410
     */
411
    public File getKeyFile() {
412
        return new File(userDir + "id_rsa");
×
413
    }
414

415
    /**
416
     * Sets the time when the introduction was last published.
417
     */
418
    public void setIntroPublishedNow() {
419
        lastTimeIntroPublished = new Date();
×
420
    }
×
421

422
    /**
423
     * Checks if the introduction has been published.
424
     *
425
     * @return True if the introduction has been published, false otherwise.
426
     */
427
    public boolean hasIntroPublished() {
428
        return lastTimeIntroPublished != null;
×
429
    }
430

431
    /**
432
     * Calculates the time since the last introduction was published.
433
     *
434
     * @return The time in milliseconds since the last introduction was published, or Long.MAX_VALUE if it has never been published.
435
     */
436
    public long getTimeSinceLastIntroPublished() {
437
        if (lastTimeIntroPublished == null) return Long.MAX_VALUE;
15!
438
        return new Date().getTime() - lastTimeIntroPublished.getTime();
×
439
    }
440

441
    /**
442
     * Sets the view mode for displaying nanopublication results.
443
     *
444
     * @param viewMode The desired view mode (e.g., GRID or LIST).
445
     */
446
    public void setNanopubResultsViewMode(NanopubResults.ViewMode viewMode) {
447
        this.nanopubResultsViewMode = viewMode;
×
448
    }
×
449

450
    /**
451
     * Retrieves the current view mode for displaying nanopublication results.
452
     *
453
     * @return The current view mode.
454
     */
455
    public NanopubResults.ViewMode getNanopubResultsViewMode() {
456
        return this.nanopubResultsViewMode;
×
457
    }
458

459
    /**
460
     * Whether the docked Claude chat panel is expanded on this session's pages.
461
     *
462
     * @return true if expanded
463
     */
464
    public boolean isClaudeChatDockExpanded() {
465
        return claudeChatDockExpanded;
×
466
    }
467

468
    /**
469
     * Set whether the docked Claude chat panel is expanded.
470
     *
471
     * @param claudeChatDockExpanded true to expand
472
     */
473
    public void setClaudeChatDockExpanded(boolean claudeChatDockExpanded) {
474
        this.claudeChatDockExpanded = claudeChatDockExpanded;
×
475
    }
×
476

477
    // --- Preview nanopub support ---
478

479
    public static class PreviewNanopub implements Serializable {
480
        private final Nanopub nanopub;
481
        private final PageParameters pageParams;
482
        private final Class<? extends WebPage> confirmPageClass;
483
        private final boolean consentChecked;
484
        private final PageReference sourcePageRef;
485

486
        public PreviewNanopub(Nanopub nanopub, PageParameters pageParams, Class<? extends WebPage> confirmPageClass, boolean consentChecked, PageReference sourcePageRef) {
×
487
            this.nanopub = nanopub;
×
488
            this.pageParams = pageParams;
×
489
            this.confirmPageClass = confirmPageClass;
×
490
            this.consentChecked = consentChecked;
×
491
            this.sourcePageRef = sourcePageRef;
×
492
        }
×
493

494
        public Nanopub getNanopub() { return nanopub; }
×
495
        public PageParameters getPageParams() { return pageParams; }
×
496
        public Class<? extends WebPage> getConfirmPageClass() { return confirmPageClass; }
×
497
        public boolean isConsentChecked() { return consentChecked; }
×
498
        public PageReference getSourcePageRef() { return sourcePageRef; }
×
499
    }
500

501
    private ConcurrentMap<String, PreviewNanopub> previewMap = new ConcurrentHashMap<>();
15✔
502

503
    public void setPreviewNanopub(String id, PreviewNanopub preview) {
504
        previewMap.put(id, preview);
×
505
    }
×
506

507
    public PreviewNanopub getPreviewNanopub(String id) {
508
        return previewMap.get(id);
×
509
    }
510

511
    public PreviewNanopub removePreviewNanopub(String id) {
512
        return previewMap.remove(id);
×
513
    }
514

515
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc