Skip to content

Commit

Permalink
implement jwt tokens for sharing (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
topi314 authored Mar 4, 2023
1 parent 2fdfdd6 commit ce17b20
Show file tree
Hide file tree
Showing 14 changed files with 343 additions and 143 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ A successful request will return a `200 OK` response with a JSON body containing
{
"key": "hocwr6i6",
"version": 1,
"update_token": "kiczgez33j7qkvqdg9f7ksrd8jk88wba"
"token": "kiczgez33j7qkvqdg9f7ksrd8jk88wba"
}
```

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

### Update a document

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.
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.

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

### Delete a document

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

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

---

### Delete a document version

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

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

Expand Down
122 changes: 73 additions & 49 deletions assets/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ document.addEventListener("DOMContentLoaded", async () => {
const version = window.location.hash === "" ? 0 : parseInt(window.location.hash.slice(1));
const params = new URLSearchParams(window.location.search);
if (params.has("token")) {
setUpdateToken(key, params.get("token"));
setToken(key, params.get("token"));
}

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

const {key, version, content, language} = getState();
const {newState, url} = createState(getUpdateToken(key) === "" ? "" : key, 0, "edit", content, language);
const {key, content, language} = getState();
const {newState, url} = createState(hasPermission(getToken(key), "write") ? key : "", 0, "edit", content, language);
updateCode(newState);
updatePage(newState);
window.history.pushState(newState, "", url);
Expand All @@ -100,17 +100,17 @@ document.querySelector("#save").addEventListener("click", async () => {
if (document.querySelector("#save").disabled) return;
const {key, mode, content, language} = getState()
if (mode !== "edit") return;
const updateToken = getUpdateToken(key);
const token = getToken(key);
const saveButton = document.querySelector("#save");
saveButton.classList.add("loading");

let response;
if (key && updateToken) {
if (key && token) {
response = await fetch(`/documents/${key}`, {
method: "PATCH",
body: content,
headers: {
Authorization: updateToken,
Authorization: `Bearer ${token}`,
Language: language
}
});
Expand All @@ -132,9 +132,10 @@ document.querySelector("#save").addEventListener("click", async () => {
return;
}

const {newState, url} = createState(body.key, body.version, "view", content, language);
setUpdateToken(body.key, body.update_token);

const {newState, url} = createState(body.key, 0, "view", content, language);
if (body.token) {
setToken(body.key, body.token);
}
const inputElement = document.createElement("input")
const labelElement = document.createElement("label")

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

const {key} = getState();
const updateToken = getUpdateToken(key);
if (updateToken === "") {
return;
}
const token = getToken(key);
if (!token) return;

const deleteConfirm = window.confirm("Are you sure you want to delete this document? This action cannot be undone.")
if (!deleteConfirm) return;
Expand All @@ -179,7 +178,7 @@ document.querySelector("#delete").addEventListener("click", async () => {
let response = await fetch(`/documents/${key}`, {
method: "DELETE",
headers: {
Authorization: updateToken
Authorization: `Bearer ${token}`
}
});
deleteButton.classList.remove("loading");
Expand All @@ -190,7 +189,7 @@ document.querySelector("#delete").addEventListener("click", async () => {
console.error("error deleting document:", response);
return;
}
deleteUpdateToken();
deleteToken();
const {newState, url} = createState("", 0, "edit", "", "");
updateCode(newState);
updatePage(newState);
Expand All @@ -217,43 +216,63 @@ document.querySelector("#share").addEventListener("click", async () => {
if (document.querySelector("#share").disabled) return;

const {key} = getState();
const updateToken = getUpdateToken(key);
if (updateToken === "") {
const token = getToken(key);
if (!hasPermission(token, "share")) {
await navigator.clipboard.writeText(window.location.href);
return;
}

document.querySelector("#share-permissions").checked = false;
document.querySelector("#share-url").value = window.location.href;
document.querySelector("#share-permissions-write").checked = false;
document.querySelector("#share-permissions-delete").checked = false;
document.querySelector("#share-permissions-share").checked = false;

document.querySelector("#share-dialog").showModal();
});

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

document.querySelector("#share-permissions").addEventListener("change", (event) => {
const {key} = getState();
const updateToken = getUpdateToken(key);
if (updateToken === "") {
return;
document.querySelector("#share-copy").addEventListener("click", async () => {
const permissions = [];
if (document.querySelector("#share-permissions-write").checked) {
permissions.push("write");
}
if (document.querySelector("#share-permissions-delete").checked) {
permissions.push("delete");
}
if (document.querySelector("#share-permissions-share").checked) {
permissions.push("share");
}

const shareUrl = document.querySelector("#share-url");
if (event.target.checked) {
shareUrl.value = `${window.location.href}?token=${updateToken}`;
if (permissions.length === 0) {
await navigator.clipboard.writeText(window.location.href);
document.querySelector("#share-dialog").close();
return;
}
shareUrl.value = window.location.href;
});

document.querySelector("#share-url").addEventListener("click", () => {
document.querySelector("#share-url").select();
});
const {key} = getState();
const token = getToken(key);

document.querySelector("#share-copy").addEventListener("click", async () => {
const shareUrl = document.querySelector("#share-url");
await navigator.clipboard.writeText(shareUrl.value);
const response = await fetch(`/documents/${key}/share`, {
method: "POST",
body: JSON.stringify({permissions: permissions}),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
}
});

if (!response.ok) {
const body = await response.json();
showErrorPopup(body.message || response.statusText)
console.error("error sharing document:", response);
return;
}

const body = await response.json()
const shareUrl = window.location.href + "?token=" + body.token;
await navigator.clipboard.writeText(shareUrl);
document.querySelector("#share-dialog").close();
});

Expand All @@ -272,9 +291,9 @@ document.querySelector("#style").addEventListener("change", (event) => {
document.querySelector("#versions").addEventListener("click", async (event) => {
if (event.target && event.target.matches("input[type='radio']")) {
const {key, version} = getState();
let newVersion = event.target.value;
if (event.target.parentElement.children.item(0).value === newVersion) {
newVersion = ""
let newVersion = parseInt(event.target.value);
if (event.target.parentElement.children.item(0).value === `${newVersion}`) {
newVersion = 0;
}
if (newVersion === version) return;
const {newState, url} = await fetchVersion(key, newVersion)
Expand Down Expand Up @@ -314,26 +333,26 @@ function createState(key, version, mode, content, language) {
return {newState: {key, version, mode, content: content.trim(), language}, url: `/${key}${version ? `#${version}` : ""}`};
}

function getUpdateToken(key) {
function getToken(key) {
const documents = localStorage.getItem("documents")
if (!documents) return ""
const updateToken = JSON.parse(documents)[key]
if (!updateToken) return ""
const token = JSON.parse(documents)[key]
if (!token) return ""

return updateToken
return token
}

function setUpdateToken(key, updateToken) {
function setToken(key, token) {
let documents = localStorage.getItem("documents")
if (!documents) {
documents = "{}"
}
const parsedDocuments = JSON.parse(documents)
parsedDocuments[key] = updateToken
parsedDocuments[key] = token
localStorage.setItem("documents", JSON.stringify(parsedDocuments))
}

function deleteUpdateToken() {
function deleteToken() {
const {key} = getState();
const documents = localStorage.getItem("documents");
if (!documents) return;
Expand All @@ -342,6 +361,13 @@ function deleteUpdateToken() {
localStorage.setItem("documents", JSON.stringify(parsedDocuments));
}

function hasPermission(token, permission) {
if (!token) return false;
const tokenSplit = token.split(".")
if (tokenSplit.length !== 3) return false;
return JSON.parse(atob(tokenSplit[1])).permissions.includes(permission);
}

function updateCode(state) {
const {mode, content} = state;

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

function updatePage(state) {
const {key, mode, content} = state;
const updateToken = getUpdateToken(key);
const token = getToken(key);
// update page title
if (key) {
document.title = `gobin - ${key}`;
Expand All @@ -386,9 +412,7 @@ function updatePage(state) {
saveButton.style.display = "none";
editButton.disabled = false;
editButton.style.display = "block";
if (updateToken) {
deleteButton.disabled = false;
}
deleteButton.disabled = !hasPermission(token, "delete");
copyButton.disabled = false;
rawButton.disabled = false;
shareButton.disabled = false;
Expand Down
47 changes: 17 additions & 30 deletions assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,20 @@ html {
border: none;
border-radius: 1rem;
padding: 1rem;

background-color: var(--bg-secondary);
}

dialog::backdrop {
background-color: rgba(0, 0, 0, 0.7);
}

#share-dialog div {
.share-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
}

#share-dialog div h2 {
.share-dialog-header h2 {
font-size: 1.5rem;
font-weight: bold;
margin: 0;
Expand All @@ -122,37 +121,19 @@ dialog::backdrop {
background-image: var(--close);
}

#share-url-label {
.share-dialog-main {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
align-items: flex-end;
justify-content: space-between;
}

#share-url-label input {
width: 100%;
padding: 0.5rem;
border: none;
border-radius: 1rem;
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: monospace;
font-size: 1rem;
}

#share-url-label button {
padding: 0.5rem;
border: none;
border-radius: 1rem;
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: monospace;
font-size: 1rem;
cursor: pointer;
}

#share-url-label button:hover {
opacity: 0.7;
.share-dialog-permissions {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
width: fit-content;
align-items: center;
}

body {
Expand Down Expand Up @@ -271,6 +252,12 @@ nav {
background-position: center;
background-size: 1rem;
cursor: pointer;
color: var(--text-primary);
}

#share-copy {
width: fit-content;
padding: 0.5rem;
}

.button:hover, button:hover {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ go 1.18
require (
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/httprate v0.7.1
github.com/go-jose/go-jose/v3 v3.0.0
github.com/jackc/pgx/v5 v5.2.0
github.com/jmoiron/sqlx v1.3.5
golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9
modernc.org/sqlite v1.20.4
)

Expand Down
Loading

0 comments on commit ce17b20

Please sign in to comment.