feat: Implement AP password authentication for the WiFi configuration page

This commit is contained in:
wenjie
2026-03-12 15:21:18 +08:00
parent 08093dddad
commit 10a512ec96
8 changed files with 166 additions and 11 deletions

View File

@@ -31,12 +31,29 @@ export function getWiFi() {
}
// connect wifi without auth (only available in wifi configuration mode)
export function connectWifiNoAuth(ssid: string, password: string) {
export function connectWifiNoAuth(ssid: string, password: string, apPassword?: string) {
const data = {
ssid,
password
};
return http.post('/api/network/wifi', data);
return http.post('/api/network/wifi', data, {
headers: {
'X-AP-Key': apPassword || ''
}
});
}
// verify ap login
export function verifyApLogin(apPassword: string) {
return http.post(
'/api/network/wifi/verify',
{},
{
headers: {
'X-AP-Key': apPassword || ''
}
}
);
}
// connect wifi

View File

@@ -48,7 +48,14 @@ const en = {
invalidMode:
'The current mode does not support network setup. Please go to your device and enable Wi-Fi configuration mode.',
confirmBtn: 'Ok',
finishBtn: 'Finished'
finishBtn: 'Finished',
ap: {
authTitle: 'Authentication Required',
authDescription: 'Please enter the AP password to continue',
authFailed: 'Invalid AP password',
passPlaceholder: 'AP password',
verifyBtn: 'Verify',
}
},
screen: {
scale: 'Scale',

View File

@@ -46,7 +46,14 @@ const zh = {
failed: '操作失败,请重试。',
invalidMode: '当前模式不支持配置网络。请先前往设备启用 Wi-Fi 配置模式。',
confirmBtn: '确定',
finishBtn: '完成'
finishBtn: '完成',
ap: {
authTitle: '身份验证',
authDescription: '请输入 AP 密码以继续',
authFailed: '密码错误',
passPlaceholder: 'AP 密码',
verifyBtn: '验证',
}
},
screen: {
scale: '缩放',

View File

@@ -58,11 +58,12 @@ class Http {
});
}
public post(url: string, data?: any): Promise<Response> {
public post(url: string, data?: any, config?: AxiosRequestConfig): Promise<Response> {
return this.instance.request({
method: 'post',
url,
data
data,
...config
});
}

View File

@@ -1,17 +1,56 @@
import { useState } from 'react';
import { CheckOutlined, LockOutlined, WifiOutlined } from '@ant-design/icons';
import { useEffect, useState } from 'react';
import { CheckOutlined, KeyOutlined, LockOutlined, WifiOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import * as api from '@/api/network.ts';
import { Head } from '@/components/head.tsx';
type State = '' | 'loading' | 'success' | 'failed' | 'denied';
type VerifyState = '' | 'failed' | 'denied';
export const Wifi = () => {
const { t } = useTranslation();
const [searchParams] = useSearchParams();
const [state, setState] = useState<State>('');
const [apPassword, setApPassword] = useState<string>('');
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [verifying, setVerifying] = useState<boolean>(false);
const [verifyState, setVerifyState] = useState<VerifyState>('');
useEffect(() => {
const pass = searchParams.get('p') || searchParams.get('P');
if (pass) {
verifyPassword(pass);
}
}, []);
async function verifyPassword(password: string) {
if (verifying) return;
setVerifying(true);
setVerifyState('');
try {
const rsp = await api.verifyApLogin(password);
if (rsp?.code === 0) {
setApPassword(password);
setIsAuthenticated(true);
} else {
setVerifyState(rsp?.code === -1 ? 'denied' : 'failed');
}
} catch (err) {
console.error(err);
setVerifyState('failed');
}
setVerifying(false);
}
async function onVerifyFinish(values: any) {
if (!values.apPassword) return;
await verifyPassword(values.apPassword);
}
async function connect(values: any) {
if (!values.ssid || !values.password) return;
@@ -20,13 +59,14 @@ export const Wifi = () => {
setState('loading');
try {
const rsp = await api.connectWifiNoAuth(values.ssid, values.password);
const rsp = await api.connectWifiNoAuth(values.ssid, values.password, apPassword);
switch (rsp?.code) {
case 0:
setState('success');
return;
case -1:
case -4:
setState('denied');
return;
case -2:
@@ -41,6 +81,48 @@ export const Wifi = () => {
setState('success');
}
if (!isAuthenticated) {
return (
<>
<Head title={t('head.wifi')} />
<div className="flex h-screen w-screen flex-col items-center justify-center">
<Form
style={{ minWidth: 300, maxWidth: 500 }}
initialValues={{ remember: true }}
onFinish={onVerifyFinish}
>
<div className="flex flex-col space-y-1 pb-5">
<span className="text-center text-2xl font-semibold text-red-500">
{t('wifi.ap.authTitle')}
</span>
<span className="text-center text-neutral-400">{t('wifi.ap.authDescription')}</span>
</div>
<Form.Item name="apPassword">
<Input.Password prefix={<KeyOutlined />} placeholder={t('wifi.ap.passPlaceholder')} />
</Form.Item>
<Form.Item>
<Button className="w-full" htmlType="submit" type="primary" loading={verifying}>
{t('wifi.ap.verifyBtn')}
</Button>
</Form.Item>
</Form>
<div className="flex max-w-[500px] justify-center px-5 pt-3 md:px-10">
{verifyState === 'failed' && (
<span className="text-sm text-red-500">{t('wifi.ap.authFailed')}</span>
)}
{verifyState === 'denied' && (
<span className="text-sm text-red-500">{t('wifi.invalidMode')}</span>
)}
</div>
</div>
</>
);
}
return (
<>
<Head title={t('head.wifi')} />

View File

@@ -44,6 +44,7 @@ export const router = createHashRouter([
},
{
path: '/wifi',
caseSensitive: false,
lazy: async () => {
const { Wifi } = await import('./pages/wifi');
return { Component: Wifi };