Skip to content

Commit ce17b20

Browse files
authored
implement jwt tokens for sharing (#6)
1 parent 2fdfdd6 commit ce17b20

File tree

14 files changed

+343
-143
lines changed

14 files changed

+343
-143
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ A successful request will return a `200 OK` response with a JSON body containing
208208
{
209209
"key": "hocwr6i6",
210210
"version": 1,
211-
"update_token": "kiczgez33j7qkvqdg9f7ksrd8jk88wba"
211+
"token": "kiczgez33j7qkvqdg9f7ksrd8jk88wba"
212212
}
213213
```
214214

@@ -271,7 +271,7 @@ The response will be a `200 OK` with the document content as `application/json`
271271

272272
### Update a document
273273

274-
To update a paste you have to send a `PATCH` request to `/documents/{key}` with the `content` as `plain/text` body and the `update_token` as `Authorization` header.
274+
To update a paste you have to send a `PATCH` request to `/documents/{key}` with the `content` as `plain/text` body and the `token` as `Authorization` header.
275275

276276
> **Note**
277277
> You can also specify the code language with the `Language` header.
@@ -303,15 +303,15 @@ A successful request will return a `200 OK` response with a JSON body containing
303303

304304
### Delete a document
305305

306-
To delete a document you have to send a `DELETE` request to `/documents/{key}` with the `update_token` as `Authorization` header.
306+
To delete a document you have to send a `DELETE` request to `/documents/{key}` with the `token` as `Authorization` header.
307307

308308
A successful request will return a `204 No Content` response with an empty body.
309309

310310
---
311311

312312
### Delete a document version
313313

314-
To delete a document version you have to send a `DELETE` request to `/documents/{key}/versions/{version}` with the `update_token` as `Authorization` header.
314+
To delete a document version you have to send a `DELETE` request to `/documents/{key}/versions/{version}` with the `token` as `Authorization` header.
315315

316316
A successful request will return a `204 No Content` response with an empty body.
317317

assets/script.js

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1717
const version = window.location.hash === "" ? 0 : parseInt(window.location.hash.slice(1));
1818
const params = new URLSearchParams(window.location.search);
1919
if (params.has("token")) {
20-
setUpdateToken(key, params.get("token"));
20+
setToken(key, params.get("token"));
2121
}
2222

2323
document.querySelector("#nav-btn").checked = false;
@@ -89,8 +89,8 @@ document.querySelector("#code-edit").addEventListener("keyup", (event) => {
8989
document.querySelector("#edit").addEventListener("click", async () => {
9090
if (document.querySelector("#edit").disabled) return;
9191

92-
const {key, version, content, language} = getState();
93-
const {newState, url} = createState(getUpdateToken(key) === "" ? "" : key, 0, "edit", content, language);
92+
const {key, content, language} = getState();
93+
const {newState, url} = createState(hasPermission(getToken(key), "write") ? key : "", 0, "edit", content, language);
9494
updateCode(newState);
9595
updatePage(newState);
9696
window.history.pushState(newState, "", url);
@@ -100,17 +100,17 @@ document.querySelector("#save").addEventListener("click", async () => {
100100
if (document.querySelector("#save").disabled) return;
101101
const {key, mode, content, language} = getState()
102102
if (mode !== "edit") return;
103-
const updateToken = getUpdateToken(key);
103+
const token = getToken(key);
104104
const saveButton = document.querySelector("#save");
105105
saveButton.classList.add("loading");
106106

107107
let response;
108-
if (key && updateToken) {
108+
if (key && token) {
109109
response = await fetch(`/documents/${key}`, {
110110
method: "PATCH",
111111
body: content,
112112
headers: {
113-
Authorization: updateToken,
113+
Authorization: `Bearer ${token}`,
114114
Language: language
115115
}
116116
});
@@ -132,9 +132,10 @@ document.querySelector("#save").addEventListener("click", async () => {
132132
return;
133133
}
134134

135-
const {newState, url} = createState(body.key, body.version, "view", content, language);
136-
setUpdateToken(body.key, body.update_token);
137-
135+
const {newState, url} = createState(body.key, 0, "view", content, language);
136+
if (body.token) {
137+
setToken(body.key, body.token);
138+
}
138139
const inputElement = document.createElement("input")
139140
const labelElement = document.createElement("label")
140141

@@ -166,10 +167,8 @@ document.querySelector("#delete").addEventListener("click", async () => {
166167
if (document.querySelector("#delete").disabled) return;
167168

168169
const {key} = getState();
169-
const updateToken = getUpdateToken(key);
170-
if (updateToken === "") {
171-
return;
172-
}
170+
const token = getToken(key);
171+
if (!token) return;
173172

174173
const deleteConfirm = window.confirm("Are you sure you want to delete this document? This action cannot be undone.")
175174
if (!deleteConfirm) return;
@@ -179,7 +178,7 @@ document.querySelector("#delete").addEventListener("click", async () => {
179178
let response = await fetch(`/documents/${key}`, {
180179
method: "DELETE",
181180
headers: {
182-
Authorization: updateToken
181+
Authorization: `Bearer ${token}`
183182
}
184183
});
185184
deleteButton.classList.remove("loading");
@@ -190,7 +189,7 @@ document.querySelector("#delete").addEventListener("click", async () => {
190189
console.error("error deleting document:", response);
191190
return;
192191
}
193-
deleteUpdateToken();
192+
deleteToken();
194193
const {newState, url} = createState("", 0, "edit", "", "");
195194
updateCode(newState);
196195
updatePage(newState);
@@ -217,43 +216,63 @@ document.querySelector("#share").addEventListener("click", async () => {
217216
if (document.querySelector("#share").disabled) return;
218217

219218
const {key} = getState();
220-
const updateToken = getUpdateToken(key);
221-
if (updateToken === "") {
219+
const token = getToken(key);
220+
if (!hasPermission(token, "share")) {
222221
await navigator.clipboard.writeText(window.location.href);
223222
return;
224223
}
225224

226-
document.querySelector("#share-permissions").checked = false;
227-
document.querySelector("#share-url").value = window.location.href;
225+
document.querySelector("#share-permissions-write").checked = false;
226+
document.querySelector("#share-permissions-delete").checked = false;
227+
document.querySelector("#share-permissions-share").checked = false;
228+
228229
document.querySelector("#share-dialog").showModal();
229230
});
230231

231232
document.querySelector("#share-dialog-close").addEventListener("click", () => {
232233
document.querySelector("#share-dialog").close();
233234
});
234235

235-
document.querySelector("#share-permissions").addEventListener("change", (event) => {
236-
const {key} = getState();
237-
const updateToken = getUpdateToken(key);
238-
if (updateToken === "") {
239-
return;
236+
document.querySelector("#share-copy").addEventListener("click", async () => {
237+
const permissions = [];
238+
if (document.querySelector("#share-permissions-write").checked) {
239+
permissions.push("write");
240+
}
241+
if (document.querySelector("#share-permissions-delete").checked) {
242+
permissions.push("delete");
243+
}
244+
if (document.querySelector("#share-permissions-share").checked) {
245+
permissions.push("share");
240246
}
241247

242-
const shareUrl = document.querySelector("#share-url");
243-
if (event.target.checked) {
244-
shareUrl.value = `${window.location.href}?token=${updateToken}`;
248+
if (permissions.length === 0) {
249+
await navigator.clipboard.writeText(window.location.href);
250+
document.querySelector("#share-dialog").close();
245251
return;
246252
}
247-
shareUrl.value = window.location.href;
248-
});
249253

250-
document.querySelector("#share-url").addEventListener("click", () => {
251-
document.querySelector("#share-url").select();
252-
});
254+
const {key} = getState();
255+
const token = getToken(key);
253256

254-
document.querySelector("#share-copy").addEventListener("click", async () => {
255-
const shareUrl = document.querySelector("#share-url");
256-
await navigator.clipboard.writeText(shareUrl.value);
257+
const response = await fetch(`/documents/${key}/share`, {
258+
method: "POST",
259+
body: JSON.stringify({permissions: permissions}),
260+
headers: {
261+
"Content-Type": "application/json",
262+
Authorization: `Bearer ${token}`
263+
}
264+
});
265+
266+
if (!response.ok) {
267+
const body = await response.json();
268+
showErrorPopup(body.message || response.statusText)
269+
console.error("error sharing document:", response);
270+
return;
271+
}
272+
273+
const body = await response.json()
274+
const shareUrl = window.location.href + "?token=" + body.token;
275+
await navigator.clipboard.writeText(shareUrl);
257276
document.querySelector("#share-dialog").close();
258277
});
259278

@@ -272,9 +291,9 @@ document.querySelector("#style").addEventListener("change", (event) => {
272291
document.querySelector("#versions").addEventListener("click", async (event) => {
273292
if (event.target && event.target.matches("input[type='radio']")) {
274293
const {key, version} = getState();
275-
let newVersion = event.target.value;
276-
if (event.target.parentElement.children.item(0).value === newVersion) {
277-
newVersion = ""
294+
let newVersion = parseInt(event.target.value);
295+
if (event.target.parentElement.children.item(0).value === `${newVersion}`) {
296+
newVersion = 0;
278297
}
279298
if (newVersion === version) return;
280299
const {newState, url} = await fetchVersion(key, newVersion)
@@ -314,26 +333,26 @@ function createState(key, version, mode, content, language) {
314333
return {newState: {key, version, mode, content: content.trim(), language}, url: `/${key}${version ? `#${version}` : ""}`};
315334
}
316335

317-
function getUpdateToken(key) {
336+
function getToken(key) {
318337
const documents = localStorage.getItem("documents")
319338
if (!documents) return ""
320-
const updateToken = JSON.parse(documents)[key]
321-
if (!updateToken) return ""
339+
const token = JSON.parse(documents)[key]
340+
if (!token) return ""
322341

323-
return updateToken
342+
return token
324343
}
325344

326-
function setUpdateToken(key, updateToken) {
345+
function setToken(key, token) {
327346
let documents = localStorage.getItem("documents")
328347
if (!documents) {
329348
documents = "{}"
330349
}
331350
const parsedDocuments = JSON.parse(documents)
332-
parsedDocuments[key] = updateToken
351+
parsedDocuments[key] = token
333352
localStorage.setItem("documents", JSON.stringify(parsedDocuments))
334353
}
335354

336-
function deleteUpdateToken() {
355+
function deleteToken() {
337356
const {key} = getState();
338357
const documents = localStorage.getItem("documents");
339358
if (!documents) return;
@@ -342,6 +361,13 @@ function deleteUpdateToken() {
342361
localStorage.setItem("documents", JSON.stringify(parsedDocuments));
343362
}
344363

364+
function hasPermission(token, permission) {
365+
if (!token) return false;
366+
const tokenSplit = token.split(".")
367+
if (tokenSplit.length !== 3) return false;
368+
return JSON.parse(atob(tokenSplit[1])).permissions.includes(permission);
369+
}
370+
345371
function updateCode(state) {
346372
const {mode, content} = state;
347373

@@ -365,7 +391,7 @@ function updateCode(state) {
365391

366392
function updatePage(state) {
367393
const {key, mode, content} = state;
368-
const updateToken = getUpdateToken(key);
394+
const token = getToken(key);
369395
// update page title
370396
if (key) {
371397
document.title = `gobin - ${key}`;
@@ -386,9 +412,7 @@ function updatePage(state) {
386412
saveButton.style.display = "none";
387413
editButton.disabled = false;
388414
editButton.style.display = "block";
389-
if (updateToken) {
390-
deleteButton.disabled = false;
391-
}
415+
deleteButton.disabled = !hasPermission(token, "delete");
392416
copyButton.disabled = false;
393417
rawButton.disabled = false;
394418
shareButton.disabled = false;

assets/style.css

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -98,21 +98,20 @@ html {
9898
border: none;
9999
border-radius: 1rem;
100100
padding: 1rem;
101-
102101
background-color: var(--bg-secondary);
103102
}
104103

105104
dialog::backdrop {
106105
background-color: rgba(0, 0, 0, 0.7);
107106
}
108107

109-
#share-dialog div {
108+
.share-dialog-header {
110109
display: flex;
111110
justify-content: space-between;
112111
align-items: center;
113112
}
114113

115-
#share-dialog div h2 {
114+
.share-dialog-header h2 {
116115
font-size: 1.5rem;
117116
font-weight: bold;
118117
margin: 0;
@@ -122,37 +121,19 @@ dialog::backdrop {
122121
background-image: var(--close);
123122
}
124123

125-
#share-url-label {
124+
.share-dialog-main {
126125
display: flex;
127-
justify-content: space-between;
128-
align-items: center;
129126
gap: 1rem;
127+
align-items: flex-end;
128+
justify-content: space-between;
130129
}
131130

132-
#share-url-label input {
133-
width: 100%;
134-
padding: 0.5rem;
135-
border: none;
136-
border-radius: 1rem;
137-
background-color: var(--bg-primary);
138-
color: var(--text-primary);
139-
font-family: monospace;
140-
font-size: 1rem;
141-
}
142-
143-
#share-url-label button {
144-
padding: 0.5rem;
145-
border: none;
146-
border-radius: 1rem;
147-
background-color: var(--bg-primary);
148-
color: var(--text-primary);
149-
font-family: monospace;
150-
font-size: 1rem;
151-
cursor: pointer;
152-
}
153-
154-
#share-url-label button:hover {
155-
opacity: 0.7;
131+
.share-dialog-permissions {
132+
display: grid;
133+
grid-template-columns: auto 1fr;
134+
gap: 1rem;
135+
width: fit-content;
136+
align-items: center;
156137
}
157138

158139
body {
@@ -271,6 +252,12 @@ nav {
271252
background-position: center;
272253
background-size: 1rem;
273254
cursor: pointer;
255+
color: var(--text-primary);
256+
}
257+
258+
#share-copy {
259+
width: fit-content;
260+
padding: 0.5rem;
274261
}
275262

276263
.button:hover, button:hover {

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ go 1.18
55
require (
66
github.com/go-chi/chi/v5 v5.0.8
77
github.com/go-chi/httprate v0.7.1
8+
github.com/go-jose/go-jose/v3 v3.0.0
89
github.com/jackc/pgx/v5 v5.2.0
910
github.com/jmoiron/sqlx v1.3.5
11+
golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9
1012
modernc.org/sqlite v1.20.4
1113
)
1214

0 commit comments

Comments
 (0)