Files
DadaoTask/web/src/components/icons.tsx
T
Steven 7558637567
CI / backend (push) Canceled after 0s
feat(auth): 短信登录默认关,换成手机号/邮箱 + 密码,发信邮箱进后台
用户要求:内测的固定验证码 123456 等于"知道手机号就能进任何账号",先关掉;
登录改用账号密码,并多一种"邮箱当账号名"的方式;发信邮箱要能在后台改。

短信那条路(默认关):
- `SMS_LOGIN_ENABLED=false`,`/auth/sms/send` 与 `/auth/login` 统一回 2017,不是静默失败;
- 生产校验改成"开着短信登录就必须配真通道",不开时 provider 是 dev 也不拦;
- 新增 `GET /auth/methods`,登录页据此渲染,关着的路不出现(少一次"点进去才发现不能用")。

邮箱登录:
- `app_user.email` + `uq_app_user_email`(`lower(email)` 部分索引),登录与绑定都归一成小写;
- `POST /auth/login/password` 收 `phone` 或 `email`(二选一),失败统一 2013、连错 10 次锁 15 分钟;
- 绑定在 `PATCH /auth/me`:格式错 2018、被占用 2019、留空串表示解绑。

系统设置(跨租户,只有平台管理员能改):
- 新表 `system_setting`(不挂租户、无 RLS,写权限由 `deps.platform_admin` 把关);
- `GET/PUT /settings/smtp`、`POST /settings/smtp/test`,授权码只写不读,留空表示沿用旧值;
- `services/mail.py` 只用标准库 smtplib:465 走 SSL、587 走 STARTTLS,认证/拒收/网络失败都归 2020 但消息不同;
- Web 新增「系统设置」页(状态卡 + 测试发信),侧栏只对平台管理员显示入口;账号页补「邮箱」。

验证:pytest 130 passed、ruff check/format 全绿、tsc 通过、vite build 通过;
`web/e2e/login-email-smtp.mjs` 15 项全绿(含用 126 授权码真发一封测试邮件)。

顺带记下的两个坑:`platform_admin_phones` 是 list,pydantic-settings 会按 JSON 解析,
要用 `NoDecode` 交给 before-validator 拆逗号;迁移号写成 0009 会撞既有的 0009,一并改成 0010。
2026-11-03 00:00:11 +08:00

311 lines
7.7 KiB
TypeScript

import type { SVGProps } from "react";
type IconProps = SVGProps<SVGSVGElement> & { size?: number };
function base({ size = 16, ...rest }: IconProps) {
return {
width: size,
height: size,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.7,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
"aria-hidden": true,
...rest,
};
}
export const Check = (p: IconProps) => (
<svg {...base(p)}>
<path d="M20 6 9 17l-5-5" />
</svg>
);
export const Bold = (p: IconProps) => (
<svg {...base(p)}>
<path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z" />
</svg>
);
export const Italic = (p: IconProps) => (
<svg {...base(p)}>
<path d="M15 5h-5M14 19H9M14.5 5 10 19" />
</svg>
);
export const Quote = (p: IconProps) => (
<svg {...base(p)}>
<path d="M7 7h4v4c0 2.2-1.3 3.7-3.5 4.5M15 7h4v4c0 2.2-1.3 3.7-3.5 4.5" />
</svg>
);
export const Code = (p: IconProps) => (
<svg {...base(p)}>
<path d="m9 8-4 4 4 4M15 8l4 4-4 4" />
</svg>
);
export const ListIcon = (p: IconProps) => (
<svg {...base(p)}>
<path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01" />
</svg>
);
export const Image = (p: IconProps) => (
<svg {...base(p)}>
<rect x="3.5" y="4.5" width="17" height="15" rx="2.5" />
<circle cx="9" cy="10" r="1.6" />
<path d="m4 17 4.5-4.5 3.5 3.5 3-3L20 17" />
</svg>
);
export const Eye = (p: IconProps) => (
<svg {...base(p)}>
<path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" />
<circle cx="12" cy="12" r="2.6" />
</svg>
);
export const Pencil = (p: IconProps) => (
<svg {...base(p)}>
<path d="M4 20h4l10.5-10.5a2.1 2.1 0 0 0-3-3L5 17z" />
</svg>
);
export const Paperclip = (p: IconProps) => (
<svg {...base(p)}>
<path d="M20 11.5 12.3 19a4.6 4.6 0 0 1-6.5-6.5l7.4-7.4a3.1 3.1 0 1 1 4.4 4.4l-7.4 7.4a1.5 1.5 0 1 1-2.2-2.2l6.6-6.6" />
</svg>
);
export const Download = (p: IconProps) => (
<svg {...base(p)}>
<path d="M12 4v11M7.5 10.5 12 15l4.5-4.5M4.5 19h15" />
</svg>
);
export const CheckCircle = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="12" cy="12" r="9" />
<path d="m8.5 12 2.5 2.5 4.5-5" />
</svg>
);
export const Circle = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="12" cy="12" r="8.5" />
</svg>
);
export const Search = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="11" cy="11" r="6.5" />
<path d="m16 16 4 4" />
</svg>
);
export const Plus = (p: IconProps) => (
<svg {...base(p)}>
<path d="M12 5v14M5 12h14" />
</svg>
);
export const X = (p: IconProps) => (
<svg {...base(p)}>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
);
export const ChevronDown = (p: IconProps) => (
<svg {...base(p)}>
<path d="m6 9 6 6 6-6" />
</svg>
);
export const ChevronRight = (p: IconProps) => (
<svg {...base(p)}>
<path d="m9 6 6 6-6 6" />
</svg>
);
export const ChevronLeft = (p: IconProps) => (
<svg {...base(p)}>
<path d="m15 6-6 6 6 6" />
</svg>
);
export const ArrowRight = (p: IconProps) => (
<svg {...base(p)}>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
);
export const Columns = (p: IconProps) => (
<svg {...base(p)}>
<rect x="3.5" y="4" width="17" height="16" rx="2.5" />
<path d="M9.5 4v16M15 4v16" />
</svg>
);
export const TicketIcon = (p: IconProps) => (
<svg {...base(p)}>
<path d="M3.5 9.5V7a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2v2.5a2.5 2.5 0 0 0 0 5V17a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2v-2.5a2.5 2.5 0 0 0 0-5Z" />
<path d="M12 7v2M12 15v2" />
</svg>
);
export const Book = (p: IconProps) => (
<svg {...base(p)}>
<path d="M4 5.5A2.5 2.5 0 0 1 6.5 3H19v15.5H6.5A2.5 2.5 0 0 0 4 21Z" />
<path d="M4 18.5V5.5M8.5 7.5h6" />
</svg>
);
export const Users = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="9" cy="8.5" r="3.2" />
<path d="M3.5 19.5a5.5 5.5 0 0 1 11 0" />
<path d="M16 6.2a3.2 3.2 0 0 1 0 6M17.5 15.2a5.5 5.5 0 0 1 3 4.3" />
</svg>
);
export const Inbox = (p: IconProps) => (
<svg {...base(p)}>
<path d="M3.5 13.5h4l1.2 2.2h6.6l1.2-2.2h4" />
<path d="M5.6 4.5h12.8l2.1 9v4.3a2 2 0 0 1-2 2H5.5a2 2 0 0 1-2-2V13.5Z" />
</svg>
);
export const Bell = (p: IconProps) => (
<svg {...base(p)}>
<path d="M6 9a6 6 0 1 1 12 0c0 3.2.8 4.9 1.6 5.9.4.5 0 1.1-.6 1.1H5c-.6 0-1-.6-.6-1.1C5.2 13.9 6 12.2 6 9Z" />
<path d="M10 19a2 2 0 0 0 4 0" />
</svg>
);
export const Sparkle = (p: IconProps) => (
<svg {...base(p)}>
<path d="M12 3.5 13.6 9l5.4 1.6-5.4 1.6L12 17.5l-1.6-5.3L5 10.6 10.4 9Z" />
<path d="M18.5 16.5 19.2 19l2.3.7-2.3.8-.7 2.4" />
</svg>
);
export const Clock = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="12" cy="12" r="8.5" />
<path d="M12 7.5V12l3 2" />
</svg>
);
export const Trash = (p: IconProps) => (
<svg {...base(p)}>
<path d="M4.5 7h15M9.5 7V5.5A1.5 1.5 0 0 1 11 4h2a1.5 1.5 0 0 1 1.5 1.5V7" />
<path d="M6.5 7l.8 11.2A2 2 0 0 0 9.3 20h5.4a2 2 0 0 0 2-1.8L17.5 7" />
</svg>
);
export const Lock = (p: IconProps) => (
<svg {...base(p)}>
<rect x="4.5" y="10.5" width="15" height="10" rx="2.2" />
<path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" />
</svg>
);
export const Logout = (p: IconProps) => (
<svg {...base(p)}>
<path d="M14.5 4.5H7a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h7.5" />
<path d="M18 12H10M15.5 8.5 19 12l-3.5 3.5" />
</svg>
);
export const Settings = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="12" cy="12" r="3" />
<path d="M12 3.5v2M12 18.5v2M20.5 12h-2M5.5 12h-2M17.9 6.1l-1.4 1.4M7.5 16.5l-1.4 1.4M17.9 17.9l-1.4-1.4M7.5 7.5 6.1 6.1" />
</svg>
);
export const Sliders = (p: IconProps) => (
<svg {...base(p)}>
<path d="M4 7.5h9M17 7.5h3M4 16.5h3M11 16.5h9" />
<circle cx="15" cy="7.5" r="2" />
<circle cx="9" cy="16.5" r="2" />
</svg>
);
export const User = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="12" cy="8.5" r="3.5" />
<path d="M5 20c0-3.3 3.1-5.5 7-5.5s7 2.2 7 5.5" />
</svg>
);
export const Flag = (p: IconProps) => (
<svg {...base(p)}>
<path d="M6 21V4.5M6 5h10.5l-1.7 3.6L16.5 12H6" />
</svg>
);
export const Link2 = (p: IconProps) => (
<svg {...base(p)}>
<path d="M9.5 14.5 14.5 9.5" />
<path d="M11 6.8 12.6 5.2a3.7 3.7 0 0 1 5.2 5.2l-1.6 1.6" />
<path d="M13 17.2 11.4 18.8a3.7 3.7 0 0 1-5.2-5.2l1.6-1.6" />
</svg>
);
/** 放大到整屏 / 收回原来的宽度:长文和图片需要有地方放 */
export const Expand = (p: IconProps) => (
<svg {...base(p)}>
<path d="M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5" />
</svg>
);
export const Collapse = (p: IconProps) => (
<svg {...base(p)}>
<path d="M4 9h5V4M20 9h-5V4M4 15h5v5M20 15h-5v5" />
</svg>
);
export const More = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="6" cy="12" r="1.2" fill="currentColor" stroke="none" />
<circle cx="12" cy="12" r="1.2" fill="currentColor" stroke="none" />
<circle cx="18" cy="12" r="1.2" fill="currentColor" stroke="none" />
</svg>
);
export const Spinner = ({ size = 16, className = "" }: { size?: number; className?: string }) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
className={`animate-spin ${className}`}
aria-hidden
>
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.6" opacity="0.2" fill="none" />
<path
d="M21 12a9 9 0 0 0-9-9"
stroke="currentColor"
strokeWidth="2.6"
strokeLinecap="round"
fill="none"
/>
</svg>
);
export const Logo = ({ size = 28 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 32 32" aria-hidden>
<rect width="32" height="32" rx="9" fill="#4F46E5" />
<path
d="M9 11.5h14M9 16h9.5M9 20.5h6"
stroke="#FFFFFF"
strokeWidth="2"
strokeLinecap="round"
/>
<circle cx="23" cy="20.5" r="3" fill="#C7D2FE" />
</svg>
);