-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathopenapi.yaml
More file actions
3916 lines (3888 loc) · 130 KB
/
Copy pathopenapi.yaml
File metadata and controls
3916 lines (3888 loc) · 130 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
openapi: 3.0.1
info:
title: SAST Link v2 API
description: |
SAST Link v2 统一认证与用户中心 API。
- Base URL: `https://link.sast.fun/v2`
- 认证方式: JWT Bearer Token(`Authorization: Bearer <access_token>`)
- Content-Type: 标准业务接口使用 `application/json`;OAuth Token/Revoke 使用 `application/x-www-form-urlencoded`
- OAuth 2.1: 授权端点使用 PKCE-S256,第一方应用无需 client_secret
- OIDC Provider: 基于 OAuth 2.1 的 OpenID Connect 认证层,scope 含 openid 时返回 ID Token
- 响应格式: 标准业务接口使用标准化响应信封;OAuth/OIDC/健康检查/公开卡片等协议或直出端点见 StandardEnvelope 注释
本文件是目标契约,不是已注册路由的清单。
当前所有路径均已注册;实现对契约的收紧之处以 `docs/API文档.md` 为准。
version: 2.0.0
contact:
name: SAST
url: https://sast.fun
tags:
- name: Auth
- name: OAuth Login
- name: Profile
- name: Identities
- name: OAuth 2.1 Server
- name: Admin
- name: Health
- name: OIDC
- name: Card
paths:
/auth/register/send-code:
post:
summary: 发送注册验证码
deprecated: false
description: 向指定邮箱发送注册验证码。邮箱域名必须为 `@njupt.edu.cn` 或 `@sast.fun`。
operationId: authSendRegisterCode
tags:
- Auth
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- login_email
properties:
login_email:
type: string
format: email
description: 注册邮箱
example: b2404****@njupt.edu.cn
examples: {}
required: true
responses:
"200":
description: 验证码已发送
content:
application/json:
schema:
$ref: "#/components/schemas/SendCodeResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"429":
$ref: "#/components/responses/TooManyRequests"
description: 频率限制(429xx)
security: []
/auth/register/verify-code:
post:
summary: 验证注册验证码
deprecated: false
description: 注册第一步。验证邮箱验证码,返回 Register-Ticket(Redis,5 分钟,一次性)。
operationId: authVerifyRegisterCode
tags:
- Auth
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- login_email
- code
properties:
login_email:
type: string
format: email
description: 注册邮箱
code:
type: string
description: 邮箱验证码
example: "123456"
examples: {}
required: true
responses:
"200":
description: 验证成功,返回 Register-Ticket
content:
application/json:
schema:
$ref: "#/components/schemas/RegisterTicketResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"401":
$ref: "#/components/responses/Unauthorized"
description: 未认证(401xx)
security: []
/auth/register:
post:
summary: 完成注册
deprecated: false
description: >-
注册第二步。凭 Register-Ticket + 补充信息完成注册,成功后自动签发 Token。
registration_state / oauth_state 服务第三方 OAuth(GitHub / 飞书)回调的无绑定分支,
两者必须同时提供或同时省略,只给一半返回 40000。传入双值时 GetDel 一次性消费
registration_state,比对其中暂存的 oauth_state 与请求传入值,匹配后在同一事务内
创建账号、资料、identities 绑定与首个会话。registration_state 只能用于新建账号,
不可用于给已存在账号追加绑定——后者只能走 POST /user/identities/{github,lark}。
未配置任何第三方 provider 时传入这对字段返回 40000;Redis 不可用返回 50300,
不降级为无绑定注册。
operationId: authCompleteRegister
tags:
- Auth
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- register_ticket
- password
- name
- phone_number
- qq_number
- college
- major
- student_id
properties:
register_ticket:
type: string
description: 验证码验证后下发的 Register-Ticket
password:
type: string
minLength: 8
description: 登录密码,最短 8 位
example: your_password
name:
type: string
description: 真实姓名
example: 张三
phone_number:
type: string
description: 手机号
example: "13800138000"
qq_number:
type: string
description: QQ 号
example: "1234567890"
registration_state:
type: string
description: 第三方 OAuth 回调下发的注册暂存令牌(可选,需与 oauth_state 配套使用)
oauth_state:
type: string
description: 原始 OAuth CSRF state 参数(可选,与 registration_state 配套校验)
college:
$ref: "#/components/schemas/College"
description: 学院
major:
type: string
description: 专业
student_id:
type: string
maxLength: 50
description: 学号
examples: {}
required: true
responses:
"201":
description: 注册成功并返回 Token
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResultResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"409":
$ref: "#/components/responses/Conflict"
description: 资源冲突(409xx)
"429":
$ref: "#/components/responses/TooManyRequests"
description: >-
注册过于频繁(42900,带 Retry-After)。按 Register-Ticket 限流而非按 IP,
默认 5 次/5 分钟(RATE_LIMIT_REGISTER_ATTEMPTS):每个被接受的请求执行一次
argon2id 派生(默认 m=19456KiB/t2),ticket 即该成本应计量的「一个已验证邮箱」;按 IP 限流会让
校园网 NAT 后整栋楼共享一个桶。检查排在全部廉价校验之后(填错表单不计费)、
registration_state 消费之前,故被限流的请求既不消费 Register-Ticket 也不消费
registration_state,可在窗口恢复后用同一 ticket 重试——窗口不得超过 ticket 的
5 分钟 TTL,服务启动时校验。
security: []
/user/login:
post:
summary: 密码登录
deprecated: false
description: 支持教育邮箱和第三方邮箱密码登录。
operationId: authPasswordLogin
tags:
- Auth
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- login_email
- password
properties:
login_email:
type: string
format: email
description: 登录邮箱
password:
type: string
description: 登录密码
additionalProperties: false
examples: {}
required: true
responses:
"200":
description: 登录成功
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResultResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"401":
$ref: "#/components/responses/Unauthorized"
description: 未认证(401xx)
"403":
$ref: "#/components/responses/Forbidden"
description: 账号已注销(40301)
"429":
$ref: "#/components/responses/TooManyRequests"
description: 登录限流或失败锁定(42900)
"500":
$ref: "#/components/responses/InternalError"
description: 服务端错误(50000)
security: []
/auth/refresh:
post:
summary: 刷新 Token(内部登录用)
deprecated: false
description: >-
Refresh Token 旋转机制,每次使用后旧 token 撤销,下发新 token。OAuth 客户端请用 `POST
/oauth/token`(grant_type=refresh_token)。
operationId: authRefreshToken
tags:
- Auth
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- refresh_token
properties:
refresh_token:
type: string
description: 刷新令牌
example: rt_abc123...
additionalProperties: false
examples: {}
required: true
responses:
"200":
description: Token 刷新成功
content:
application/json:
schema:
$ref: "#/components/schemas/TokenRefreshResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"401":
$ref: "#/components/responses/Unauthorized"
description: Refresh Token 无效、过期或被撤销(40102)
"403":
$ref: "#/components/responses/Forbidden"
description: 账号已注销(40301)
"429":
$ref: "#/components/responses/TooManyRequests"
description: >-
`42900`(带 Retry-After)—— 按调用方 IP 限流,默认 100 次/60s
(`RATE_LIMIT_REFRESH_RPM` / `RATE_LIMIT_REFRESH_WINDOW`)。检查排在
token 查库之前。限流器故障时 fail-open。
"500":
$ref: "#/components/responses/InternalError"
description: 服务端错误(50000)
security: []
/auth/logout:
post:
summary: 登出
deprecated: false
description: 撤销当前 access_token(jti)及整条 refresh_token family。
operationId: authLogout
tags:
- Auth
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- refresh_token
properties:
refresh_token:
type: string
description: 刷新令牌
additionalProperties: false
required: true
responses:
"200":
description: 登出成功
content:
application/json:
schema:
$ref: "#/components/schemas/MessageResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"401":
$ref: "#/components/responses/Unauthorized"
description: Access/Refresh Token 无效(401xx)
"403":
$ref: "#/components/responses/Forbidden"
description: 账号已注销(40301)
"500":
$ref: "#/components/responses/InternalError"
description: 服务端错误(50000)
security:
- bearerAuth: []
/auth/change-password:
post:
summary: 修改密码
deprecated: false
description: 新密码最短 8 位,修改成功后撤销该用户所有 token family。
operationId: authChangePassword
tags:
- Auth
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- old_password
- new_password
properties:
old_password:
type: string
description: 旧密码
new_password:
type: string
minLength: 8
description: 新密码,最短 8 位
required: true
responses:
"200":
description: 密码修改成功
content:
application/json:
schema:
$ref: "#/components/schemas/MessageResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"401":
$ref: "#/components/responses/Unauthorized"
description: 未认证(401xx)
"422":
$ref: "#/components/responses/Unprocessable"
description: 业务校验失败(422xx)
security:
- bearerAuth: []
/auth/forgot-password/send-code:
post:
summary: 发送重置密码验证码
deprecated: false
description: 对格式合法且未触发限流的邮箱统一受理。响应不表示账号存在,也不保证邮件已经送达;服务端通过有界内存队列异步处理。
operationId: authSendForgotPasswordCode
tags:
- Auth
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- login_email
properties:
login_email:
type: string
format: email
description: 注册邮箱
examples: {}
required: true
responses:
"200":
description: 请求已受理;不表示账号存在或邮件已送达
content:
application/json:
schema:
$ref: "#/components/schemas/SendCodeResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"429":
$ref: "#/components/responses/TooManyRequests"
description: 频率限制(429xx)
security: []
/auth/reset-password:
post:
summary: 重置密码
deprecated: false
description: 新密码最短 8 位。
operationId: authResetPassword
tags:
- Auth
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- login_email
- code
- new_password
properties:
login_email:
type: string
format: email
description: 注册邮箱
code:
type: string
description: 邮箱验证码
new_password:
type: string
minLength: 8
description: 新密码,最短 8 位
examples: {}
required: true
responses:
"200":
description: 密码重置成功
content:
application/json:
schema:
$ref: "#/components/schemas/MessageResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"422":
$ref: "#/components/responses/Unprocessable"
description: 业务校验失败(422xx)
security: []
/oauth/github:
get:
summary: GitHub 登录
deprecated: false
description: >
重定向至 GitHub OAuth 授权页,并在 Redis 写入一次性 `oauth_state`(10min)。
`redirect` 按精确匹配校验 `OAUTH_LOGIN_REDIRECTS` 白名单,不支持前缀匹配:
回调会把 `login_code` 交给它重定向到的地址,前缀规则会让
`https://link.sast.fun.evil.test` 也通过。省略时使用白名单首项。
未启用该 provider(`OAUTH_GITHUB_ENABLED=false`)时路由仍然存在,返回 `40000`。
operationId: oauthLoginGithub
tags:
- OAuth Login
parameters:
- name: redirect
in: query
required: false
description: 登录完成后返回的前端地址,必须在 OAUTH_LOGIN_REDIRECTS 白名单内。
schema:
type: string
format: uri
responses:
"302":
description: 重定向至 GitHub OAuth 授权页
headers: {}
"400":
description: >-
`40000` —— redirect 不在允许列表中,或该 provider 未启用
"429":
$ref: "#/components/responses/TooManyRequests"
description: >-
`42900`(带 Retry-After)—— 按调用方 IP 限流,默认 100 次/60s
(RATE_LIMIT_OAUTH_LOGIN_RPM)。与 /oauth/authorize 形状相同:
无认证且每次调用写一个 oauth_state 键。限流在解析 provider 之前生效,
因此未启用的 provider 那条返回 40000 的路由也不是无成本探测面。
"503":
description: >-
`50300` —— Redis 不可用,无法写入 oauth_state。fail-closed:
没有已存储的 state 就无法校验回调,因此登录不予开始
security: []
/oauth/github/callback:
get:
summary: GitHub 回调
deprecated: false
description: >
处理 GitHub OAuth 回调。
- 已有绑定 → 签发一次性 login_code,302 重定向至前端 `?code=<login_code>`
- 无绑定 → 生成 registration_state(Redis,15min),302 重定向至注册补全页
`?registration_state=<registration_state>&oauth_state=<oauth_state>&provider=github&name=<login>&avatar=<url>`
operationId: oauthGithubCallback
tags:
- OAuth Login
parameters:
- name: code
in: query
description: GitHub 授权码
required: true
schema:
type: string
- name: state
in: query
description: CSRF 状态值
required: true
schema:
type: string
responses:
"302":
description: 重定向至前端
headers: {}
security: []
/oauth/lark:
get:
summary: 飞书登录
deprecated: false
description: >
重定向至飞书 OAuth 授权页,并在 Redis 写入一次性 `oauth_state`(10min)。
`redirect` 的白名单校验与 `/oauth/github` 相同。未启用该 provider
(`OAUTH_FEISHU_ENABLED=false`)时路由仍然存在,返回 `40000`。
operationId: oauthLoginLark
tags:
- OAuth Login
parameters:
- name: redirect
in: query
required: false
description: 登录完成后返回的前端地址,必须在 OAUTH_LOGIN_REDIRECTS 白名单内。
schema:
type: string
format: uri
responses:
"302":
description: 重定向至飞书 OAuth 授权页
headers: {}
"400":
description: >-
`40000` —— redirect 不在允许列表中,或该 provider 未启用
"429":
$ref: "#/components/responses/TooManyRequests"
description: >-
`42900`(带 Retry-After)—— 按调用方 IP 限流,默认 100 次/60s
(RATE_LIMIT_OAUTH_LOGIN_RPM),与 GitHub 授权端点共用同一档配额与
同一 Redis 计数域(endpoint 名 oauth_login)。
"503":
description: "`50300` —— Redis 不可用,无法写入 oauth_state(fail-closed)"
security: []
/oauth/lark/callback:
get:
summary: 飞书回调
deprecated: false
description: >
处理飞书 OAuth 回调,仅限 SAST 企业内飞书用户。
- 已有绑定 → 签发一次性 login_code,302 重定向至前端
- 无绑定 → 生成 registration_state(Redis,15min),302 重定向至注册补全页
`?registration_state=<registration_state>&oauth_state=<oauth_state>&provider=lark&name=<name>&avatar=<url>`
- 非 SAST 企业用户 → 拒绝
operationId: oauthLarkCallback
tags:
- OAuth Login
parameters:
- name: code
in: query
description: 飞书授权码
required: true
schema:
type: string
- name: state
in: query
description: CSRF 状态值
required: true
schema:
type: string
responses:
"302":
description: 重定向至前端
headers: {}
security: []
/oauth/exchange-code:
post:
summary: 交换登录码
deprecated: false
description: 用 OAuth 回调中的一次性 login_code(Redis,60 秒)换取 token。
operationId: authExchangeLoginCode
tags:
- OAuth Login
parameters: []
requestBody:
content:
application/json:
schema:
type: object
required:
- code
properties:
code:
type: string
description: 一次性登录码
example: lc_abc123...
examples: {}
required: true
responses:
"200":
description: 交换成功,返回 Token
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResultResponse"
headers: {}
"401":
$ref: "#/components/responses/Unauthorized"
description: 未认证(401xx)
"429":
$ref: "#/components/responses/TooManyRequests"
description: >-
`42900`(带 Retry-After)—— 按调用方 IP 限流,默认 100 次/60s
(RATE_LIMIT_EXCHANGE_CODE_RPM)。本端点按设计免认证——兑换 login_code
正是取得会话的方式——故配额是约束无成本探测的唯一手段。限流检查排在
空 code 校验之前:调用方控制输入,先直接拒空会让每次猜测一次
Redis GetDel 的昂贵路径保持敞开。被限流的请求不消费 login_code。
security: []
/user/profile:
get:
summary: 获取当前用户信息
deprecated: false
description: ""
operationId: userGetProfile
tags:
- Profile
parameters: []
responses:
"200":
description: 用户信息
content:
application/json:
schema:
$ref: "#/components/schemas/UserProfileResponse"
headers: {}
"401":
$ref: "#/components/responses/Unauthorized"
description: 未认证(401xx)
"403":
$ref: "#/components/responses/Forbidden"
description: 账号已注销(40301)
"500":
$ref: "#/components/responses/InternalError"
description: 服务端错误(50000)
security:
- bearerAuth: []
put:
summary: 更新用户资料
deprecated: false
description: |
更新当前登录用户可自助维护的个人信息。允许更新 user 表中的基础信息
(真实姓名、手机号、QQ 号、学院、专业、学号)以及 profile 表中的展示资料。
登录邮箱、角色、状态、邮箱类型等身份与权限字段不可通过此接口修改,传入未知字段返回 40000。
未传的字段保持不变;传 null 等同于未传,不表示清空。profile 侧的可空展示字段
(nickname、intro、email、blog_url、github_url、department)传空字符串表示
清空为 null;user 侧字段 NOT NULL,传空字符串或纯空白返回 40000。
所有文本字段拒绝控制字符(含 NUL、CR、LF、Tab),返回 40000。
blog_url / github_url 必须是 http/https 绝对 URL。
college 必须是 college_enum 完整枚举值,department 必须是 software / media 或空字符串。
operationId: userUpdateProfile
tags:
- Profile
parameters: []
requestBody:
content:
application/json:
schema:
type: object
minProperties: 1
properties:
name:
type: string
nullable: true
maxLength: 255
description: 真实姓名
example: 张三
phone_number:
type: string
nullable: true
maxLength: 20
description: 手机号
example: "13800138000"
qq_number:
type: string
nullable: true
maxLength: 20
description: QQ 号
example: "1234567890"
college:
allOf:
- $ref: "#/components/schemas/College"
nullable: true
description: 学院。NOT NULL 字段,传空字符串或空白返回 40000;传 null 表示不修改
major:
type: string
nullable: true
maxLength: 50
description: 专业
example: 软件工程
student_id:
type: string
nullable: true
maxLength: 50
description: 学号
example: B2404****
nickname:
type: string
nullable: true
maxLength: 255
description: 昵称
example: 新昵称
department:
type: string
nullable: true
enum: [software, media, ""]
description: 部门。传空字符串清空为 NULL,传 null 表示不修改
intro:
type: string
nullable: true
maxLength: 255
description: 自我介绍
example: 新的自我介绍
email:
type: string
nullable: true
format: email
maxLength: 255
description: 对外展示邮箱
example: display@example.com
blog_url:
type: string
nullable: true
format: uri
maxLength: 512
description: 个人博客链接
example: https://blog.example.com
github_url:
type: string
nullable: true
format: uri
maxLength: 512
description: GitHub 主页链接
example: https://github.com/example
required: true
responses:
"200":
description: 更新成功
content:
application/json:
schema:
$ref: "#/components/schemas/ProfileUpdateResponse"
headers: {}
"401":
$ref: "#/components/responses/Unauthorized"
description: 未认证(401xx)
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误、未知字段、枚举/长度/链接校验失败(40000)
"403":
$ref: "#/components/responses/Forbidden"
description: 账号已注销(40301)
"409":
$ref: "#/components/responses/Conflict"
description: 学号已被占用(40902)
"500":
$ref: "#/components/responses/InternalError"
security:
- bearerAuth: []
/user/avatar:
put:
summary: 上传头像
deprecated: false
description: >-
上传头像至对象存储(腾讯云 COS),经内容审核后写入 profile.avatar。
依赖 STORAGE_* 配置;未配置时返回 50002。
图片限制 5MB,格式 jpg/png/webp。
operationId: userUploadAvatar
tags:
- Profile
parameters: []
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
description: 图片文件(限制 5MB,格式 jpg/png/webp)
example: ""
required:
- file
required: true
responses:
"200":
description: 上传成功
content:
application/json:
schema:
$ref: "#/components/schemas/AvatarUploadResponse"
headers: {}
"400":
$ref: "#/components/responses/BadRequest"
description: 参数错误(400xx)
"401":
$ref: "#/components/responses/Unauthorized"
description: 未认证(401xx)
"422":
description: >-
头像未通过内容审核(42203)
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"429":
$ref: "#/components/responses/RateLimited"
description: 请求过于频繁(42900)
"500":
$ref: "#/components/responses/InternalError"
description: 服务端错误(500xx,含 50002 对象存储失败)
"503":
description: 内容审核服务不可用(50300,fail-closed)
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
security:
- bearerAuth: []
/card/{id}:
get:
summary: 获取个人卡片
deprecated: true
description: |
已下线(路由注释,暂不响应)。顺序 ID 的公开 URL 可枚举全站成员名单,隐私重设计中。
重开后为 owner-only + 不可枚举标识,不会原样启用。
operationId: cardGetCard
tags:
- Card
parameters:
- name: id
in: path
description: 用户 ID,正整数;非正整数或含非数字字符返回 404
required: true
example: 12
schema:
type: integer
format: int64
minimum: 1
responses:
"200":
description: 个人卡片数据
content:
application/json:
schema:
$ref: "#/components/schemas/CardResponse"
headers: {}
"404":
$ref: "#/components/responses/NotFound"
description: 用户不存在或已注销
"429":
$ref: "#/components/responses/TooManyRequests"
description: >-
请求过于频繁(42900,带 Retry-After)。按调用方 IP 限流,默认 300 次/60s
(RATE_LIMIT_CARD_RPM):本端点无认证且路径参数是连续用户 ID,
不限流即等于开放全站公开卡片的抓取。限流在 ID 校验之前生效,
因此无效 ID 同样消耗配额——它得到的 404 正是枚举者要读的信号。
配额按共享出口 IP 下的成员墙定档,只能减缓而非阻止抓取;
公开卡片的批量读取应交由反向代理缓存承担。
"500":
$ref: "#/components/responses/InternalError"
security: []
/user/identities:
get:
summary: 获取绑定列表
deprecated: false
description: ""
operationId: userListIdentities
tags:
- Identities
parameters: []
responses:
"200":
description: 绑定列表
content:
application/json:
schema:
$ref: "#/components/schemas/IdentitiesListResponse"
headers: {}
"401":
$ref: "#/components/responses/Unauthorized"
description: 未认证(401xx)
"403":
$ref: "#/components/responses/Forbidden"
description: 账号已注销(40301)
"500":
$ref: "#/components/responses/InternalError"
security:
- bearerAuth: []
/user/identities/lark:
post:
summary: 绑定飞书
deprecated: false
description: >
每个用户只能绑定一个飞书账号,每个飞书账号只能绑定一个用户。
绑定与登录使用**不同的回调地址**,两条都要登记进飞书应用的重定向白名单。
登录回调(`OAUTH_FEISHU_REDIRECT_URI`)指向本后端的
`/oauth/lark/callback`;绑定回调是前端页面,由它拿到 provider code 后
调用本接口。
本接口不接受 `registration_state`:该值只证明有人完成了一次第三方回调,
不证明是哪个 SAST 账号在操作,因此调用者一律由 Bearer token 认定。
operationId: userBindLark
tags:
- Identities
parameters:
- name: code
in: query
description: 飞书 OAuth 授权码
required: true
schema:
type: string
- name: redirect_uri
in: query
required: false
description: >
签发该 code 时使用的回调地址(前端的绑定回调页)。RFC 6749 §4.1.3
要求 token 交换重复这个值;飞书注册了多条回调时,不一致会以
invalid_grant 拒绝。省略时回退到 OAUTH_FEISHU_REDIRECT_URI,仅在
绑定与登录共用同一回调地址时适用。
schema:
type: string
format: uri
responses:
"200":
description: 绑定成功
content:
application/json:
schema:
$ref: "#/components/schemas/IdentityBindResponse"
headers: {}
"409":
$ref: "#/components/responses/Conflict"