feat: App→网页免登录(一次性换票 SSO)——移动/桌面点链接直达已登录官网
- backend:POST /auth/web-ticket(需登录,签 60s 单次票据,每用户限频)+ POST /auth/web-ticket/exchange(公开,原子即焚兑换正常登录态,响应同 /auth/login 另带 shop_code);web_tickets 新表 + user_sessions.kind 列 (sso 会话:web 平台、refresh 12h 硬到期、不占设备并发配额、设备管理可见 可踢);清理任务顺带清过期票据;schema.sql/AutoMigrate/testutil 同步 - web:/sso 落地页——兑票写入与 Web 版共享的 localStorage 后跳 redirect; redirect 白名单只收本站相对路径(防 open redirect),票据即时从地址栏抹除 - client:launchAuthedWebPath(ref, path) 封装(签票→拼 /sso URL→launchUrl, 失败降级未登录直开;桌面/移动同一套);授权面板「查看套餐价格」接入 test(backend): web_ticket_test 三用例(兑换+续期/单次即焚/过期拒绝/ sso 不占配额双向验证) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
// core/utils/web_launch.dart — App→官网免登录跳转(一次性换票 SSO)。
|
||||
// 先向后端签一张 60s 单次票据,再打开 官网/sso?t=<票>&redirect=<路径>,
|
||||
// 落地页兑票写入登录态后跳目标页——桌面/移动同一套(launchUrl 跨端)。
|
||||
// 签票失败(未登录/网络异常)时降级为直接打开目标链接(未登录态浏览)。
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
import '../config/app_config.dart';
|
||||
|
||||
/// 打开官网 [path](如 `/#pricing`、`/profile/`)并自动登录。
|
||||
Future<void> launchAuthedWebPath(WidgetRef ref, String path) async {
|
||||
final base = AppConfig.publicBaseUrl;
|
||||
Uri target = Uri.parse('$base$path');
|
||||
try {
|
||||
final resp = await ref.read(apiClientProvider).post('/auth/web-ticket');
|
||||
final ticket = resp.data?['data']?['ticket'] as String?;
|
||||
if (ticket != null && ticket.isNotEmpty) {
|
||||
target = Uri.parse(
|
||||
'$base/sso/?t=$ticket&redirect=${Uri.encodeComponent(path)}');
|
||||
}
|
||||
} catch (_) {
|
||||
// 签票失败降级:未登录态直开目标页
|
||||
}
|
||||
await launchUrl(target, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
@@ -4,11 +4,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../core/auth/auth_state.dart';
|
||||
import '../../core/config/app_info.dart';
|
||||
import '../../core/config/license_copy.dart';
|
||||
import '../../core/utils/web_launch.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_dims.g.dart';
|
||||
import '../../core/theme/app_fonts.dart';
|
||||
@@ -114,8 +113,8 @@ class _LicensePanelState extends ConsumerState<LicensePanel> {
|
||||
Text('在线购买请联系门店管理员 · ',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
InkWell(
|
||||
onTap: () =>
|
||||
launchUrl(Uri.parse('${AppInfo.website}/#pricing')),
|
||||
// 免登跳官网(一次性换票 SSO):浏览器落地即已登录,可直接购买
|
||||
onTap: () => launchAuthedWebPath(ref, '/#pricing'),
|
||||
child: Text('查看套餐价格 →',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
|
||||
Reference in New Issue
Block a user