First merge from develop to main (WIP) #7

Merged
sugar merged 199 commits from develop into main 2026-01-27 17:47:07 +00:00
6 changed files with 109 additions and 35 deletions
Showing only changes of commit 27ac4d9b4a - Show all commits

View File

@@ -44,6 +44,7 @@
"clsx": "^2.1.1",
"culori": "^4.0.2",
"immer": "^11.1.0",
"lodash-es": "^4.17.22",
"lucide-react": "^0.562.0",
"next-themes": "^0.4.6",
"qrcode": "^1.5.4",
@@ -69,6 +70,7 @@
"@tanstack/router-plugin": "^1.141.7",
"@types/base-64": "^1.0.2",
"@types/culori": "^4.0.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^25.0.3",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.5",
@@ -82,6 +84,7 @@
"lint-staged": "^16.2.7",
"simple-git-hooks": "^2.13.1",
"tw-animate-css": "^1.4.0",
"type-fest": "^5.4.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4",

View File

@@ -110,6 +110,9 @@ importers:
immer:
specifier: ^11.1.0
version: 11.1.3
lodash-es:
specifier: ^4.17.22
version: 4.17.22
lucide-react:
specifier: ^0.562.0
version: 0.562.0(react@19.2.3)
@@ -180,6 +183,9 @@ importers:
'@types/culori':
specifier: ^4.0.1
version: 4.0.1
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
'@types/node':
specifier: ^25.0.3
version: 25.0.9
@@ -219,6 +225,9 @@ importers:
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
type-fest:
specifier: ^5.4.1
version: 5.4.1
typescript:
specifier: ~5.9.3
version: 5.9.3
@@ -1730,6 +1739,12 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.23':
resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==}
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -2981,6 +2996,9 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.22:
resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -3650,6 +3668,10 @@ packages:
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
engines: {node: ^14.18.0 || >=16.0.0}
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
@@ -3721,6 +3743,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
type-fest@5.4.1:
resolution: {integrity: sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ==}
engines: {node: '>=20'}
typescript-eslint@8.53.1:
resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5392,6 +5418,12 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.23
'@types/lodash@4.17.23': {}
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -6733,6 +6765,8 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.22: {}
lodash.merge@4.6.2: {}
lodash@4.17.21: {}
@@ -7639,6 +7673,8 @@ snapshots:
dependencies:
'@pkgr/core': 0.2.9
tagged-tag@1.0.0: {}
tailwind-merge@3.4.0: {}
tailwindcss@4.1.18: {}
@@ -7699,6 +7735,10 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
type-fest@5.4.1:
dependencies:
tagged-tag: 1.0.0
typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)

View File

@@ -1,10 +1,17 @@
import type { AxiosRequestConfig } from 'axios';
import axios, { AxiosError } from 'axios';
import type { AxiosError, AxiosRequestConfig } from 'axios';
import type { JsonValue } from 'type-fest';
import axios from 'axios';
import { isNil } from 'lodash-es';
import { router } from '@/lib/router';
import { doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token';
import { clearTokens, doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token';
export const HEADER_API_VERSION = {
'X-Api-Version': 'latest',
};
export const axiosClient = axios.create({
baseURL: '/api/v1/',
headers: HEADER_API_VERSION,
});
axiosClient.interceptors.request.use((config) => {
@@ -18,27 +25,43 @@ axiosClient.interceptors.request.use((config) => {
type RetryConfig = AxiosRequestConfig & { _retry?: boolean };
axiosClient.interceptors.response.use(undefined, async (error: AxiosError) => {
interface ResponseData {
code: number;
error_id: string;
status: string;
data: JsonValue;
}
axiosClient.interceptors.response.use(async (response) => {
const data = response.data as ResponseData;
if (data.code !== 200) {
return Promise.reject(data);
}
response.data = data.data;
return response;
}, async (error: AxiosError) => {
const originalRequest = error.config as RetryConfig | undefined;
if (!error.response || error.response.status !== 401 || !originalRequest) {
return Promise.reject(error);
}
if (error.response.status === 401 && getRefreshToken() !== null) {
if (error.response.status === 401 && originalRequest.url !== '/auth/refresh' && !isNil(getRefreshToken())) {
try {
const maybeRefreshTokenValue = await doRefreshToken();
const { access_token, refresh_token } = maybeRefreshTokenValue.data;
const maybeRefreshTokenResponse = await doRefreshToken();
if (maybeRefreshTokenResponse.status !== 200) {
throw new Error('Failed to refresh token');
}
const { access_token, refresh_token } = maybeRefreshTokenResponse.data;
originalRequest.headers = originalRequest.headers ?? {};
originalRequest.headers.Authorization = `Bearer ${access_token}`;
setToken(access_token);
setRefreshToken(refresh_token);
return await axiosClient(originalRequest);
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (e) {
if (e instanceof AxiosError && e.status === 401) {
await router.navigate({ to: '/authorize' });
return Promise.reject(error);
}
// Should remove token (tokens are out of date)
clearTokens();
await router.navigate({ to: '/authorize' });
}
}
});

View File

@@ -1,4 +1,4 @@
import axios from 'axios';
import { axiosClient, HEADER_API_VERSION } from './axios';
export function setToken(token: string) {
localStorage.setItem('token', token);
@@ -30,11 +30,11 @@ export function clearTokens() {
}
export async function doSetTokenByCode(code: string) {
const { data } = await axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/token', { code });
const { data } = await axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/token', { code }, { headers: HEADER_API_VERSION });
setToken(data.access_token);
setRefreshToken(data.refresh_token);
}
export async function doRefreshToken() {
return axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/refresh', { refresh_token: getRefreshToken() });
return axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/refresh', { refresh_token: getRefreshToken() }, { headers: HEADER_API_VERSION });
}

View File

@@ -1,7 +1,9 @@
import { createFileRoute } from '@tanstack/react-router';
import { zodValidator } from '@tanstack/zod-adapter';
import { isNil } from 'lodash-es';
import z from 'zod';
import { LoginForm } from '@/components/login-form';
import { axiosClient } from '@/lib/axios';
import { generateOAuthState } from '@/lib/random';
import { getToken } from '@/lib/token';
@@ -22,15 +24,21 @@ export const Route = createFileRoute('/authorize')({
function RouteComponent() {
const token = getToken();
const oauthParams = Route.useSearch();
if (token !== null) {
const base = new URL(window.location.origin);
const url = new URL('/api/v1/auth/redirect', base);
url.searchParams.set('client_id', oauthParams.client_id);
url.searchParams.set('response_type', oauthParams.response_type);
url.searchParams.set('redirect_uri', oauthParams.redirect_uri);
url.searchParams.set('state', oauthParams.state);
window.location.href = url.toString();
return null;
/**
* Auth by Token Flow
*/
if (!isNil(token)) {
axiosClient.post<{ redirect_uri: string }>('/auth/exchange', {
client_id: oauthParams.client_id,
redirect_uri: oauthParams.redirect_uri,
state: oauthParams.state,
}).then((res) => {
window.location.href = res.data.redirect_uri;
}).catch((e) => {
console.error(e);
return 'Token exchange failed';
});
return 'Redirecting';
}
return (
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">

View File

@@ -1,15 +1,15 @@
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import svgr from "vite-plugin-svgr";
import path from 'node:path';
import tailwindcss from '@tailwindcss/vite';
import { tanstackRouter } from '@tanstack/router-plugin/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
// https://vite.dev/config/
export default defineConfig({
plugins: [
tanstackRouter({
target: "react",
target: 'react',
autoCodeSplitting: true,
}),
react(),
@@ -18,15 +18,15 @@ export default defineConfig({
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
'@': path.resolve(__dirname, './src'),
},
},
server: {
proxy: {
"/api": "http://10.0.0.250:8000",
'/api': 'http://10.0.0.10:8000',
},
host: "0.0.0.0",
host: '0.0.0.0',
port: 5173,
allowedHosts: ["nix.org.cn", "nixos.party"],
allowedHosts: ['nix.org.cn', 'nixos.party'],
},
});