Make auth JWT-only for Capacitor WebView origins (#22)
Unit Tests / test (pull_request) Successful in 11s

Drop CSRF cookie dependency (js-cookie), store tokens via localStorage with
optional Capacitor Preferences mirror, and attach Authorization on every
request so Android/iOS shells work without third-party cookies.
This commit is contained in:
2026-07-26 16:06:28 -05:00
parent 0fc8739d26
commit 8ac0b2a4ff
16 changed files with 503 additions and 111 deletions
-9
View File
@@ -23,7 +23,6 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"chroma-js": "^3.1.2", "chroma-js": "^3.1.2",
"formik": "^2.4.6", "formik": "^2.4.6",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-to-jsx": "^7.7.2", "markdown-to-jsx": "^7.7.2",
@@ -12814,14 +12813,6 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-1
View File
@@ -18,7 +18,6 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"chroma-js": "^3.1.2", "chroma-js": "^3.1.2",
"formik": "^2.4.6", "formik": "^2.4.6",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-to-jsx": "^7.7.2", "markdown-to-jsx": "^7.7.2",
+102 -57
View File
@@ -1,17 +1,31 @@
import axios from "axios"; import axios from "axios";
import Cookies from "js-cookie"; import {
authorizationHeader,
clearTokens,
getAccessToken,
getRefreshToken,
setTokens,
} from "./llm-fe/auth/tokenStorage";
import {
decodeJwtPayload,
isTokenNotValidError,
} from "./llm-fe/auth/jwtHelpers";
import { redirectToAppPath } from "./llm-fe/platform/nativePlatform"; import { redirectToAppPath } from "./llm-fe/platform/nativePlatform";
const baseURL = process.env.REACT_APP_BACKEND_REST_API_BASE_URL; const baseURL = process.env.REACT_APP_BACKEND_REST_API_BASE_URL;
/**
* Shared JWT axios client — Authorization bearer only, no cookies/CSRF (#22).
* Works for browser + Capacitor WebView origins (https://localhost / capacitor://localhost).
*/
export const axiosInstance = axios.create({ export const axiosInstance = axios.create({
baseURL: baseURL, baseURL: baseURL,
timeout: 5000, timeout: 5000,
headers: { headers: {
Authorization: "JWT " + localStorage.getItem("access_token"),
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
}, },
withCredentials: false,
}); });
export const cleanAxiosInstance = axios.create({ export const cleanAxiosInstance = axios.create({
@@ -21,77 +35,108 @@ export const cleanAxiosInstance = axios.create({
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
}, },
withCredentials: false,
}); });
export const axiosInstanceCSRF = axios.create({ /**
baseURL: baseURL, * Apply access token to axios defaults (login / refresh).
timeout: 5000, * @param {string|null} access
headers: { */
"X-CSRFToken": Cookies.get("csrftoken"), // Include CSRF token in headers export function applyAccessToken(access) {
}, const header = authorizationHeader(access);
withCredentials: true, if (header) {
}); axiosInstance.defaults.headers.common["Authorization"] = header;
} else {
delete axiosInstance.defaults.headers.common["Authorization"];
}
}
applyAccessToken(getAccessToken());
axiosInstance.interceptors.request.use((config) => { axiosInstance.interceptors.request.use((config) => {
config.timeout = 100000; config.timeout = 100000;
config.withCredentials = false;
const header = authorizationHeader(getAccessToken());
if (header) {
config.headers = config.headers || {};
config.headers.Authorization = header;
} else if (config.headers) {
delete config.headers.Authorization;
}
return config; return config;
}); });
/**
* @param {string} refreshToken
* @returns {Promise<{ access: string, refresh?: string }>}
*/
export async function refreshAccessToken(refreshToken) {
const response = await axiosInstance.post("/token/refresh/", {
refresh: refreshToken,
});
await setTokens(response.data.access, response.data.refresh ?? refreshToken);
applyAccessToken(response.data.access);
return response.data;
}
export { decodeJwtPayload, isTokenNotValidError };
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
(response) => response, (response) => response,
(error) => { async (error) => {
const originalRequest = error.config; const originalRequest = error.config;
if (!error.response || !originalRequest) {
// Prevent infinite loop
if (
error.response.status === 401 &&
originalRequest.url === baseURL + "/token/refresh/"
) {
// Hash-safe on Capacitor (#24); plain path on web BrowserRouter.
redirectToAppPath("/signin/");
//console.log('remove the local storage here')
return Promise.reject(error); return Promise.reject(error);
} }
if ( const requestUrl = originalRequest.url || "";
error.response.data.code === "token_not_valid" && const isRefreshCall =
error.response.status === 401 && requestUrl.includes("/token/refresh/") ||
error.response.statusText === "Unauthorized" requestUrl === `${baseURL}/token/refresh/`;
) {
const refresh_token = localStorage.getItem("refresh_token");
if (refresh_token) { if (error.response.status === 401 && isRefreshCall) {
const tokenParts = JSON.parse(atob(refresh_token.split(".")[1])); await clearTokens();
applyAccessToken(null);
const now = Math.ceil(Date.now() / 1000); // Hash-safe on Capacitor (#24); plain path on web BrowserRouter.
//console.log(tokenParts.exp) redirectToAppPath("/signin/");
return Promise.reject(error);
if (tokenParts.exp > now) {
return axiosInstance
.post("/token/refresh/", { refresh: refresh_token })
.then((response) => {
localStorage.setItem("access_token", response.data.access);
localStorage.setItem("refresh_token", response.data.refresh);
axiosInstance.defaults.headers["Authorization"] =
"JWT " + response.data.access;
originalRequest.headers["Authorization"] =
"JWT " + response.data.access;
return axiosInstance(originalRequest);
})
.catch((err) => {
console.log(err);
});
} else {
console.log("Refresh token is expired");
redirectToAppPath("/signin/");
}
} else {
console.log("Refresh token not available");
redirectToAppPath("/signin/");
}
} }
if (isTokenNotValidError(error) && !originalRequest._retry) {
originalRequest._retry = true;
const refresh_token = getRefreshToken();
if (!refresh_token) {
await clearTokens();
applyAccessToken(null);
redirectToAppPath("/signin/");
return Promise.reject(error);
}
const tokenParts = decodeJwtPayload(refresh_token);
const now = Math.ceil(Date.now() / 1000);
if (tokenParts?.exp != null && tokenParts.exp > now) {
try {
const data = await refreshAccessToken(refresh_token);
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = authorizationHeader(
data.access,
);
return axiosInstance(originalRequest);
} catch (err) {
await clearTokens();
applyAccessToken(null);
redirectToAppPath("/signin/");
return Promise.reject(err);
}
}
await clearTokens();
applyAccessToken(null);
redirectToAppPath("/signin/");
}
return Promise.reject(error); return Promise.reject(error);
}, },
); );
+32
View File
@@ -0,0 +1,32 @@
/**
* Pure JWT auth helpers (no axios) so unit tests avoid ESM transform issues.
*/
/**
* Decode JWT payload without verifying signature (client-side checks only).
* @param {string} token
* @returns {{ exp?: number } | null}
*/
export function decodeJwtPayload(token) {
try {
const parts = token.split(".");
if (parts.length < 2) {
return null;
}
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4);
return JSON.parse(atob(padded));
} catch {
return null;
}
}
/**
* @param {{ response?: { status?: number, data?: { code?: string } } }} error
* @returns {boolean}
*/
export function isTokenNotValidError(error) {
const status = error.response?.status;
const code = error.response?.data?.code;
return status === 401 && code === "token_not_valid";
}
+47
View File
@@ -0,0 +1,47 @@
import { decodeJwtPayload, isTokenNotValidError } from './jwtHelpers';
import {
ACCESS_TOKEN_KEY,
authorizationHeader,
clearTokens,
setTokens,
} from './tokenStorage';
describe('jwtHelpers', () => {
it('decodeJwtPayload reads exp claim', () => {
const payload = btoa(JSON.stringify({ exp: 1999999999 }));
const token = `h.${payload}.s`;
expect(decodeJwtPayload(token)).toEqual({ exp: 1999999999 });
});
it('decodeJwtPayload returns null for garbage', () => {
expect(decodeJwtPayload('not-a-jwt')).toBeNull();
});
it('isTokenNotValidError detects SimpleJWT shape', () => {
expect(
isTokenNotValidError({
response: { status: 401, data: { code: 'token_not_valid' } },
}),
).toBe(true);
expect(
isTokenNotValidError({
response: { status: 403, data: { code: 'token_not_valid' } },
}),
).toBe(false);
});
});
describe('JWT-only auth contract (#22)', () => {
beforeEach(() => {
localStorage.clear();
});
it('stores tokens without cookies and builds Authorization header', async () => {
await setTokens('access-xyz', 'refresh-xyz');
expect(document.cookie).not.toMatch(/csrftoken/);
expect(authorizationHeader()).toBe('JWT access-xyz');
await clearTokens();
expect(localStorage.getItem(ACCESS_TOKEN_KEY)).toBeNull();
expect(authorizationHeader()).toBeNull();
});
});
+146
View File
@@ -0,0 +1,146 @@
/**
* Header-only JWT storage for web + Capacitor WebViews (#22).
*
* localStorage is the sync source of truth (axios interceptors).
* When Capacitor Preferences is present, tokens are mirrored there so native
* builds do not depend on third-party cookies (blocked in WKWebView).
*/
export const ACCESS_TOKEN_KEY = "access_token";
export const REFRESH_TOKEN_KEY = "refresh_token";
/**
* @returns {{ get?: Function, set?: Function, remove?: Function } | undefined}
*/
function getPreferencesPlugin() {
if (typeof window === "undefined") {
return undefined;
}
return window.Capacitor?.Plugins?.Preferences;
}
/**
* @param {string} key
* @returns {string|null}
*/
export function getToken(key) {
if (typeof localStorage === "undefined") {
return null;
}
return localStorage.getItem(key);
}
/**
* @returns {string|null}
*/
export function getAccessToken() {
return getToken(ACCESS_TOKEN_KEY);
}
/**
* @returns {string|null}
*/
export function getRefreshToken() {
return getToken(REFRESH_TOKEN_KEY);
}
/**
* Persist tokens to localStorage and best-effort mirror to Capacitor Preferences.
* @param {string} access
* @param {string} [refresh]
* @returns {Promise<void>}
*/
export async function setTokens(access, refresh) {
if (typeof localStorage !== "undefined") {
localStorage.setItem(ACCESS_TOKEN_KEY, access);
if (refresh != null) {
localStorage.setItem(REFRESH_TOKEN_KEY, refresh);
}
}
const Preferences = getPreferencesPlugin();
if (!Preferences?.set) {
return;
}
const writes = [Preferences.set({ key: ACCESS_TOKEN_KEY, value: access })];
if (refresh != null) {
writes.push(Preferences.set({ key: REFRESH_TOKEN_KEY, value: refresh }));
}
await Promise.all(writes.map((p) => Promise.resolve(p).catch(() => undefined)));
}
/**
* Clear tokens from localStorage and Preferences.
* @returns {Promise<void>}
*/
export async function clearTokens() {
if (typeof localStorage !== "undefined") {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
}
const Preferences = getPreferencesPlugin();
if (!Preferences?.remove) {
return;
}
await Promise.all(
[ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY].map((key) =>
Promise.resolve(Preferences.remove({ key })).catch(() => undefined),
),
);
}
/**
* On native, hydrate localStorage from Preferences if local keys are empty.
* Safe no-op on web / when Preferences missing.
* @returns {Promise<{ access: string|null, refresh: string|null }>}
*/
export async function hydrateTokensFromNativeStorage() {
const Preferences = getPreferencesPlugin();
if (!Preferences?.get) {
return { access: getAccessToken(), refresh: getRefreshToken() };
}
let access = getAccessToken();
let refresh = getRefreshToken();
if (!access) {
try {
const result = await Preferences.get({ key: ACCESS_TOKEN_KEY });
if (result?.value) {
localStorage.setItem(ACCESS_TOKEN_KEY, result.value);
access = result.value;
}
} catch {
/* ignore */
}
}
if (!refresh) {
try {
const result = await Preferences.get({ key: REFRESH_TOKEN_KEY });
if (result?.value) {
localStorage.setItem(REFRESH_TOKEN_KEY, result.value);
refresh = result.value;
}
} catch {
/* ignore */
}
}
return { access, refresh };
}
/**
* Authorization header value for SimpleJWT (`AUTH_HEADER_TYPES`: JWT).
* @param {string|null|undefined} access
* @returns {string|null}
*/
export function authorizationHeader(access = getAccessToken()) {
if (!access) {
return null;
}
return `JWT ${access}`;
}
+108
View File
@@ -0,0 +1,108 @@
import {
ACCESS_TOKEN_KEY,
REFRESH_TOKEN_KEY,
authorizationHeader,
clearTokens,
getAccessToken,
getRefreshToken,
hydrateTokensFromNativeStorage,
setTokens,
} from './tokenStorage';
describe('tokenStorage', () => {
const originalCapacitor = window.Capacitor;
beforeEach(() => {
localStorage.clear();
delete window.Capacitor;
});
afterEach(() => {
if (originalCapacitor === undefined) {
delete window.Capacitor;
} else {
window.Capacitor = originalCapacitor;
}
});
it('stores and reads tokens from localStorage', async () => {
await setTokens('access-1', 'refresh-1');
expect(getAccessToken()).toBe('access-1');
expect(getRefreshToken()).toBe('refresh-1');
expect(localStorage.getItem(ACCESS_TOKEN_KEY)).toBe('access-1');
expect(localStorage.getItem(REFRESH_TOKEN_KEY)).toBe('refresh-1');
});
it('clears tokens from localStorage', async () => {
await setTokens('a', 'r');
await clearTokens();
expect(getAccessToken()).toBeNull();
expect(getRefreshToken()).toBeNull();
});
it('builds JWT Authorization header', () => {
expect(authorizationHeader('tok')).toBe('JWT tok');
expect(authorizationHeader(null)).toBeNull();
});
it('mirrors tokens to Capacitor Preferences when available', async () => {
const set = jest.fn().mockResolvedValue(undefined);
window.Capacitor = {
Plugins: {
Preferences: { set, get: jest.fn(), remove: jest.fn() },
},
};
await setTokens('access-n', 'refresh-n');
expect(set).toHaveBeenCalledWith({
key: ACCESS_TOKEN_KEY,
value: 'access-n',
});
expect(set).toHaveBeenCalledWith({
key: REFRESH_TOKEN_KEY,
value: 'refresh-n',
});
});
it('hydrates localStorage from Preferences when empty', async () => {
const get = jest.fn(async ({ key }) => {
if (key === ACCESS_TOKEN_KEY) {
return { value: 'pref-access' };
}
if (key === REFRESH_TOKEN_KEY) {
return { value: 'pref-refresh' };
}
return { value: null };
});
window.Capacitor = {
Plugins: { Preferences: { get, set: jest.fn(), remove: jest.fn() } },
};
const result = await hydrateTokensFromNativeStorage();
expect(result).toEqual({
access: 'pref-access',
refresh: 'pref-refresh',
});
expect(getAccessToken()).toBe('pref-access');
expect(getRefreshToken()).toBe('pref-refresh');
});
it('does not overwrite existing localStorage on hydrate', async () => {
localStorage.setItem(ACCESS_TOKEN_KEY, 'local-access');
localStorage.setItem(REFRESH_TOKEN_KEY, 'local-refresh');
const get = jest.fn();
window.Capacitor = {
Plugins: { Preferences: { get, set: jest.fn(), remove: jest.fn() } },
};
const result = await hydrateTokensFromNativeStorage();
expect(get).not.toHaveBeenCalled();
expect(result).toEqual({
access: 'local-access',
refresh: 'local-refresh',
});
});
});
@@ -3,7 +3,8 @@ import React, { useContext } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { AuthContext } from '../../contexts/AuthContext'; import { AuthContext } from '../../contexts/AuthContext';
import { AccountContext } from '../../contexts/AccountContext'; import { AccountContext } from '../../contexts/AccountContext';
import { axiosInstance } from '../../../axiosApi'; import { applyAccessToken, axiosInstance } from '../../../axiosApi';
import { clearTokens, getRefreshToken } from '../../auth/tokenStorage';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import { AccountBox, Dashboard, Feedback, Logout } from '@mui/icons-material'; import { AccountBox, Dashboard, Feedback, Logout } from '@mui/icons-material';
@@ -19,12 +20,11 @@ const Header = ({drawerWidth=0, handleDrawerToggle}: HeaderProps): JSX.Element =
const handleSignOut = async () => { const handleSignOut = async () => {
try{ try{
const response = await axiosInstance.post('blacklist/',{ await axiosInstance.post('blacklist/',{
'refresh_token': localStorage.getItem("refresh_token") 'refresh_token': getRefreshToken()
}) })
localStorage.removeItem('access_token') await clearTokens();
localStorage.removeItem('refresh_token') applyAccessToken(null);
axiosInstance.defaults.headers['Authorization'] = null;
setAuthentication(false) setAuthentication(false)
setAccount(undefined); setAccount(undefined);
navigate('/signin/') navigate('/signin/')
@@ -3,7 +3,8 @@ import styled, { useTheme } from 'styled-components';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AuthContext } from '../../contexts/AuthContext'; import { AuthContext } from '../../contexts/AuthContext';
import { AccountContext } from '../../contexts/AccountContext'; import { AccountContext } from '../../contexts/AccountContext';
import { axiosInstance } from '../../../axiosApi'; import { applyAccessToken, axiosInstance } from '../../../axiosApi';
import { clearTokens, getRefreshToken } from '../../auth/tokenStorage';
const HeaderContainer = styled.header` const HeaderContainer = styled.header`
position: absolute; position: absolute;
@@ -129,11 +130,10 @@ const Header2 = ({ absolute = false, light = false, isMini = false }: Header2Pro
const handleSignOut = async () => { const handleSignOut = async () => {
try { try {
await axiosInstance.post('blacklist/', { await axiosInstance.post('blacklist/', {
'refresh_token': localStorage.getItem("refresh_token") 'refresh_token': getRefreshToken()
}) })
localStorage.removeItem('access_token') await clearTokens();
localStorage.removeItem('refresh_token') applyAccessToken(null);
axiosInstance.defaults.headers['Authorization'] = null;
setAuthentication(false) setAuthentication(false)
setAccount(undefined); setAccount(undefined);
navigate('/signin/') navigate('/signin/')
+26 -16
View File
@@ -1,5 +1,9 @@
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
import { createContext, ReactNode, useState, useEffect } from "react" import { createContext, ReactNode, useState, useEffect } from "react"
import {
getAccessToken,
hydrateTokensFromNativeStorage,
} from "../auth/tokenStorage";
type AuthProviderProps = { type AuthProviderProps = {
children?: ReactNode; children?: ReactNode;
@@ -31,27 +35,33 @@ const AuthProvider = ({children}: AuthProviderProps) => {
const [loading, setLoading] = useState(true); // Add a loading state const [loading, setLoading] = useState(true); // Add a loading state
useEffect(() => { useEffect(() => {
//console.log('we are in the auth provider') let cancelled = false;
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
const decodedToken = jwtDecode(accessToken)
//console.log(decodedToken)
if(decodedToken.exp){
//console.log(decodedToken.exp * 1000)
//console.log(Date.now())
if (decodedToken.exp * 1000> Date.now()) {
//console.log('We are setting that we are authenticated')
setAuthentication(true);
}
const bootstrap = async () => {
await hydrateTokensFromNativeStorage();
if (cancelled) {
return;
} }
} const accessToken = getAccessToken();
setLoading(false); if (accessToken) {
try {
const decodedToken = jwtDecode(accessToken)
if (decodedToken.exp && decodedToken.exp * 1000 > Date.now()) {
setAuthentication(true);
}
} catch {
/* invalid token — stay unauthenticated */
}
}
setLoading(false);
};
bootstrap();
return () => {
cancelled = true;
};
}, []) }, [])
//console.log(authenticated)
const [ needsNewPassword, setNeedsNewPassword] = useState(initialValues.needsNewPassword) const [ needsNewPassword, setNeedsNewPassword] = useState(initialValues.needsNewPassword)
return ( return (
@@ -1,5 +1,6 @@
import { AccountContext } from "./AccountContext"; import { AccountContext } from "./AccountContext";
import { axiosInstance } from "../../axiosApi"; import { applyAccessToken, axiosInstance } from "../../axiosApi";
import { setTokens } from "../auth/tokenStorage";
import { import {
ConnectionStatus, ConnectionStatus,
DEFAULT_RECONNECT_CONFIG, DEFAULT_RECONNECT_CONFIG,
@@ -183,8 +184,11 @@ function WebSocketProvider({ children }) {
const response = await axiosInstance.post("/token/refresh/", { const response = await axiosInstance.post("/token/refresh/", {
refresh, refresh,
}); });
axiosInstance.defaults.headers["Authorization"] = await setTokens(
"JWT " + response.data.access; response.data.access,
response.data.refresh ?? refresh,
);
applyAccessToken(response.data.access);
return response.data; return response.data;
}, },
}); });
@@ -10,8 +10,15 @@ import {
jest.mock('../../axiosApi', () => ({ jest.mock('../../axiosApi', () => ({
axiosInstance: { axiosInstance: {
post: jest.fn(), post: jest.fn(),
defaults: { headers: {} }, defaults: { headers: { common: {} } },
}, },
applyAccessToken: jest.fn(),
}));
jest.mock('../auth/tokenStorage', () => ({
setTokens: jest.fn().mockResolvedValue(undefined),
getAccessToken: jest.fn(() => null),
getRefreshToken: jest.fn(() => null),
})); }));
class MockWebSocket { class MockWebSocket {
@@ -15,8 +15,9 @@ jest.mock('../../../axiosApi', () => ({
axiosInstance: { axiosInstance: {
post: (...args: unknown[]) => mockPost(...args), post: (...args: unknown[]) => mockPost(...args),
get: (...args: unknown[]) => mockGet(...args), get: (...args: unknown[]) => mockGet(...args),
defaults: { headers: {} as Record<string, string | null> }, defaults: { headers: { common: {} as Record<string, string | null> } },
}, },
applyAccessToken: jest.fn(),
})); }));
const renderSignIn = () => { const renderSignIn = () => {
+4 -4
View File
@@ -1,6 +1,7 @@
import { Form, Formik, Field } from 'formik'; import { Form, Formik, Field } from 'formik';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { axiosInstance } from '../../../axiosApi'; import { applyAccessToken, axiosInstance } from '../../../axiosApi';
import { setTokens } from '../../auth/tokenStorage';
import { AuthContext } from '../../contexts/AuthContext'; import { AuthContext } from '../../contexts/AuthContext';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AccountContext } from '../../contexts/AccountContext'; import { AccountContext } from '../../contexts/AccountContext';
@@ -148,9 +149,8 @@ const SignIn = (): JSX.Element => {
password: password, password: password,
}) })
axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access; await setTokens(response.data.access, response.data.refresh);
localStorage.setItem('access_token', response.data.access); applyAccessToken(response.data.access);
localStorage.setItem('refresh_token', response.data.refresh);
const get_user_response: AxiosResponse<AccountType> = await axiosInstance.get('/user/get/') const get_user_response: AxiosResponse<AccountType> = await axiosInstance.get('/user/get/')
+1 -1
View File
@@ -22,7 +22,7 @@ export function isNativePlatform() {
/** /**
* Absolute in-app path for full page navigations. * Absolute in-app path for full page navigations.
* Native Capacitor builds use HashRouter; web keeps BrowserRouter paths. * Native Capacitor builds use HashRouter (#24); web keeps BrowserRouter paths.
* *
* @param {string} path e.g. "/signin/" * @param {string} path e.g. "/signin/"
* @returns {string} * @returns {string}
+7 -5
View File
@@ -1,16 +1,18 @@
/// <reference types="react-scripts" /> /// <reference types="react-scripts" />
interface CapacitorPreferencesPlugin {
get?: (options: { key: string }) => Promise<{ value?: string | null }>;
set?: (options: { key: string; value: string }) => Promise<void>;
remove?: (options: { key: string }) => Promise<void>;
}
interface CapacitorBridge { interface CapacitorBridge {
isNativePlatform?: () => boolean; isNativePlatform?: () => boolean;
isNative?: boolean; isNative?: boolean;
Plugins?: { Plugins?: {
Preferences?: CapacitorPreferencesPlugin;
App?: { addListener?: (...args: unknown[]) => unknown }; App?: { addListener?: (...args: unknown[]) => unknown };
Network?: { addListener?: (...args: unknown[]) => unknown }; Network?: { addListener?: (...args: unknown[]) => unknown };
Preferences?: {
get?: (options: { key: string }) => Promise<{ value?: string | null }>;
set?: (options: { key: string; value: string }) => Promise<void>;
remove?: (options: { key: string }) => Promise<void>;
};
}; };
} }