Skip to content

Commit

Permalink
Merge pull request #74 from fumiX/69-Fix-Short-Description-Not-Saved-…
Browse files Browse the repository at this point in the history
…On-Edit

persist description
  • Loading branch information
floscher authored May 16, 2023
2 parents cb87f95 + 9b5134c commit 7a3911a
Show file tree
Hide file tree
Showing 28 changed files with 383 additions and 87 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@types/bootstrap": "^5.2.6",
"@types/luxon": "^3.2.0",
"@types/node": "^18.15.1",
"@unhead/vue": "^1.1.26",
"@vitejs/plugin-vue": "^4.1.0",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.0",
Expand Down
25 changes: 14 additions & 11 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,16 @@ const setOperator = (operator: string) => {
router.replace({ query: { ...route.query, operator: operator } });
};
const getLoggedInUser = async (): Promise<User> => {
return AuthEndpoints.getLoggedInUser().then((it) => it.user);
};
const setLoginUSerAndPermissions = async () => {
loggedInUser.value = await getLoggedInUser();
userPermissions.value = permissionsForUser(loggedInUser.value);
const setLoginUserAndPermissions = async () => {
AuthEndpoints.getLoggedInUser()
.then((oauthAccount) => {
loggedInUser.value = oauthAccount.user;
userPermissions.value = permissionsForUser(oauthAccount.user);
})
.catch(() => {
loggedInUser.value = null;
userPermissions.value = null;
});
};
watch(route, async (value) => {
Expand All @@ -149,14 +152,14 @@ watch(route, async (value) => {
}
});
onMounted(async () => {
onMounted(() => {
// listen for token-changed event to gracefully handle login/logout
window.addEventListener("token-changed", async (event) => {
window.addEventListener("token-changed", (event) => {
if (!loggedInUser.value) {
setLoginUSerAndPermissions();
setLoginUserAndPermissions();
}
});
setLoginUSerAndPermissions();
setLoginUserAndPermissions();
});
const startSearch = (search: string, operator: string = "and") => {
Expand Down
4 changes: 3 additions & 1 deletion client/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import i18n from "@client/plugins/i18n.js";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { createHead } from "@unhead/vue";
import "bootstrap";
import { DateTime } from "luxon";
import { createApp } from "vue";
Expand All @@ -16,7 +17,8 @@ declare module "@vue/runtime-core" {
}

const app = createApp(App);

const head = createHead();
app.use(head);
app.use(i18n);

app.config.globalProperties.$luxonDateTime = DateTime;
Expand Down
16 changes: 10 additions & 6 deletions client/src/util/api-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { loadIdToken } from "@client/util/storage.js";
import { loadIdToken, updateIdToken } from "@client/util/storage.js";
import type {
AiSummaryData,
DataUrl,
Expand All @@ -9,7 +9,7 @@ import type {
OAuthAccount,
SupportedImageMimeType,
} from "@fumix/fu-blog-common";
import { imageBytesToDataUrl } from "@fumix/fu-blog-common";
import { HttpHeader, imageBytesToDataUrl } from "@fumix/fu-blog-common";

export type ApiUrl = `/api/${string}`;

Expand All @@ -28,9 +28,9 @@ async function callServer<
const token = authenticated ? loadIdToken() : undefined;
const headers: HeadersInit = { Accept: responseType };
if (token) {
headers["X-OAuth-Type"] = token.type;
headers["X-OAuth-Issuer"] = token.issuer;
headers["Authorization"] = `Bearer ${token.id_token}`;
headers[HttpHeader.Request.OAUTH_TYPE] = token.type;
headers[HttpHeader.Request.OAUTH_ISSUER] = token.issuer;
headers[HttpHeader.Request.AUTHORIZATION] = `Bearer ${token.id_token}`;
}
if (!(payload instanceof ApiRequestJsonPayloadWithFiles)) {
headers["Content-Type"] = contentType;
Expand All @@ -39,11 +39,15 @@ async function callServer<
return fetch(url, {
method,
headers,
body: payload === null || payload instanceof ApiRequestJsonPayloadWithFiles ? toFormData(payload) : JSON.stringify(payload),
body: payload === null || payload instanceof ApiRequestJsonPayloadWithFiles ? toFormData(payload) : JSON.stringify(payload.json),
}).then(async (response) => {
if (!response.ok) {
throw new Error("Error response: " + response.status + " " + response.statusText);
}
const refreshedIdToken = response.headers.get(HttpHeader.Response.OAUTH_REFRESHED_ID_TOKEN);
if (refreshedIdToken) {
updateIdToken(refreshedIdToken);
}
if (responseType === "application/json") {
return response
.json()
Expand Down
21 changes: 19 additions & 2 deletions client/src/util/storage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SavedOAuthToken, UserTheme } from "@fumix/fu-blog-common";
import { isOAuthType } from "@fumix/fu-blog-common";
import type { JsonWebToken, SavedOAuthToken, UserTheme } from "@fumix/fu-blog-common";
import { base64UrlToBuffer, isOAuthType } from "@fumix/fu-blog-common";

type OAuthState = { key: string; redirect_uri?: string };
const idTokenKey = "id_token";
Expand All @@ -19,6 +19,7 @@ export function loadOauthStateByKey(key: string | undefined | null): OAuthState

export function saveIdToken(token: SavedOAuthToken | null): void {
if (token) {
logIdTokenValidity(token.id_token);
saveToStorageAsString(window.localStorage, idTokenKey, token.id_token);
saveToStorageAsString(window.localStorage, oauthTypeKey, token.type);
saveToStorageAsString(window.localStorage, oauthIssuerKey, token.issuer);
Expand All @@ -30,6 +31,22 @@ export function saveIdToken(token: SavedOAuthToken | null): void {
window.dispatchEvent(new CustomEvent("token-changed", { detail: token }));
}

export function updateIdToken(idToken: JsonWebToken | null): void {
if (idToken) {
logIdTokenValidity(idToken);
saveToStorageAsString(window.localStorage, idTokenKey, idToken);
} else {
removeKeyFromStorage(window.localStorage, idTokenKey);
}
}

function logIdTokenValidity(idToken: JsonWebToken): void {
const payload = idToken.split(".", 3)[1];
if (payload) {
console.debug("New ID token saved, valid until ", new Date(1000 * JSON.parse(base64UrlToBuffer(payload).toString("utf-8"))["exp"]));
}
}

export function loadIdToken(): SavedOAuthToken | undefined {
const id_token = loadFromStorageAsStringOrUndefined(window.localStorage, idTokenKey);
const type = loadFromStorageAsStringOrUndefined(window.localStorage, oauthTypeKey);
Expand Down
4 changes: 2 additions & 2 deletions client/src/views/LoginView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ const register = async () => {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
savedToken: userInfo.value?.token,
fullName: fullName,
username: username,
fullName: fullName.value,
username: username.value,
profilePictureUrl: userInfo.value?.user?.profilePictureUrl,
}),
}).then(async (it) => {
Expand Down
15 changes: 9 additions & 6 deletions client/src/views/PostFormView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
</div>

<div class="form-floating mb-3">
<div contenteditable="true" style="overflow-y: scroll; height: 6rem" class="form-control">
{{ form.description }}
</div>
<textarea
v-model="form.description"
style="overflow-y: scroll; height: 6rem"
class="form-control"
id="description"
></textarea>
<label for="description">{{ t("posts.form.description") }}</label>
</div>

Expand Down Expand Up @@ -356,16 +359,16 @@ const dropMarkdown = (evt: DragEvent) => {
}
};
const openFileDialog = () => {
const openFileDialog = (): void => {
document.getElementById("file")?.click();
};
const highlightDropzone = (event: DragEvent, value: boolean = false) => {
const highlightDropzone = (event: DragEvent, value: boolean = false): void => {
event.preventDefault();
dropzoneHighlight.value = value && [...(event.dataTransfer?.items ?? [])].some((it) => it.kind === "file");
};
const handleFileChange = (e: Event) => {
const handleFileChange = (e: Event): void => {
if (e instanceof DragEvent) {
e.preventDefault();
const items: DataTransferItemList | undefined = e.dataTransfer?.items as DataTransferItemList;
Expand Down
6 changes: 6 additions & 0 deletions client/src/views/PostView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import PostNotAvailable from "@client/components/PostNotAvailable.vue";
import { faClock } from "@fortawesome/free-regular-svg-icons";
import { faArrowLeft, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
import type { ConfirmDialogData, Post, UserRolePermissionsType } from "@fumix/fu-blog-common";
import { useSeoMeta } from "@unhead/vue";
import { onMounted, type PropType, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
Expand All @@ -110,6 +111,11 @@ onMounted(async () => {
try {
const route = useRoute();
const id = route.params.id;
useSeoMeta({
ogImage: `${window.location.protocol}//${window.location.hostname}/api/posts/${route.params.id}/og-image`,
ogType: "article",
ogUrl: `${window.location.protocol}//${window.location.hostname}/posts/post/${route.params.id}`,
});
const res = await fetch(`/api/posts/${id}`);
const response = await res.json();
post.value = response.data;
Expand Down
2 changes: 2 additions & 0 deletions common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "./dto/AppSettingsDto.js";
export * from "./dto/HyperlinkDto.js";
export * from "./dto/OAuthProvidersDto.js";
export * from "./dto/SupportedImageMimeType.js";
export * from "./dto/oauth/JwtToken.js";
export * from "./dto/oauth/OAuthCodeDto.js";
export * from "./dto/oauth/OAuthType.js";
export * from "./dto/oauth/OAuthUserInfoDto.js";
Expand All @@ -21,6 +22,7 @@ export * from "./entity/UserRole.js";
export * from "./entity/permission/UserRolePermissions.js";
export * from "./markdown-converter-common.js";
export * from "./util/base64.js";
export * from "./util/cookie-header-helpers.js";
export * from "./util/filesize.js";
export * from "./util/markdown.js";
export * from "./util/mimeType.js";
Expand Down
8 changes: 4 additions & 4 deletions common/src/util/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ export function bytesToBase64URL(bytes: Uint8Array): string {
return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}

export function base64ToBytes(base64: string): Uint8Array {
return new Uint8Array(Buffer.from(base64, "base64"));
export function base64ToBuffer(base64: string): Buffer {
return Buffer.from(base64, "base64");
}

export function base64UrlToBytes(base64: string): Uint8Array {
return new Uint8Array(Buffer.from(base64, "base64url"));
export function base64UrlToBuffer(base64: string): Buffer {
return Buffer.from(base64.replace(/-/g, "+").replace(/_/g, "/"), "base64");
}

export function bytesToDataUrl(mimeType: string, bytes: Uint8Array): DataUrl {
Expand Down
39 changes: 39 additions & 0 deletions common/src/util/cookie-header-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Request, Response } from "express";

export class HttpHeader {
public static Response = {
/**
* This header passes a refreshed OAuth token from the server
* back to the client, as soon as the old one expired.
*/
OAUTH_REFRESHED_ID_TOKEN: "X-OAuth-Token",
};
/**
* Headers that are passed from the client to the server
*/
public static Request = {
AUTHORIZATION: "Authorization",
OAUTH_ISSUER: "X-OAuth-Issuer",
OAUTH_TYPE: "X-OAuth-Type",
};
}
export class Cookies {
private static REFRESH_TOKEN = "refresh";

static setRefreshTokenCookie: (res: Response, newRefreshToken: string | undefined) => void = (res, newRefreshToken) => {
if (newRefreshToken) {
res.cookie(Cookies.REFRESH_TOKEN, newRefreshToken, { sameSite: "strict", secure: true, httpOnly: true });
}
};

static getCookies: (req: Request) => { [key: string]: string } = (req) => {
return Object.fromEntries(
req
.header("Cookie")
?.split(";")
?.map((it) => it.split("=", 2))
?.filter((it) => it && it.length === 2 && it.every((x) => x.trim().length > 0))
?.map((it) => [it[0].trim(), it[1].trim()]) ?? [],
);
};
}
Loading

0 comments on commit 7a3911a

Please sign in to comment.