Skip to content

Commit 93dcdd8

Browse files
committed
Initial UI/JS created with Claude
1 parent ed0e9d6 commit 93dcdd8

9 files changed

Lines changed: 379 additions & 3 deletions

File tree

docs/setup.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ This disables all of Gokapi's internal authentication except for API calls. The
254254
- ``/e2eSetup``
255255
- ``/filerequests``
256256
- ``/logs``
257+
- ``/paste``
257258
- ``/uploadChunk``
258259
- ``/uploadStatus``
259260
- ``/users``

internal/configuration/setup/ProtectedUrls.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/webserver/Webserver.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ func Start() {
113113
mux.HandleFunc("/e2eSetup", requireLogin(showE2ESetup, true, false))
114114
mux.HandleFunc("/error", showError)
115115
mux.HandleFunc("/filerequests", requireLogin(showUploadRequest, true, false))
116+
mux.HandleFunc("/paste", requireLogin(showPaste, true, false))
116117
mux.HandleFunc("/forgotpw", forgotPassword)
117118
mux.HandleFunc("/h/", showHotlink)
118119
mux.HandleFunc("/hotlink/", showHotlink) // backward compatibility
@@ -435,6 +436,18 @@ func showUploadRequest(w http.ResponseWriter, r *http.Request) {
435436
helper.CheckIgnoreTimeout(err)
436437
}
437438

439+
// Handling of /paste
440+
// Lists existing pastes for the current user and provides the paste creation UI
441+
func showPaste(w http.ResponseWriter, r *http.Request) {
442+
user, err := authentication.GetUserFromRequest(r)
443+
if err != nil {
444+
panic(err)
445+
}
446+
view := (&AdminView{}).convertGlobalConfig(ViewPaste, user)
447+
err = templateFolder.ExecuteTemplate(w, "paste", view)
448+
helper.CheckIgnoreTimeout(err)
449+
}
450+
438451
// Handling of /api
439452
// If the user is authenticated, this menu lists all uploads and enables uploading new files
440453
func showApiAdmin(w http.ResponseWriter, r *http.Request) {
@@ -741,6 +754,7 @@ type e2ESetupView struct {
741754
// AdminView contains parameters for all admin-related pages
742755
type AdminView struct {
743756
Items []models.FileApiOutput
757+
Pastes []models.FileApiOutput
744758
ApiKeys []models.ApiKey
745759
Users []userInfo
746760
FileRequests []models.FileRequest
@@ -804,6 +818,8 @@ const (
804818
ViewUsers
805819
// ViewFileRequests is the identifier for the file request menu
806820
ViewFileRequests
821+
// ViewPaste is the identifier for the paste menu
822+
ViewPaste
807823
)
808824

809825
// Converts the globalConfig variable to an AdminView struct to pass the infos to
@@ -868,6 +884,19 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView {
868884
}
869885
u.Users = append(u.Users, userWithUploads)
870886
}
887+
case ViewPaste:
888+
for _, element := range database.GetAllMetadata() {
889+
if !element.IsPaste {
890+
continue
891+
}
892+
if element.UserId != user.Id && !user.HasPermissionListOtherUploads() {
893+
continue
894+
}
895+
fileInfo, err := element.ToFileApiOutput(config.ServerUrl, config.IncludeFilename)
896+
helper.Check(err)
897+
u.Pastes = append(u.Pastes, fileInfo)
898+
}
899+
u.Pastes = sortMetaDataApi(u.Pastes)
871900
case ViewFileRequests:
872901
for _, fileRequest := range filerequest.GetAll() {
873902
// Double-checking if the owner of the file request exists

internal/webserver/api/Api.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,9 @@ func apiPasteAdd(w http.ResponseWriter, r requestParser, user models.User) {
690690
if !ok {
691691
panic("invalid parameter passed")
692692
}
693+
if request.Title == "" {
694+
request.Title = "Untitled Paste"
695+
}
693696
file, err := storage.NewPaste([]byte(request.PasteContent), request.Title, user.Id, models.UploadParameters{
694697
UserId: user.Id,
695698
AllowedDownloads: request.AllowedDownloads,

internal/webserver/web/static/js/admin_api.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,3 +1026,56 @@ async function apiURequestSave(id, name, maxfiles, maxsize, expiry, notes) {
10261026
throw error;
10271027
}
10281028
}
1029+
1030+
// /files/addPaste
1031+
1032+
async function apiAddPaste(content, title, allowedDownloads, expiryDays, password) {
1033+
const apiUrl = './api/files/addPaste';
1034+
const reqPerm = 'PERM_UPLOAD';
1035+
1036+
let token;
1037+
try {
1038+
token = await getToken(reqPerm, false);
1039+
} catch (error) {
1040+
console.error("Unable to gain permission token:", error);
1041+
throw error;
1042+
}
1043+
1044+
const formData = new FormData();
1045+
formData.append("pasteContent", content);
1046+
if (title) {
1047+
formData.append("title", title);
1048+
}
1049+
formData.append("allowedDownloads", allowedDownloads);
1050+
formData.append("expiryDays", expiryDays);
1051+
if (password) {
1052+
formData.append("password", password);
1053+
}
1054+
1055+
const requestOptions = {
1056+
method: 'POST',
1057+
headers: {
1058+
'apikey': token,
1059+
},
1060+
body: formData,
1061+
};
1062+
1063+
try {
1064+
const response = await fetch(apiUrl, requestOptions);
1065+
if (!response.ok) {
1066+
let errorMessage;
1067+
try {
1068+
const errorResponse = await response.json();
1069+
errorMessage = errorResponse.ErrorMessage || `Request failed with status: ${response.status}`;
1070+
} catch {
1071+
const errorText = await response.text();
1072+
errorMessage = errorText || `Request failed with status: ${response.status}`;
1073+
}
1074+
throw new Error(errorMessage);
1075+
}
1076+
return await response.json();
1077+
} catch (error) {
1078+
console.error("Error in apiAddPaste:", error);
1079+
throw error;
1080+
}
1081+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// This file contains JS code for the paste view.
2+
// All files named admin_*.js will be merged together and minimised by calling
3+
// go generate ./...
4+
5+
function pasteToggleDownloads(checkbox) {
6+
document.getElementById("paste-downloads").disabled = !checkbox.checked;
7+
}
8+
9+
function pasteToggleExpiry(checkbox) {
10+
document.getElementById("paste-expiry").disabled = !checkbox.checked;
11+
}
12+
13+
function pasteTogglePassword(checkbox) {
14+
document.getElementById("paste-password").disabled = !checkbox.checked;
15+
}
16+
17+
function submitPaste() {
18+
const content = document.getElementById("paste-content").value.trim();
19+
if (!content) {
20+
alert("Please enter some content before creating a paste.");
21+
return;
22+
}
23+
24+
const title = document.getElementById("paste-title").value.trim();
25+
const limitViews = document.getElementById("paste-enable-downloads").checked;
26+
const limitExpiry = document.getElementById("paste-enable-expiry").checked;
27+
const usePassword = document.getElementById("paste-enable-password").checked;
28+
29+
const allowedDownloads = limitViews ? parseInt(document.getElementById("paste-downloads").value, 10) : 0;
30+
const expiryDays = limitExpiry ? parseInt(document.getElementById("paste-expiry").value, 10) : 0;
31+
const password = usePassword ? document.getElementById("paste-password").value : "";
32+
33+
apiAddPaste(content, title, allowedDownloads, expiryDays, password)
34+
.then(data => {
35+
const info = data.FileInfo;
36+
pasteInsertRow(info);
37+
document.getElementById("paste-content").value = "";
38+
document.getElementById("paste-title").value = "";
39+
pasteCopyUrl(info.UrlDownload, info.Id);
40+
})
41+
.catch(error => {
42+
alert("Failed to create paste: " + error);
43+
console.error("Error:", error);
44+
});
45+
}
46+
47+
function pasteInsertRow(info) {
48+
const tbody = document.getElementById("paste-tbody");
49+
const row = document.createElement("tr");
50+
row.id = "pasterow-" + info.Id;
51+
52+
const views = info.UnlimitedDownloads ? "Unlimited" : info.DownloadsRemaining;
53+
54+
row.innerHTML = `
55+
<td>${escapeHtml(info.Name)}</td>
56+
<td><span id="paste-created-${info.Id}"></span></td>
57+
<td><span id="paste-expiry-${info.Id}"></span></td>
58+
<td>${escapeHtml(String(views))}</td>
59+
<td>
60+
<div class="btn-group" role="group">
61+
<button type="button" class="btn btn-outline-light btn-sm" title="Copy URL"
62+
onclick="pasteCopyUrl('${escapeHtml(info.UrlDownload)}', '${escapeHtml(info.Id)}')">
63+
<i class="bi bi-copy"></i>
64+
</button>
65+
<button type="button" class="btn btn-outline-danger btn-sm" title="Delete"
66+
onclick="pasteDelete('${escapeHtml(info.Id)}')">
67+
<i class="bi bi-trash3"></i>
68+
</button>
69+
</div>
70+
</td>`;
71+
72+
tbody.prepend(row);
73+
insertDateWithNegative(info.UploadDate, "paste-created-" + info.Id, "Unknown");
74+
75+
if (info.UnlimitedTime) {
76+
document.getElementById("paste-expiry-" + info.Id).innerText = "Never";
77+
} else {
78+
insertFileRequestExpiry(info.ExpireAt, "paste-expiry-" + info.Id);
79+
}
80+
}
81+
82+
function pasteCopyUrl(url, id) {
83+
navigator.clipboard.writeText(url).then(() => {
84+
const toastEl = document.getElementById("paste-toast");
85+
document.getElementById("paste-toast-body").innerText = "URL copied to clipboard!";
86+
bootstrap.Toast.getOrCreateInstance(toastEl).show();
87+
}).catch(() => {
88+
prompt("Copy this URL:", url);
89+
});
90+
}
91+
92+
function pasteDelete(id) {
93+
if (!confirm("Delete this paste?")) {
94+
return;
95+
}
96+
apiFilesDelete(id, 0)
97+
.then(() => {
98+
const row = document.getElementById("pasterow-" + id);
99+
if (row) {
100+
row.classList.add("rowDeleting");
101+
setTimeout(() => row.remove(), 290);
102+
}
103+
})
104+
.catch(error => {
105+
alert("Failed to delete paste: " + error);
106+
console.error("Error:", error);
107+
});
108+
}
109+
110+
function escapeHtml(str) {
111+
return String(str)
112+
.replace(/&/g, "&amp;")
113+
.replace(/</g, "&lt;")
114+
.replace(/>/g, "&gt;")
115+
.replace(/"/g, "&quot;")
116+
.replace(/'/g, "&#39;");
117+
}

internal/webserver/web/static/js/min/admin.min.ccb62a8d5b.js

Lines changed: 18 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/webserver/web/templates/html_header.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
<h1>{{.PublicName}}</h1>
8080
<nav class="nav nav-masthead justify-content-center">
8181
<a class="nav-link {{ if eq .ActiveView 0}}active{{ end }}" href="./admin">Upload</a>
82+
<a class="nav-link {{ if eq .ActiveView 5 }}active{{ end }}" href="./paste">Paste</a>
8283
{{ if .ActiveUser.HasPermissionCreateFileRequests }}
8384
<a class="nav-link {{ if eq .ActiveView 4 }}active{{ end }}" href="./filerequests">File Requests</a>
8485
{{ end }}

0 commit comments

Comments
 (0)