Skip to content

Commit cd87c28

Browse files
committed
fix(api): allow superusers to submit jobs by provisioning proxy user record
feat(ui): clarify Load Example button in jobs console
1 parent 9fa32dd commit cd87c28

4 files changed

Lines changed: 86 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ and this project follows versions of format `{year}.{month}.{patch_number}`.
1010
### Changed
1111

1212
- `qpi-ui`: Made the metric cards on the Overview dashboard (Active QPUs, Queue Status, Next Booking) clickable so they quickly route to their respective tabs.
13+
- `qpi-ui`: Clarified the "Load Example" button text and icon in the Jobs Console to read "Load Bell State Example".
14+
15+
### Fixed
16+
17+
- `qpi-ui`: Fixed "authentication required" error that occurred when superusers attempted to submit a quantum job. Superusers are now transparently issued a proxy `users` record with unlimited QPU seconds to satisfy relational constraints.
1318

1419
## [0.0.30] - 2026-06-26
1520

qpi-ui/internal/api/utils.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/rand"
55
"encoding/hex"
66
"fmt"
7+
"log"
78
"net"
89
"net/http"
910
"qpi/internal/config"
@@ -58,6 +59,50 @@ func getCurrentUser(re *core.RequestEvent) (*db.User, error) {
5859
return &user, nil
5960
}
6061

62+
if re.HasSuperuserAuth() && re.Auth != nil {
63+
usersCol, err := re.App.FindCollectionByNameOrId("users")
64+
if err != nil {
65+
return nil, re.Error(http.StatusInternalServerError, "users collection not found", err)
66+
}
67+
68+
record, err := re.App.FindFirstRecordByData("users", "email", re.Auth.Email())
69+
if err != nil {
70+
// Proxy user does not exist, so create one for superuser
71+
record = core.NewRecord(usersCol)
72+
// Let PocketBase auto-generate the ID
73+
record.Set("email", re.Auth.Email())
74+
username := re.Auth.Id
75+
if len(username) > 5 {
76+
username = username[:5]
77+
}
78+
record.Set("username", "admin_" + username)
79+
record.Set("qpu_seconds", 999999999.0)
80+
81+
if err := re.App.SaveNoValidate(record); err != nil {
82+
log.Printf("Failed to create proxy user: %v", err)
83+
return nil, re.Error(http.StatusInternalServerError, "failed to provision proxy user", err)
84+
}
85+
}
86+
87+
var user db.User
88+
err = user.RefreshFromRecord(record)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
// Ensure superuser has practically infinite QPU seconds
94+
if user.QPUSeconds < 999999999.0 {
95+
record.Set("qpu_seconds", 999999999.0)
96+
if err := re.App.SaveNoValidate(record); err != nil {
97+
log.Printf("Failed to update proxy user quota: %v", err)
98+
return nil, re.Error(http.StatusInternalServerError, "failed to update proxy user quota", err)
99+
}
100+
user.QPUSeconds = 999999999.0
101+
}
102+
103+
return &user, nil
104+
}
105+
61106
token := getToken(re)
62107
user, err := db.GetUserByToken(re.App, token)
63108
if err != nil {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
describe("Jobs Console — Admin Job Submission", () => {
2+
beforeEach(() => {
3+
cy.clearCookies();
4+
cy.clearLocalStorage();
5+
cy.visit("/");
6+
7+
// Login as admin
8+
cy.contains("button", "Administrator").click();
9+
cy.get('input[type="text"]').clear().type("admin@example.com");
10+
cy.get('input[type="password"]').clear().type("supersecretpassword1234");
11+
cy.get('button[type="submit"]').click();
12+
cy.contains("h1", "QPI Interface").should("be.visible");
13+
14+
cy.contains("button", "Jobs Console").click();
15+
cy.contains("h1", "Jobs Console").should("be.visible");
16+
});
17+
18+
it("submits a job successfully as an admin", () => {
19+
// Execute job with default form values
20+
cy.contains("button", "Execute Job").click();
21+
22+
// Wait for the job to complete
23+
cy.contains("div", "completed", { timeout: 15000 }).should("be.visible");
24+
25+
// Verify duration is displayed
26+
cy.contains("span", "Duration")
27+
.parent()
28+
.find("span.font-mono")
29+
.should("be.visible")
30+
.and("contain", "s");
31+
});
32+
});

qpi-ui/internal/dashboard/src/components/tabs/JobsConsoleTab/elements/JobSubmissionPanel.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect } from "react";
2-
import { Copy, Play, Loader2 } from "lucide-react";
2+
import { FileCode, Play, Loader2 } from "lucide-react";
33
import type { QPU } from "@/types";
44

55
interface Props {
@@ -102,9 +102,10 @@ export const JobSubmissionPanel: React.FC<Props> = ({
102102
</span>
103103
<button
104104
onClick={loadExample}
105-
className="text-gray-400 dark:text-zinc-500 hover:text-gray-900 dark:text-white text-xs transition-colors flex items-center gap-1 focus:outline-none"
105+
title="Load a simple Bell State entanglement circuit"
106+
className="text-gray-400 dark:text-zinc-500 hover:text-gray-900 dark:text-white text-xs transition-colors flex items-center gap-1.5 focus:outline-none"
106107
>
107-
<Copy className="w-3.5 h-3.5" /> Load Example
108+
<FileCode className="w-3.5 h-3.5" /> Load Bell State Example
108109
</button>
109110
</div>
110111
<textarea

0 commit comments

Comments
 (0)