fully working refresh tokens. No more expiring :)

This commit is contained in:
2025-09-21 15:43:14 +01:00
parent a3345afbfa
commit 3015d7bac2
8 changed files with 1142 additions and 842 deletions

View File

@@ -3,44 +3,57 @@ import { platform } from "@tauri-apps/plugin-os";
import { jwtDecode } from "jwt-decode";
import { Component, ParentProps, Show } from "solid-js";
import { save_token } from "tauri-plugin-ios-shared-token-api";
import { InferOutput, literal, number, object, parse, pipe, string, transform } from "valibot";
export const isTokenValid = (): boolean => {
const token = localStorage.getItem("access");
const token = localStorage.getItem("access");
if (token == null) {
return false;
}
if (token == null) {
return false;
}
try {
jwtDecode(token);
return true;
} catch (err) {
return false;
}
try {
jwtDecode(token);
return true;
} catch (err) {
return false;
}
};
const accessTokenPropertiesValidator = object({
UserID: string(),
Type: literal('access'),
exp: pipe(number(), transform(i => new Date(i)))
});
export const getTokenProperties = (token: string): InferOutput<typeof accessTokenPropertiesValidator> => {
const decoded = jwtDecode(token);
return parse(accessTokenPropertiesValidator, decoded);
}
export const ProtectedRoute: Component<ParentProps> = (props) => {
const isValid = isTokenValid();
const isValid = isTokenValid();
if (isValid) {
const token = localStorage.getItem("access");
if (token == null) {
throw new Error("unreachable");
}
if (isValid) {
const token = localStorage.getItem("access");
if (token == null) {
throw new Error("unreachable");
}
if (platform() === "ios") {
// iOS share extension is a seperate process to the App.
// Therefore, we need to share our access token somewhere both the App & Share Extension can access
// This involves App Groups.
save_token(token)
.then(() => console.log("Saved token!!!"))
.catch((e) => console.error(e));
}
}
if (platform() === "ios") {
// iOS share extension is a seperate process to the App.
// Therefore, we need to share our access token somewhere both the App & Share Extension can access
// This involves App Groups.
save_token(token)
.then(() => console.log("Saved token!!!"))
.catch((e) => console.error(e));
}
}
return (
<Show when={isValid} fallback={<Navigate href="/login" />}>
{props.children}
</Show>
);
return (
<Show when={isValid} fallback={<Navigate href="/login" />}>
{props.children}
</Show>
);
};

View File

@@ -1,4 +1,6 @@
import { getTokenProperties } from "@components/protected-route";
import { fetch } from "@tauri-apps/plugin-http";
import { jwtDecode } from "jwt-decode";
import {
type InferOutput,
@@ -6,6 +8,7 @@ import {
literal,
null_,
nullable,
parse,
pipe,
safeParse,
strictObject,
@@ -31,14 +34,40 @@ const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => {
});
};
const getBaseAuthorizedRequest = ({
const refreshTokenValidator = strictObject({
access: string(),
})
const getBaseAuthorizedRequest = async ({
path,
body,
method,
}: BaseRequestParams): Request => {
}: BaseRequestParams): Promise<Request> => {
let accessToken = localStorage.getItem("access")?.toString();
const refreshToken = localStorage.getItem("refresh")?.toString();
if (accessToken == null && refreshToken == null) {
throw new Error("your are not logged in")
}
const isValidAccessToken = accessToken != null && getTokenProperties(accessToken).exp.getTime() > Date.now()
if (!isValidAccessToken) {
const newAccessToken = await fetch(getBaseRequest({
path: 'auth/refresh', method: "POST", body: JSON.stringify({
refresh: refreshToken,
})
})).then(r => r.json());
const { access } = parse(refreshTokenValidator, newAccessToken);
localStorage.setItem("access", access);
accessToken = access
}
return new Request(`${base}/${path}`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("access")?.toString()}`,
Authorization: `Bearer ${accessToken}`,
},
body,
method,
@@ -55,7 +84,7 @@ export const sendImageFile = async (
imageName: string,
file: File,
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
const request = getBaseAuthorizedRequest({
const request = await getBaseAuthorizedRequest({
path: `images/${imageName}`,
body: file,
method: "POST",
@@ -77,7 +106,7 @@ export const sendImageFile = async (
export const deleteImage = async (
imageID: string
): Promise<void> => {
const request = getBaseAuthorizedRequest({
const request = await getBaseAuthorizedRequest({
path: `images/${imageID}`,
method: "DELETE",
});
@@ -86,7 +115,7 @@ export const deleteImage = async (
}
export const deleteImageFromStack = async (listID: string, imageID: string): Promise<void> => {
const request = getBaseAuthorizedRequest({
const request = await getBaseAuthorizedRequest({
path: `stacks/${listID}/${imageID}`,
method: "DELETE",
});
@@ -104,7 +133,7 @@ export const sendImage = async (
imageName: string,
base64Image: string,
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
const request = getBaseAuthorizedRequest({
const request = await getBaseAuthorizedRequest({
path: `images/${imageName}`,
body: base64Image,
method: "POST",
@@ -222,7 +251,7 @@ export type JustTheImageWhatAreTheseNames = InferOutput<
export const getUserImages = async (): Promise<
InferOutput<typeof imageRequestValidator>
> => {
const request = getBaseAuthorizedRequest({ path: "images" });
const request = await getBaseAuthorizedRequest({ path: "images" });
const res = await fetch(request).then((res) => res.json());
@@ -283,7 +312,7 @@ export const createList = async (
title: string,
description: string,
): Promise<void> => {
const request = getBaseAuthorizedRequest({
const request = await getBaseAuthorizedRequest({
path: "stacks",
method: "POST",
body: JSON.stringify({ title, description }),

View File

@@ -52,6 +52,12 @@ export const List: Component = () => {
const { lists, onDeleteImageFromStack } = useSearchImageContext();
// TODO: make sure this is up to date. Put it behind a resource.
const accessToken = localStorage.getItem("access");
if (accessToken == null) {
return <>Ermm... Access token is not set :(</>
}
const list = () => lists().find((l) => l.ID === listId);
return (