@weifuwujs/auth 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -0
- package/dist/auth.d.ts +4 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +625 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +159 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/password.d.ts +21 -0
- package/dist/password.d.ts.map +1 -0
- package/dist/password.js +75 -0
- package/dist/password.js.map +1 -0
- package/dist/postgres.d.ts +22 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +240 -0
- package/dist/postgres.js.map +1 -0
- package/dist/sqlite.d.ts +3 -0
- package/dist/sqlite.d.ts.map +1 -0
- package/dist/sqlite.js +2 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/stores/memory.d.ts +4 -0
- package/dist/stores/memory.d.ts.map +1 -0
- package/dist/stores/memory.js +150 -0
- package/dist/stores/memory.js.map +1 -0
- package/dist/stores/sqlite.d.ts +13 -0
- package/dist/stores/sqlite.d.ts.map +1 -0
- package/dist/stores/sqlite.js +215 -0
- package/dist/stores/sqlite.js.map +1 -0
- package/dist/testing.d.ts +13 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +123 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
- package/templates/react-router-auth/app/auth.ts +13 -0
- package/templates/react-router-auth/app/context.ts +6 -0
- package/templates/react-router-auth/app/routes/account.tsx +47 -0
- package/templates/react-router-auth/app/routes/login.tsx +59 -0
- package/templates/react-router-auth/app/routes/register.tsx +47 -0
- package/templates/react-router-auth/app/routes/reset-password.tsx +54 -0
- package/templates/react-router-auth/app/routes.ts +9 -0
- package/templates/react-router-auth/app/server.ts +31 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { App, Context, Middleware, MountableRoute } from '@weifuwujs/weifuwu';
|
|
2
|
+
export type AuthRole = 'user' | 'admin';
|
|
3
|
+
/** 内部用户记录(含密码哈希);对外只暴露 {@link PublicUser} */
|
|
4
|
+
export interface AuthUser {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
passwordHash: string;
|
|
8
|
+
role: AuthRole;
|
|
9
|
+
disabledAt: Date | null;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
export type PublicUser = Omit<AuthUser, 'passwordHash'>;
|
|
14
|
+
export interface AuthSession {
|
|
15
|
+
id: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
tokenHash: string;
|
|
18
|
+
createdAt: Date;
|
|
19
|
+
expiresAt: Date;
|
|
20
|
+
lastSeenAt: Date;
|
|
21
|
+
}
|
|
22
|
+
export interface UserStore {
|
|
23
|
+
createUser(input: {
|
|
24
|
+
email: string;
|
|
25
|
+
passwordHash: string;
|
|
26
|
+
role?: AuthRole;
|
|
27
|
+
}): Promise<AuthUser>;
|
|
28
|
+
findUserByEmail(email: string): Promise<AuthUser | null>;
|
|
29
|
+
findUserById(id: string): Promise<AuthUser | null>;
|
|
30
|
+
updateUserEmail(id: string, email: string): Promise<void>;
|
|
31
|
+
updateUserPassword(id: string, passwordHash: string): Promise<void>;
|
|
32
|
+
setUserRole(id: string, role: AuthRole): Promise<void>;
|
|
33
|
+
setUserDisabled(id: string, disabledAt: Date | null): Promise<void>;
|
|
34
|
+
/** 可选:列出用户(CLI `user:list` 等管理场景) */
|
|
35
|
+
listUsers?(): Promise<AuthUser[]>;
|
|
36
|
+
}
|
|
37
|
+
export interface SessionStore {
|
|
38
|
+
createSession(input: {
|
|
39
|
+
userId: string;
|
|
40
|
+
tokenHash: string;
|
|
41
|
+
expiresAt: Date;
|
|
42
|
+
}): Promise<AuthSession>;
|
|
43
|
+
findSessionByTokenHash(tokenHash: string): Promise<AuthSession | null>;
|
|
44
|
+
touchSession(id: string, lastSeenAt: Date): Promise<void>;
|
|
45
|
+
deleteSession(id: string): Promise<void>;
|
|
46
|
+
/** 删除用户的所有会话;`exceptId` 用于改密时保留当前会话 */
|
|
47
|
+
deleteUserSessions(userId: string, exceptId?: string): Promise<void>;
|
|
48
|
+
deleteExpiredSessions(now: Date): Promise<number>;
|
|
49
|
+
}
|
|
50
|
+
export interface ResetTokenStore {
|
|
51
|
+
createResetToken(input: {
|
|
52
|
+
userId: string;
|
|
53
|
+
tokenHash: string;
|
|
54
|
+
expiresAt: Date;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
/** 原子消费:有效则标记已用并返回 userId,否则返回 null */
|
|
57
|
+
consumeResetToken(tokenHash: string, now: Date): Promise<string | null>;
|
|
58
|
+
deleteExpiredResetTokens(now: Date): Promise<number>;
|
|
59
|
+
}
|
|
60
|
+
export interface AuthStore extends UserStore, SessionStore, ResetTokenStore {
|
|
61
|
+
/** 可选:建表/迁移(sqlite/postgres 实现;memory 不需要) */
|
|
62
|
+
migrate?(): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
export interface PasswordHasher {
|
|
65
|
+
hash(plain: string): string | Promise<string>;
|
|
66
|
+
verify(plain: string, stored: string): boolean | Promise<boolean>;
|
|
67
|
+
/** 返回 true 时,登录成功后会自动用新算法重算(惰性迁移) */
|
|
68
|
+
needsRehash?(stored: string): boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface RateLimitStore {
|
|
71
|
+
hit(key: string, now: number, windowMs: number): Promise<{
|
|
72
|
+
count: number;
|
|
73
|
+
resetAt: number;
|
|
74
|
+
}> | {
|
|
75
|
+
count: number;
|
|
76
|
+
resetAt: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export interface SessionOptions {
|
|
80
|
+
/** cookie 名,默认 `ww_session` */
|
|
81
|
+
cookieName?: string;
|
|
82
|
+
/** 绝对过期(秒),默认 30 天 */
|
|
83
|
+
maxAge?: number;
|
|
84
|
+
/** 空闲过期(秒),默认关闭 */
|
|
85
|
+
idleMaxAge?: number;
|
|
86
|
+
/** 默认:生产环境为 true */
|
|
87
|
+
secure?: boolean;
|
|
88
|
+
sameSite?: 'Lax' | 'Strict' | 'None';
|
|
89
|
+
path?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface RateLimitOptions {
|
|
92
|
+
/** 窗口内最大请求数,默认 10 */
|
|
93
|
+
max?: number;
|
|
94
|
+
/** 窗口毫秒数,默认 60000 */
|
|
95
|
+
windowMs?: number;
|
|
96
|
+
store?: RateLimitStore;
|
|
97
|
+
}
|
|
98
|
+
export interface AuthHooks {
|
|
99
|
+
onRegister?: (user: PublicUser) => void | Promise<void>;
|
|
100
|
+
onLogin?: (user: PublicUser) => void | Promise<void>;
|
|
101
|
+
onPasswordChange?: (user: PublicUser) => void | Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
export interface AuthOptions {
|
|
104
|
+
store: AuthStore;
|
|
105
|
+
passwordHasher?: PasswordHasher;
|
|
106
|
+
session?: SessionOptions;
|
|
107
|
+
/** 默认每 IP 每分钟 10 次;`false` 关闭 */
|
|
108
|
+
rateLimit?: RateLimitOptions | false;
|
|
109
|
+
/** 重置密码链接的投递 hook;未配置时 dev 打印、生产静默 */
|
|
110
|
+
sendResetToken?: (user: PublicUser, url: string) => void | Promise<void>;
|
|
111
|
+
/** 重置链接的基地址,默认取请求 origin */
|
|
112
|
+
baseUrl?: string;
|
|
113
|
+
/** 重置页路径,默认 `/reset-password` */
|
|
114
|
+
resetPath?: string;
|
|
115
|
+
/** 允许的跨站 Origin;默认仅同源。`false` 关闭校验 */
|
|
116
|
+
origin?: string[] | false;
|
|
117
|
+
/** 重置 token 有效期(毫秒),默认 15 分钟 */
|
|
118
|
+
resetTokenTtlMs?: number;
|
|
119
|
+
/** 密码最小长度,默认 8 */
|
|
120
|
+
minPasswordLength?: number;
|
|
121
|
+
/** 登录页路径(require 的 302 目标与表单回跳),默认 `/login` */
|
|
122
|
+
loginPath?: string;
|
|
123
|
+
/** 注册页路径(表单失败回跳),默认 `/register` */
|
|
124
|
+
registerPath?: string;
|
|
125
|
+
/** 账户页路径(表单成功/失败回跳),默认 `/account` */
|
|
126
|
+
accountPath?: string;
|
|
127
|
+
/** 端点前缀,默认 `/auth` */
|
|
128
|
+
routePrefix?: string;
|
|
129
|
+
hooks?: AuthHooks;
|
|
130
|
+
/** 可注入时钟(测试用) */
|
|
131
|
+
now?: () => Date;
|
|
132
|
+
}
|
|
133
|
+
export interface AuthContext {
|
|
134
|
+
user: PublicUser | null;
|
|
135
|
+
/** 建立会话并把 cookie 写回当前响应(自定义 OAuth/魔术链接流程用) */
|
|
136
|
+
login(user: PublicUser): Promise<void>;
|
|
137
|
+
logout(): Promise<void>;
|
|
138
|
+
verifyPassword(plain: string): Promise<boolean>;
|
|
139
|
+
}
|
|
140
|
+
/** require 的检查:角色枚举或谓词 */
|
|
141
|
+
export type AuthCheck = AuthRole | ((user: PublicUser) => boolean);
|
|
142
|
+
export type AuthErrorCode = 'invalid_credentials' | 'invalid_email' | 'weak_password' | 'email_taken' | 'invalid_token' | 'unauthorized' | 'forbidden' | 'rate_limited';
|
|
143
|
+
export type AuthResult = {
|
|
144
|
+
ok: true;
|
|
145
|
+
user: PublicUser;
|
|
146
|
+
} | {
|
|
147
|
+
ok: false;
|
|
148
|
+
error: AuthErrorCode;
|
|
149
|
+
};
|
|
150
|
+
export interface Auth {
|
|
151
|
+
readonly store: AuthStore;
|
|
152
|
+
middleware: Middleware<Context, Context & {
|
|
153
|
+
auth: AuthContext;
|
|
154
|
+
}, Response | void>;
|
|
155
|
+
require(check?: AuthCheck): Middleware<Context, Context & {
|
|
156
|
+
auth: AuthContext;
|
|
157
|
+
}, Response | void>;
|
|
158
|
+
/** 一次性接线:`app.use(auth.middleware)` + `app.wsUse(auth.middleware)` */
|
|
159
|
+
install(app: App<any>): void;
|
|
160
|
+
/** 挂到 `app.mount(auth)` 的 REST 路由 */
|
|
161
|
+
routes(): MountableRoute[];
|
|
162
|
+
migrate(): Promise<void>;
|
|
163
|
+
createSession(userId: string): Promise<{
|
|
164
|
+
token: string;
|
|
165
|
+
expiresAt: Date;
|
|
166
|
+
}>;
|
|
167
|
+
register(input: {
|
|
168
|
+
email: string;
|
|
169
|
+
password: string;
|
|
170
|
+
}): Promise<AuthResult>;
|
|
171
|
+
authenticate(input: {
|
|
172
|
+
email: string;
|
|
173
|
+
password: string;
|
|
174
|
+
}): Promise<AuthResult>;
|
|
175
|
+
changePassword(input: {
|
|
176
|
+
userId: string;
|
|
177
|
+
currentPassword: string;
|
|
178
|
+
newPassword: string;
|
|
179
|
+
exceptSessionId?: string;
|
|
180
|
+
}): Promise<AuthResult>;
|
|
181
|
+
requestPasswordReset(email: string, baseUrl?: string): Promise<void>;
|
|
182
|
+
resetPassword(input: {
|
|
183
|
+
token: string;
|
|
184
|
+
password: string;
|
|
185
|
+
}): Promise<AuthResult>;
|
|
186
|
+
/** session cookie 的 Set-Cookie 值(自定义流程/测试用) */
|
|
187
|
+
sessionCookie(token: string): string;
|
|
188
|
+
clearSessionCookie(): string;
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weifuwujs/auth",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "weifuwu 用户系统:scrypt、会话轮换、防枚举、限流、无 JS 表单页(memory/sqlite/postgres store + 契约套件)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./sqlite": {
|
|
13
|
+
"types": "./dist/sqlite.d.ts",
|
|
14
|
+
"import": "./dist/sqlite.js",
|
|
15
|
+
"default": "./dist/sqlite.js"
|
|
16
|
+
},
|
|
17
|
+
"./postgres": {
|
|
18
|
+
"types": "./dist/postgres.d.ts",
|
|
19
|
+
"import": "./dist/postgres.js",
|
|
20
|
+
"default": "./dist/postgres.js"
|
|
21
|
+
},
|
|
22
|
+
"./testing": {
|
|
23
|
+
"types": "./dist/testing.d.ts",
|
|
24
|
+
"import": "./dist/testing.js",
|
|
25
|
+
"default": "./dist/testing.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"templates"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -p tsconfig.build.json",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
35
|
+
"test": "node --test --test-force-exit \"test/**/*.test.ts\"",
|
|
36
|
+
"prepublishOnly": "npm run typecheck && npm run build && npm test"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@weifuwujs/weifuwu": ">=0.8 <1",
|
|
40
|
+
"postgres": ">=3.4.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"postgres": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@weifuwujs/weifuwu": "file:../.."
|
|
49
|
+
},
|
|
50
|
+
"bin": {
|
|
51
|
+
"weifuwu-auth": "./dist/cli.js"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createAuth } from '@weifuwujs/weifuwu/auth'
|
|
2
|
+
import { sqliteAuthStore } from '@weifuwujs/weifuwu/auth/sqlite'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 用户系统:默认 sqlite 文件(`weifuwu auth migrate` 建表)。
|
|
6
|
+
* 换 postgres:`import { postgresAuthStore } from '@weifuwujs/weifuwu/auth/postgres'`,
|
|
7
|
+
* `store: postgresAuthStore(process.env.DATABASE_URL!)`。
|
|
8
|
+
*/
|
|
9
|
+
export const auth = createAuth({
|
|
10
|
+
store: sqliteAuthStore('data/auth.db'),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type { AuthContext, PublicUser } from '@weifuwujs/weifuwu/auth'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContext } from 'react-router'
|
|
2
|
+
import type { PublicUser } from '@weifuwujs/weifuwu/auth'
|
|
3
|
+
|
|
4
|
+
/** 由 weifuwu 中间件注入、在 RR loader 中读取的上下文 */
|
|
5
|
+
export const greetingContext = createContext<string>()
|
|
6
|
+
export const authUserContext = createContext<PublicUser | null>()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Form } from 'react-router'
|
|
2
|
+
import type { Route } from './+types/account'
|
|
3
|
+
import { authUserContext } from '~/context.ts'
|
|
4
|
+
|
|
5
|
+
export function meta() {
|
|
6
|
+
return [{ title: '账户' }]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function loader({ context }: Route.LoaderArgs) {
|
|
10
|
+
const user = context.get(authUserContext)
|
|
11
|
+
if (!user) throw new Response('Unauthorized', { status: 401 })
|
|
12
|
+
return { user }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function Account({ loaderData }: Route.ComponentProps) {
|
|
16
|
+
const { user } = loaderData
|
|
17
|
+
return (
|
|
18
|
+
<main className="mx-auto max-w-sm p-8">
|
|
19
|
+
<h1 className="text-2xl font-bold">账户</h1>
|
|
20
|
+
<p className="mt-2 text-gray-600">{user.email}</p>
|
|
21
|
+
|
|
22
|
+
<h2 className="mt-6 font-semibold">修改密码</h2>
|
|
23
|
+
<Form method="post" action="/auth/password/change" className="mt-2 space-y-3">
|
|
24
|
+
<input
|
|
25
|
+
name="currentPassword"
|
|
26
|
+
type="password"
|
|
27
|
+
required
|
|
28
|
+
placeholder="当前密码"
|
|
29
|
+
className="w-full border p-2"
|
|
30
|
+
/>
|
|
31
|
+
<input
|
|
32
|
+
name="newPassword"
|
|
33
|
+
type="password"
|
|
34
|
+
required
|
|
35
|
+
minLength={8}
|
|
36
|
+
placeholder="新密码(至少 8 位)"
|
|
37
|
+
className="w-full border p-2"
|
|
38
|
+
/>
|
|
39
|
+
<button className="w-full bg-blue-600 p-2 text-white">更新密码</button>
|
|
40
|
+
</Form>
|
|
41
|
+
|
|
42
|
+
<Form method="post" action="/auth/logout" className="mt-6">
|
|
43
|
+
<button className="w-full border p-2">退出登录</button>
|
|
44
|
+
</Form>
|
|
45
|
+
</main>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Form, Link, useSearchParams } from 'react-router'
|
|
2
|
+
|
|
3
|
+
const ERRORS: Record<string, string> = {
|
|
4
|
+
invalid_credentials: '邮箱或密码不正确',
|
|
5
|
+
invalid_email: '邮箱格式不正确',
|
|
6
|
+
rate_limited: '尝试过于频繁,请稍后再试',
|
|
7
|
+
forbidden: '请求被拒绝,请刷新后重试',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function meta() {
|
|
11
|
+
return [{ title: '登录' }]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function Login() {
|
|
15
|
+
const [params] = useSearchParams()
|
|
16
|
+
const error = params.get('error')
|
|
17
|
+
return (
|
|
18
|
+
<main className="mx-auto max-w-sm p-8">
|
|
19
|
+
<h1 className="text-2xl font-bold">登录</h1>
|
|
20
|
+
{params.has('sent') && (
|
|
21
|
+
<p className="mt-2 text-sm text-green-700">
|
|
22
|
+
如果邮箱存在,重置链接已发送
|
|
23
|
+
</p>
|
|
24
|
+
)}
|
|
25
|
+
{params.has('reset') && (
|
|
26
|
+
<p className="mt-2 text-sm text-green-700">密码已重置,请登录</p>
|
|
27
|
+
)}
|
|
28
|
+
{error && (
|
|
29
|
+
<p className="mt-2 text-sm text-red-600">{ERRORS[error] ?? '请求失败'}</p>
|
|
30
|
+
)}
|
|
31
|
+
<Form method="post" action="/auth/login" className="mt-4 space-y-3">
|
|
32
|
+
<input
|
|
33
|
+
type="hidden"
|
|
34
|
+
name="next"
|
|
35
|
+
value={params.get('next') ?? '/account'}
|
|
36
|
+
/>
|
|
37
|
+
<input
|
|
38
|
+
name="email"
|
|
39
|
+
type="email"
|
|
40
|
+
required
|
|
41
|
+
placeholder="邮箱"
|
|
42
|
+
className="w-full border p-2"
|
|
43
|
+
/>
|
|
44
|
+
<input
|
|
45
|
+
name="password"
|
|
46
|
+
type="password"
|
|
47
|
+
required
|
|
48
|
+
placeholder="密码"
|
|
49
|
+
className="w-full border p-2"
|
|
50
|
+
/>
|
|
51
|
+
<button className="w-full bg-blue-600 p-2 text-white">登录</button>
|
|
52
|
+
</Form>
|
|
53
|
+
<p className="mt-4 space-x-4 text-sm">
|
|
54
|
+
<Link to="/register">注册</Link>
|
|
55
|
+
<Link to="/reset-password">忘记密码</Link>
|
|
56
|
+
</p>
|
|
57
|
+
</main>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Form, Link, useSearchParams } from 'react-router'
|
|
2
|
+
|
|
3
|
+
const ERRORS: Record<string, string> = {
|
|
4
|
+
email_taken: '该邮箱已注册',
|
|
5
|
+
invalid_email: '邮箱格式不正确',
|
|
6
|
+
weak_password: '密码至少 8 位',
|
|
7
|
+
rate_limited: '尝试过于频繁,请稍后再试',
|
|
8
|
+
forbidden: '请求被拒绝,请刷新后重试',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function meta() {
|
|
12
|
+
return [{ title: '注册' }]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function Register() {
|
|
16
|
+
const [params] = useSearchParams()
|
|
17
|
+
const error = params.get('error')
|
|
18
|
+
return (
|
|
19
|
+
<main className="mx-auto max-w-sm p-8">
|
|
20
|
+
<h1 className="text-2xl font-bold">注册</h1>
|
|
21
|
+
{error && (
|
|
22
|
+
<p className="mt-2 text-sm text-red-600">{ERRORS[error] ?? '请求失败'}</p>
|
|
23
|
+
)}
|
|
24
|
+
<Form method="post" action="/auth/register" className="mt-4 space-y-3">
|
|
25
|
+
<input
|
|
26
|
+
name="email"
|
|
27
|
+
type="email"
|
|
28
|
+
required
|
|
29
|
+
placeholder="邮箱"
|
|
30
|
+
className="w-full border p-2"
|
|
31
|
+
/>
|
|
32
|
+
<input
|
|
33
|
+
name="password"
|
|
34
|
+
type="password"
|
|
35
|
+
required
|
|
36
|
+
minLength={8}
|
|
37
|
+
placeholder="密码(至少 8 位)"
|
|
38
|
+
className="w-full border p-2"
|
|
39
|
+
/>
|
|
40
|
+
<button className="w-full bg-blue-600 p-2 text-white">注册</button>
|
|
41
|
+
</Form>
|
|
42
|
+
<p className="mt-4 text-sm">
|
|
43
|
+
<Link to="/login">已有账号,去登录</Link>
|
|
44
|
+
</p>
|
|
45
|
+
</main>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Form, Link, useSearchParams } from 'react-router'
|
|
2
|
+
|
|
3
|
+
const ERRORS: Record<string, string> = {
|
|
4
|
+
invalid_token: '链接无效或已过期,请重新申请',
|
|
5
|
+
weak_password: '密码至少 8 位',
|
|
6
|
+
rate_limited: '尝试过于频繁,请稍后再试',
|
|
7
|
+
forbidden: '请求被拒绝,请刷新后重试',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function meta() {
|
|
11
|
+
return [{ title: '重置密码' }]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function ResetPassword() {
|
|
15
|
+
const [params] = useSearchParams()
|
|
16
|
+
const token = params.get('token') ?? ''
|
|
17
|
+
const error = params.get('error')
|
|
18
|
+
return (
|
|
19
|
+
<main className="mx-auto max-w-sm p-8">
|
|
20
|
+
<h1 className="text-2xl font-bold">重置密码</h1>
|
|
21
|
+
{error && (
|
|
22
|
+
<p className="mt-2 text-sm text-red-600">{ERRORS[error] ?? '请求失败'}</p>
|
|
23
|
+
)}
|
|
24
|
+
{!error && (
|
|
25
|
+
<Form method="post" action="/auth/password/reset" className="mt-4 space-y-3">
|
|
26
|
+
<input type="hidden" name="token" value={token} />
|
|
27
|
+
<input
|
|
28
|
+
name="password"
|
|
29
|
+
type="password"
|
|
30
|
+
required
|
|
31
|
+
minLength={8}
|
|
32
|
+
placeholder="新密码(至少 8 位)"
|
|
33
|
+
className="w-full border p-2"
|
|
34
|
+
/>
|
|
35
|
+
<button className="w-full bg-blue-600 p-2 text-white">重置</button>
|
|
36
|
+
</Form>
|
|
37
|
+
)}
|
|
38
|
+
<form method="post" action="/auth/password/forgot" className="mt-4 space-y-3">
|
|
39
|
+
<p className="text-sm text-gray-600">没有链接?输入邮箱重新申请:</p>
|
|
40
|
+
<input
|
|
41
|
+
name="email"
|
|
42
|
+
type="email"
|
|
43
|
+
required
|
|
44
|
+
placeholder="邮箱"
|
|
45
|
+
className="w-full border p-2"
|
|
46
|
+
/>
|
|
47
|
+
<button className="w-full border p-2">发送重置链接</button>
|
|
48
|
+
</form>
|
|
49
|
+
<p className="mt-4 text-sm">
|
|
50
|
+
<Link to="/login">返回登录</Link>
|
|
51
|
+
</p>
|
|
52
|
+
</main>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { index, route, type RouteConfig } from '@react-router/dev/routes'
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
index('routes/home.tsx'),
|
|
5
|
+
route('login', 'routes/login.tsx'),
|
|
6
|
+
route('register', 'routes/register.tsx'),
|
|
7
|
+
route('reset-password', 'routes/reset-password.tsx'),
|
|
8
|
+
route('account', 'routes/account.tsx'),
|
|
9
|
+
] satisfies RouteConfig
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createApp, json } from '@weifuwujs/weifuwu'
|
|
2
|
+
import { reactRouter } from '@weifuwujs/weifuwu/react-router'
|
|
3
|
+
import type { Context, Middleware } from '@weifuwujs/weifuwu'
|
|
4
|
+
import { RouterContextProvider } from 'react-router'
|
|
5
|
+
import { authUserContext, greetingContext } from './context.ts'
|
|
6
|
+
import { auth } from './auth.ts'
|
|
7
|
+
|
|
8
|
+
type AppContext = Context & { greeting: string; auth?: import('@weifuwujs/weifuwu/auth').AuthContext }
|
|
9
|
+
|
|
10
|
+
const withGreeting: Middleware<Context, AppContext> = async (
|
|
11
|
+
request,
|
|
12
|
+
context,
|
|
13
|
+
next,
|
|
14
|
+
) => next(request, { ...context, greeting: 'hello-from-weifuwu' })
|
|
15
|
+
|
|
16
|
+
export default createApp()
|
|
17
|
+
.use(withGreeting)
|
|
18
|
+
.use(auth.middleware)
|
|
19
|
+
.mount(auth)
|
|
20
|
+
.use('/account', auth.require())
|
|
21
|
+
.get('/api/health', () => json({ ok: true }))
|
|
22
|
+
.notFound(
|
|
23
|
+
reactRouter<AppContext>({
|
|
24
|
+
getLoadContext(_request, context) {
|
|
25
|
+
const loadContext = new RouterContextProvider()
|
|
26
|
+
loadContext.set(greetingContext, context.greeting)
|
|
27
|
+
loadContext.set(authUserContext, context.auth?.user ?? null)
|
|
28
|
+
return loadContext
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
)
|