4 Commits
7 changed files with 48 additions and 13 deletions
+20
View File
@@ -2,6 +2,26 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [1.0.5] - 2026-06-21
### Added
### Changed
### Fixed
- Redirect to `/login` when there is no refresh token.
- Include required JMAP capabilities in `using`.
- Default scopes omit `offline_access`.
## [1.0.4] - 2026-05-11
### Added
### Changed
### Fixed
- Align `base32` alphabet with the server.
## [1.0.3] - 2026-05-05
### Added
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "stalwart-webui",
"private": true,
"version": "1.0.3",
"version": "1.0.5",
"description": "Stalwart WebUI",
"type": "module",
"scripts": {
+2 -2
View File
@@ -8,10 +8,10 @@ import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '@/stores/authStore';
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const accessToken = useAuthStore((s) => s.accessToken);
const authenticated = useAuthStore((s) => s.isAuthenticated());
const bypassToken = import.meta.env.VITE_ACCESS_TOKEN;
const location = useLocation();
if (!accessToken && !bypassToken) {
if (!authenticated && !bypassToken) {
const originalPath = location.pathname + location.search;
return <Navigate to="/login" replace state={{ from: originalPath }} />;
}
@@ -11,7 +11,7 @@ import { CircleDot, CircleX, Clock } from 'lucide-react';
import { useSchemaStore } from '@/stores/schemaStore';
import type { TraceEvent, TraceKeyValue, TraceValue } from '../types';
const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz792013';
function intToBase32(input: number | string): string {
let n: bigint;
+11 -5
View File
@@ -107,11 +107,17 @@ export async function apiFetch(path: string, options?: RequestInit): Promise<Res
let response = await makeRequest();
if (response.status === 401 && useAuthStore.getState().refreshToken) {
try {
await refreshAccessToken();
response = await makeRequest();
} catch {
if (response.status === 401) {
if (useAuthStore.getState().refreshToken) {
try {
await refreshAccessToken();
response = await makeRequest();
} catch {
throw new ApiError(401, 'Unauthorized', null);
}
} else {
useAuthStore.getState().logout();
window.location.href = `${getBasePath()}/login`;
throw new ApiError(401, 'Unauthorized', null);
}
}
+2 -3
View File
@@ -125,8 +125,7 @@ function getRedirectUri(): string {
}
export async function startAuthFlow(username: string, returnUrl?: string | null): Promise<void> {
const { authorization_endpoint, token_endpoint, end_session_endpoint, scopes_supported } =
await discover(username);
const { authorization_endpoint, token_endpoint, end_session_endpoint, scopes_supported } = await discover(username);
const codeVerifier = generateCodeVerifier();
const { challenge: codeChallenge, method: codeChallengeMethod } = await generateCodeChallenge(codeVerifier);
@@ -167,7 +166,7 @@ export async function startAuthFlow(username: string, returnUrl?: string | null)
if (SCOPES && SCOPES.length > 0) {
scope = SCOPES;
} else if (scopes_supported?.includes('openid')) {
scope = ['openid', 'email', 'profile'].filter((s) => scopes_supported.includes(s)).join(' ');
scope = ['openid', 'email', 'profile', 'offline_access'].filter((s) => scopes_supported.includes(s)).join(' ');
} else {
scope = '';
}
+11 -1
View File
@@ -10,7 +10,17 @@ import { logJmapExchange } from '@/lib/debug';
import type { JmapMethodCall, JmapMethodResponse, JmapQueryResponse, JmapResponse } from '@/types/jmap';
import type { Schema } from '@/types/schema';
const JMAP_USING = ['urn:ietf:params:jmap:core', 'urn:stalwart:jmap'];
const JMAP_USING = [
'urn:ietf:params:jmap:core',
'urn:stalwart:jmap',
'urn:ietf:params:jmap:blob',
'urn:ietf:params:jmap:mail',
'urn:ietf:params:jmap:calendars',
'urn:ietf:params:jmap:contacts',
'urn:ietf:params:jmap:principals',
'urn:ietf:params:jmap:sieve',
'urn:ietf:params:jmap:vacationresponse',
];
export function getAccountId(objectType: string): string {
const { primaryAccountId, activeAccountId } = useAuthStore.getState();