fully working refresh tokens. No more expiring :)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user