forked from casdoor/casdoor
Compare commits
718 Commits
master_old
...
copilot/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed74d69fcc | ||
|
|
d898202dad | ||
|
|
b25f11ad9a | ||
|
|
103e2fef02 | ||
|
|
fa89321cb7 | ||
|
|
376fa9751f | ||
|
|
3cb9df3723 | ||
|
|
9d1e5c10d0 | ||
|
|
ef84c4b0b4 | ||
|
|
5a108bd921 | ||
|
|
ac671ec1ee | ||
|
|
7814caf2ab | ||
|
|
f966f4a0f9 | ||
|
|
a4b1a068a8 | ||
|
|
362797678d | ||
|
|
7879e1bf09 | ||
|
|
c246f102c9 | ||
|
|
37d1c4910c | ||
|
|
3bcde7cb7c | ||
|
|
6a90d21941 | ||
|
|
80b4c0b1a7 | ||
|
|
eb5a422026 | ||
|
|
f7bd70e0a3 | ||
|
|
5e7dbe4b56 | ||
|
|
bd1fca2f32 | ||
|
|
3d4cc42f1f | ||
|
|
1836cab44d | ||
|
|
75b18635f7 | ||
|
|
47cd44c7ce | ||
|
|
090ca97dcd | ||
|
|
bed01b31f1 | ||
|
|
c8f8f88d85 | ||
|
|
7acb303995 | ||
|
|
2607f8d3e5 | ||
|
|
481db33e58 | ||
|
|
f556c7e11f | ||
|
|
f590992f28 | ||
|
|
80f9db0fa2 | ||
|
|
0748661d2a | ||
|
|
83552ed143 | ||
|
|
8cb8541f96 | ||
|
|
5b646a726c | ||
|
|
19b9586670 | ||
|
|
73f8d19c5f | ||
|
|
04da531df3 | ||
|
|
d97558051d | ||
|
|
ac55355290 | ||
|
|
a2da380be4 | ||
|
|
ecf8039c5d | ||
|
|
0a6948034c | ||
|
|
442f8fb19e | ||
|
|
b771add9e3 | ||
|
|
df8e9fceea | ||
|
|
d674f0c33d | ||
|
|
1e1b5273d9 | ||
|
|
cf5e88915c | ||
|
|
c8973e6c9e | ||
|
|
87ea451561 | ||
|
|
8f32779b42 | ||
|
|
aba471b4e8 | ||
|
|
72b70c3b03 | ||
|
|
a1c56894c7 | ||
|
|
a9ae9394c7 | ||
|
|
5f0fa5f23e | ||
|
|
f99aa047a9 | ||
|
|
1d22b7ebd0 | ||
|
|
d147053329 | ||
|
|
0f8cd92be4 | ||
|
|
7ea6f1296d | ||
|
|
db8c649f5e | ||
|
|
a06d003589 | ||
|
|
33298e44d4 | ||
|
|
f4d86f8d92 | ||
|
|
af4337a1ae | ||
|
|
81e650df65 | ||
|
|
fcea1e4c07 | ||
|
|
639a8a47b1 | ||
|
|
43f61d4426 | ||
|
|
e90cdb8a74 | ||
|
|
bfe8955250 | ||
|
|
36b9c4602a | ||
|
|
18117833e1 | ||
|
|
78dde97b64 | ||
|
|
3a06c66057 | ||
|
|
aa59901400 | ||
|
|
8e03b2d97c | ||
|
|
d1da9499e8 | ||
|
|
2e7673c015 | ||
|
|
2d1ace427e | ||
|
|
039c12afa3 | ||
|
|
4236160fa7 | ||
|
|
071b5ddec0 | ||
|
|
f46b92d225 | ||
|
|
cc7eb4664c | ||
|
|
1567723e2b | ||
|
|
074253f45e | ||
|
|
23c86e9018 | ||
|
|
f088827a50 | ||
|
|
663815fefe | ||
|
|
0d003d347e | ||
|
|
7d495ca5f2 | ||
|
|
f89495b35c | ||
|
|
4a3aefc5f5 | ||
|
|
15646b23ff | ||
|
|
4b663a437f | ||
|
|
9fb90fbb95 | ||
|
|
65eeaef8a7 | ||
|
|
ecf8e2eb32 | ||
|
|
e49e678d16 | ||
|
|
623ee23285 | ||
|
|
0901a1d5a0 | ||
|
|
58ff2fe69c | ||
|
|
737f44a059 | ||
|
|
32cef8e828 | ||
|
|
9e854abc77 | ||
|
|
9b3343d3db | ||
|
|
5b71725c94 | ||
|
|
59b6854ccc | ||
|
|
0daf67c52c | ||
|
|
4b612269ea | ||
|
|
f438d39720 | ||
|
|
f8df200dbf | ||
|
|
cb1b3b767e | ||
|
|
3bec49f16c | ||
|
|
e28344f0e7 | ||
|
|
93fefed6e8 | ||
|
|
ea9abb2f29 | ||
|
|
337a8c357b | ||
|
|
d8cebfbf04 | ||
|
|
91d5039155 | ||
|
|
5996ee8695 | ||
|
|
8c9331932b | ||
|
|
db594e2096 | ||
|
|
b46b79ee44 | ||
|
|
b9dbbca716 | ||
|
|
313cf6d480 | ||
|
|
0548597d04 | ||
|
|
eb8e26748f | ||
|
|
516a23ab1b | ||
|
|
9887d80e55 | ||
|
|
13dd4337a6 | ||
|
|
36c69a6da1 | ||
|
|
3f4a60096a | ||
|
|
b6240fa356 | ||
|
|
d61f06b053 | ||
|
|
6fe785b6a4 | ||
|
|
cccddea67e | ||
|
|
83b8c5477a | ||
|
|
ac0e069f71 | ||
|
|
4b25e56048 | ||
|
|
39740e3d6c | ||
|
|
87c5bf3855 | ||
|
|
c4a28acbd8 | ||
|
|
ee26b896f6 | ||
|
|
4a8cb9535e | ||
|
|
387a22d5f8 | ||
|
|
36cadded1c | ||
|
|
7d130392d9 | ||
|
|
f82c90b901 | ||
|
|
1a08d6514e | ||
|
|
4d5bf09b36 | ||
|
|
f050deada7 | ||
|
|
dee94666e0 | ||
|
|
b84b7d787b | ||
|
|
d425183137 | ||
|
|
ff7fcd277c | ||
|
|
ed5c0b2713 | ||
|
|
eb60e43192 | ||
|
|
d0170532e6 | ||
|
|
7ddb87cdf8 | ||
|
|
fac45f5ac7 | ||
|
|
266d361244 | ||
|
|
b454ab1931 | ||
|
|
ff39b6f186 | ||
|
|
0597dbbe20 | ||
|
|
49c417c70e | ||
|
|
8b30e12915 | ||
|
|
2e18c65429 | ||
|
|
27c98bb056 | ||
|
|
4400b66862 | ||
|
|
e7e7d18ee7 | ||
|
|
66d1e28300 | ||
|
|
53782a6706 | ||
|
|
30bb0ce92f | ||
|
|
29f7dda858 | ||
|
|
68b82ed524 | ||
|
|
c4ce88198f | ||
|
|
a11fa23add | ||
|
|
add6ba32db | ||
|
|
37379dee13 | ||
|
|
2066670b76 | ||
|
|
e751148be2 | ||
|
|
c541d0bcdd | ||
|
|
f0db95d006 | ||
|
|
e4db367eaa | ||
|
|
9df81e3ffc | ||
|
|
048d6acc83 | ||
|
|
e440199977 | ||
|
|
cb4e559d51 | ||
|
|
4d1d0b95d6 | ||
|
|
9cc1133a96 | ||
|
|
897c28e8ad | ||
|
|
9d37a7e38e | ||
|
|
ea597296b4 | ||
|
|
427ddd215e | ||
|
|
24de79b100 | ||
|
|
9ab9c7c8e0 | ||
|
|
0728a9716b | ||
|
|
471570f24a | ||
|
|
2fa520844b | ||
|
|
2306acb416 | ||
|
|
d3f3f76290 | ||
|
|
fe93128495 | ||
|
|
7fd890ff14 | ||
|
|
83b56d7ceb | ||
|
|
503e5a75d2 | ||
|
|
5a607b4991 | ||
|
|
ca2dc2825d | ||
|
|
446d0b9047 | ||
|
|
ee708dbf48 | ||
|
|
221ca28488 | ||
|
|
e93d3f6c13 | ||
|
|
e285396d4e | ||
|
|
10320bb49f | ||
|
|
4d27ebd82a | ||
|
|
6d5e6dab0a | ||
|
|
e600ea7efd | ||
|
|
8002613398 | ||
|
|
a48b1d0c73 | ||
|
|
d8b5ecba36 | ||
|
|
e3a8a464d5 | ||
|
|
a575ba02d6 | ||
|
|
a9fcfceb8f | ||
|
|
712482ffb9 | ||
|
|
84e2c760d9 | ||
|
|
4ab85d6781 | ||
|
|
2ede56ac46 | ||
|
|
6a819a9a20 | ||
|
|
ddaeac46e8 | ||
|
|
f9d061d905 | ||
|
|
5e550e4364 | ||
|
|
146d54d6f6 | ||
|
|
1df15a2706 | ||
|
|
f7d73bbfdd | ||
|
|
a8b7217348 | ||
|
|
40a3b19cee | ||
|
|
98b45399a7 | ||
|
|
90edb7ab6b | ||
|
|
e21b995eca | ||
|
|
81221f07f0 | ||
|
|
5fc2cdf637 | ||
|
|
5e852e0121 | ||
|
|
513ac6ffe9 | ||
|
|
821ba5673d | ||
|
|
d3ee73e48c | ||
|
|
1d719e3759 | ||
|
|
b3355a9fa6 | ||
|
|
ccc88cdafb | ||
|
|
abf328bbe5 | ||
|
|
5530253d38 | ||
|
|
4cef6c5f3f | ||
|
|
7e6929b900 | ||
|
|
46ae1a9580 | ||
|
|
37e22f3e2c | ||
|
|
68cde65d84 | ||
|
|
1c7f5fdfe4 | ||
|
|
1a5be46325 | ||
|
|
f7bafb28d6 | ||
|
|
6f815aefdf | ||
|
|
eb49f29529 | ||
|
|
5ad4e6aac0 | ||
|
|
3c28a2202d | ||
|
|
0a9a9117e5 | ||
|
|
f3ee1f83fe | ||
|
|
171af2901c | ||
|
|
2ded293e10 | ||
|
|
a1c6d6c6cf | ||
|
|
bf42176708 | ||
|
|
23a45c1d33 | ||
|
|
6894ca407e | ||
|
|
d288ecf6ed | ||
|
|
0a04174ec8 | ||
|
|
3feb723abf | ||
|
|
ff8b8fb631 | ||
|
|
df38c0dd62 | ||
|
|
93e87e009e | ||
|
|
f0a4ccbc3c | ||
|
|
f17c8622f7 | ||
|
|
09698b0714 | ||
|
|
1d913677a0 | ||
|
|
f3b00fb431 | ||
|
|
c95a427635 | ||
|
|
778be62bae | ||
|
|
5574c6ad0d | ||
|
|
36db852a32 | ||
|
|
8ee8767882 | ||
|
|
af5a9c805d | ||
|
|
f8e5fedf8b | ||
|
|
962a4970f4 | ||
|
|
d239b3f0cb | ||
|
|
0df467ce5e | ||
|
|
3d5356a1f0 | ||
|
|
1824762e00 | ||
|
|
a533212d8a | ||
|
|
53e1813dc8 | ||
|
|
ba95c7ffb0 | ||
|
|
10105de418 | ||
|
|
9582163bdd | ||
|
|
cc7408e976 | ||
|
|
d67d714105 | ||
|
|
0aab27f154 | ||
|
|
212090325b | ||
|
|
b24e43c736 | ||
|
|
1728bf01ac | ||
|
|
86a7a87c57 | ||
|
|
61c8e08eb0 | ||
|
|
caccd75edb | ||
|
|
7b2666d23e | ||
|
|
b7b6d2377a | ||
|
|
d43ee2d48f | ||
|
|
242c75d9dc | ||
|
|
6571ad88a2 | ||
|
|
bb33c8ea31 | ||
|
|
48f5531332 | ||
|
|
3e5114e42d | ||
|
|
03082db9f2 | ||
|
|
a2363e55e7 | ||
|
|
dde4e41e24 | ||
|
|
c3eea4d895 | ||
|
|
4ff28cacbe | ||
|
|
e8ed9ca9e3 | ||
|
|
8f8b7e5215 | ||
|
|
099e6437a9 | ||
|
|
fdbb0d52da | ||
|
|
9c89705a19 | ||
|
|
18451a874e | ||
|
|
99dae68c53 | ||
|
|
7e2c2bfc64 | ||
|
|
4ae6675198 | ||
|
|
8c37533b92 | ||
|
|
3e77bd30a0 | ||
|
|
55257d6190 | ||
|
|
b9046bec01 | ||
|
|
40d4e3a1a9 | ||
|
|
60bfc8891a | ||
|
|
126879533b | ||
|
|
469b6036fd | ||
|
|
6c750867b0 | ||
|
|
625b3e2c63 | ||
|
|
28dff8083a | ||
|
|
02c4bddb5f | ||
|
|
df65fb3525 | ||
|
|
d3bbf954f8 | ||
|
|
f3755d925c | ||
|
|
ca819e7e83 | ||
|
|
d619e91d9e | ||
|
|
5079c37818 | ||
|
|
d5f29d716a | ||
|
|
00b278a00f | ||
|
|
d883db907b | ||
|
|
8e7efe5c23 | ||
|
|
bf75508d95 | ||
|
|
986b94cc90 | ||
|
|
890f528556 | ||
|
|
b46e779235 | ||
|
|
5c80948a06 | ||
|
|
1467199159 | ||
|
|
64c2b8f0c2 | ||
|
|
8f7ea7f0a0 | ||
|
|
2ab85c0c44 | ||
|
|
bf67be2af6 | ||
|
|
bc94735a8d | ||
|
|
89c6ef5aae | ||
|
|
21da9f5ff2 | ||
|
|
3b11e778e7 | ||
|
|
ad240a373f | ||
|
|
01000f7022 | ||
|
|
f93aeb5350 | ||
|
|
8fa681f883 | ||
|
|
3b16406442 | ||
|
|
fbc16ef124 | ||
|
|
f26f56e88b | ||
|
|
9cb633c9e2 | ||
|
|
d0d059d42f | ||
|
|
c184dc7f3a | ||
|
|
2fa0890c11 | ||
|
|
a0e2be7ba8 | ||
|
|
09b389b1f7 | ||
|
|
a23033758f | ||
|
|
f7bc822087 | ||
|
|
e533ff1ee1 | ||
|
|
9f187f690e | ||
|
|
fe5aa1f214 | ||
|
|
eda742a848 | ||
|
|
83df077a02 | ||
|
|
ad6080e763 | ||
|
|
c179324de4 | ||
|
|
645716e485 | ||
|
|
955e73ddd1 | ||
|
|
2493ae9cfe | ||
|
|
b5c80513fb | ||
|
|
0653353be1 | ||
|
|
d6778fb4e6 | ||
|
|
fee7773839 | ||
|
|
d47ac6b957 | ||
|
|
857824df19 | ||
|
|
1e98d1e11b | ||
|
|
48ba88de2d | ||
|
|
a3a142db39 | ||
|
|
3bb7cc6b81 | ||
|
|
1fb3249bfd | ||
|
|
ff8f61a84c | ||
|
|
a118879dc0 | ||
|
|
386b673446 | ||
|
|
6abd46fe81 | ||
|
|
49d734d249 | ||
|
|
f5b4cd7fab | ||
|
|
76f322861a | ||
|
|
124c28f1e1 | ||
|
|
e0d9cc7ed1 | ||
|
|
75c1ae4366 | ||
|
|
d537377b31 | ||
|
|
462ecce43b | ||
|
|
a84664b55d | ||
|
|
941c56e69e | ||
|
|
a28b871a46 | ||
|
|
387f5d58f7 | ||
|
|
7d846b2060 | ||
|
|
c1c2dcab38 | ||
|
|
f9264f700b | ||
|
|
f3af2a26aa | ||
|
|
0ac69bde53 | ||
|
|
70c99f0e59 | ||
|
|
8d1fdc3a08 | ||
|
|
30c15b8135 | ||
|
|
2d6de216b8 | ||
|
|
ac39722687 | ||
|
|
26a9ec8ee6 | ||
|
|
fea6317430 | ||
|
|
5f702ca418 | ||
|
|
0495d17a07 | ||
|
|
c6a2d59aa4 | ||
|
|
d867afdd70 | ||
|
|
a92430e8fd | ||
|
|
447cb70553 | ||
|
|
e05fbec739 | ||
|
|
65ab36f073 | ||
|
|
d027e07383 | ||
|
|
d3c718b577 | ||
|
|
ea68e6c2dc | ||
|
|
7aa0b2e63f | ||
|
|
a39b121280 | ||
|
|
feef4cc242 | ||
|
|
1b5ef53655 | ||
|
|
18d639cca2 | ||
|
|
3ac5aad648 | ||
|
|
2a53241128 | ||
|
|
835273576b | ||
|
|
7fdc264ff6 | ||
|
|
a120734bb1 | ||
|
|
edd0b30e08 | ||
|
|
2da597b26f | ||
|
|
ef14c84edc | ||
|
|
cb5c7667b5 | ||
|
|
920ed87f75 | ||
|
|
6598f0ccdf | ||
|
|
8e71e23d75 | ||
|
|
146a369f80 | ||
|
|
9bbe5afb7c | ||
|
|
b42391c6ce | ||
|
|
fb035a5353 | ||
|
|
b1f68a60a4 | ||
|
|
201d704a31 | ||
|
|
bf91ad6c97 | ||
|
|
3ccc0339c7 | ||
|
|
1f2b0a3587 | ||
|
|
0b3feb0d5f | ||
|
|
568c0e2c3d | ||
|
|
f4ad2b4034 | ||
|
|
c9f8727890 | ||
|
|
e2e3c1fbb8 | ||
|
|
73915ac0a0 | ||
|
|
bf9d55ff40 | ||
|
|
b36fb50239 | ||
|
|
4307baa759 | ||
|
|
3964bae1df | ||
|
|
d9b97d70be | ||
|
|
ca224fdd4c | ||
|
|
37daea2bbc | ||
|
|
af231bf946 | ||
|
|
6dc7b4d533 | ||
|
|
12cc0f429e | ||
|
|
8cc22dec91 | ||
|
|
0c08ae5365 | ||
|
|
c3485268d3 | ||
|
|
64a4956c42 | ||
|
|
855bdf47e8 | ||
|
|
de7e322fbb | ||
|
|
4cb0cd7c5a | ||
|
|
c6a50349cc | ||
|
|
8a098a4b6e | ||
|
|
09f98fd24a | ||
|
|
515d209063 | ||
|
|
4e17dae2c2 | ||
|
|
0ad4d82d9c | ||
|
|
731daf5204 | ||
|
|
b6b77da7cf | ||
|
|
8b4637aa3a | ||
|
|
87506b84e3 | ||
|
|
fed9332246 | ||
|
|
33afc52a0b | ||
|
|
9035ca365a | ||
|
|
b97ae72179 | ||
|
|
9190db1099 | ||
|
|
1173f75794 | ||
|
|
086859d1ce | ||
|
|
9afaf5d695 | ||
|
|
521f90a603 | ||
|
|
4260efcfd0 | ||
|
|
d772b0b7a8 | ||
|
|
702b390da1 | ||
|
|
b15b3b9335 | ||
|
|
f8f864c5b9 | ||
|
|
90e790f83c | ||
|
|
58413246f3 | ||
|
|
8f307dd907 | ||
|
|
fe42b5e0ba | ||
|
|
383bf44391 | ||
|
|
36f5de3203 | ||
|
|
eae69c41d7 | ||
|
|
91057f54f3 | ||
|
|
daa7b79915 | ||
|
|
d3a5539dae | ||
|
|
7d1c614452 | ||
|
|
e2eafa909b | ||
|
|
56bcef0592 | ||
|
|
0860cbf343 | ||
|
|
2f4180b1b6 | ||
|
|
e3d5619b25 | ||
|
|
019fd87b92 | ||
|
|
5c41c6c4a5 | ||
|
|
b7fafcc62b | ||
|
|
493ceddcd9 | ||
|
|
fc618b9bd5 | ||
|
|
a00900e405 | ||
|
|
77ef5828dd | ||
|
|
c11f013e04 | ||
|
|
b3bafe8402 | ||
|
|
f04a431d85 | ||
|
|
952538916d | ||
|
|
18bb445e71 | ||
|
|
cca88e2cb0 | ||
|
|
86c10fe0ab | ||
|
|
c1b3bf0f45 | ||
|
|
62bda61af5 | ||
|
|
b6f943e326 | ||
|
|
2cc5e82d91 | ||
|
|
e55cd94298 | ||
|
|
08f7a05e61 | ||
|
|
4bee21f4a3 | ||
|
|
5417a90223 | ||
|
|
131820e34e | ||
|
|
2fcbf7cf6c | ||
|
|
14ade8b7e4 | ||
|
|
a11fe59704 | ||
|
|
af55d0547f | ||
|
|
81102f8298 | ||
|
|
141372cb86 | ||
|
|
15a037ca74 | ||
|
|
73c680d56f | ||
|
|
aafc16e4f4 | ||
|
|
7be026dd1f | ||
|
|
3e7938e5f6 | ||
|
|
30789138e2 | ||
|
|
9610ce5b8c | ||
|
|
a39a311d2f | ||
|
|
08e41ab762 | ||
|
|
85ca318e2f | ||
|
|
9032865e60 | ||
|
|
5692522ee0 | ||
|
|
cb1882e589 | ||
|
|
41d9422687 | ||
|
|
3297db688b | ||
|
|
cc82d292f0 | ||
|
|
f2e3037bc5 | ||
|
|
d986a4a9e0 | ||
|
|
2df3878c15 | ||
|
|
24ab8880cc | ||
|
|
f26b4853c5 | ||
|
|
d78e8e9776 | ||
|
|
d61f9a1856 | ||
|
|
aa52af02b3 | ||
|
|
2a5722e45b | ||
|
|
26718bc4a1 | ||
|
|
f8d44e2dca | ||
|
|
26eea501be | ||
|
|
63b8e857bc | ||
|
|
81b336b37a | ||
|
|
9c39179849 | ||
|
|
37d93a5eea | ||
|
|
e926a07c58 | ||
|
|
9c46344e68 | ||
|
|
c0ec73dfd3 | ||
|
|
b1b6ebe692 | ||
|
|
a0931e4597 | ||
|
|
c181006661 | ||
|
|
2e83e49492 | ||
|
|
5661942175 | ||
|
|
7f9f7c6468 | ||
|
|
b7a818e2d3 | ||
|
|
1a8cfe4ee6 | ||
|
|
b3526de675 | ||
|
|
3b9e08b70d | ||
|
|
cfc6015aca | ||
|
|
1600a6799a | ||
|
|
ca60cc3a33 | ||
|
|
df295717f0 | ||
|
|
e3001671a2 | ||
|
|
bbe2162e27 | ||
|
|
92b5ce3722 | ||
|
|
bad21fb6bb | ||
|
|
5a78dcf06d | ||
|
|
558b168477 | ||
|
|
802b6812a9 | ||
|
|
a5a627f92e | ||
|
|
9701818a6e | ||
|
|
06986fbd41 | ||
|
|
3d12ac8dc2 | ||
|
|
f01839123f | ||
|
|
e1b3b0ac6a | ||
|
|
4b0a2fdbfc | ||
|
|
db551eb24a | ||
|
|
18b49bb731 | ||
|
|
17653888a3 | ||
|
|
ee16616df4 | ||
|
|
ea450005e0 | ||
|
|
4c5ad14f6b | ||
|
|
49dda2aea5 | ||
|
|
a74a004540 | ||
|
|
2b89f6b37b | ||
|
|
c699e35e6b | ||
|
|
e28d90d0aa | ||
|
|
4fc7600865 | ||
|
|
19f62a461b | ||
|
|
7ddc2778c0 | ||
|
|
b96fa2a995 | ||
|
|
fcfb73af6e | ||
|
|
43bebc03b9 | ||
|
|
c5f25cbc7d | ||
|
|
3feb6ce84d | ||
|
|
08d6b45fc5 | ||
|
|
56d0de64dc | ||
|
|
1813e8e8c7 | ||
|
|
e27c764a55 | ||
|
|
e5a2057382 | ||
|
|
8457ff7433 | ||
|
|
888a6f2feb | ||
|
|
b57b64fc36 | ||
|
|
0d239ba1cf | ||
|
|
8927e08217 | ||
|
|
0636069584 | ||
|
|
4d0f73c84e | ||
|
|
74a2478e10 | ||
|
|
acc6f3e887 | ||
|
|
185ab9750a | ||
|
|
48adc050d6 | ||
|
|
b0e318c9db | ||
|
|
f9a6efc00f | ||
|
|
bd4a6775dd | ||
|
|
e3a43d0062 | ||
|
|
0cf281cac0 | ||
|
|
7322f67ae0 | ||
|
|
b927c6d7b4 | ||
|
|
01212cd1f3 | ||
|
|
bf55f94d41 | ||
|
|
f14711d315 | ||
|
|
58e1c28f7c | ||
|
|
922b19c64b | ||
|
|
1d21c3fa90 | ||
|
|
6175fd6764 | ||
|
|
2ceb54f058 | ||
|
|
aaeaa7fefa | ||
|
|
d522247552 | ||
|
|
79dbdab6c9 | ||
|
|
fe40910e3b | ||
|
|
2d1736f13a | ||
|
|
12b4d1c7cd | ||
|
|
a45d2b87c1 | ||
|
|
8484465d09 | ||
|
|
dff65eee20 | ||
|
|
596016456c | ||
|
|
673261c258 | ||
|
|
3c5985a3c0 | ||
|
|
4f3d62520a | ||
|
|
96f8b3d937 | ||
|
|
7ab5a5ade1 | ||
|
|
5cbd0a96ca | ||
|
|
7ccd8c4d4f | ||
|
|
b0fa3fc484 | ||
|
|
af01c4226a | ||
|
|
7a3d85a29a | ||
|
|
fd5ccd8d41 | ||
|
|
a439c5195d | ||
|
|
ba2e997d54 | ||
|
|
0818de85d1 | ||
|
|
457c6098a4 | ||
|
|
60f979fbb5 | ||
|
|
ff53e44fa6 | ||
|
|
1832de47db | ||
|
|
535eb0c465 | ||
|
|
c190634cf3 | ||
|
|
f7559aa040 | ||
|
|
1e0b709c73 | ||
|
|
c0800b7fb3 | ||
|
|
6fcdad2100 | ||
|
|
69d26d5c21 | ||
|
|
94e6b5ecb8 | ||
|
|
95e8bdcd36 |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,5 +1,5 @@
|
||||
*.go linguist-detectable=true
|
||||
*.js linguist-detectable=false
|
||||
# Declare files that will always have LF line endings on checkout.
|
||||
# Git will always convert line endings to LF on checkout. You should use this for files that must keep LF endings, even on Windows.
|
||||
*.go linguist-detectable=true
|
||||
*.js linguist-detectable=false
|
||||
# Declare files that will always have LF line endings on checkout.
|
||||
# Git will always convert line endings to LF on checkout. You should use this for files that must keep LF endings, even on Windows.
|
||||
*.sh text eol=lf
|
||||
143
.github/workflows/build.yml
vendored
143
.github/workflows/build.yml
vendored
@@ -1,6 +1,10 @@
|
||||
name: Build
|
||||
|
||||
on: [ push, pull_request ]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -20,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.16.5'
|
||||
go-version: '1.23'
|
||||
cache-dependency-path: ./go.mod
|
||||
- name: Tests
|
||||
run: |
|
||||
@@ -40,6 +44,12 @@ jobs:
|
||||
cache-dependency-path: ./web/yarn.lock
|
||||
- run: yarn install && CI=false yarn run build
|
||||
working-directory: ./web
|
||||
- name: Upload build artifacts
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-build-${{ github.run_id }}
|
||||
path: ./web/build
|
||||
|
||||
backend:
|
||||
name: Back-end
|
||||
@@ -49,7 +59,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.16.5'
|
||||
go-version: '1.23'
|
||||
cache-dependency-path: ./go.mod
|
||||
- run: go version
|
||||
- name: Build
|
||||
@@ -65,7 +75,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.16.5'
|
||||
go-version: '1.23'
|
||||
cache: false
|
||||
|
||||
# gen a dummy config file
|
||||
@@ -94,11 +104,28 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.16.5'
|
||||
go-version: '1.23'
|
||||
cache-dependency-path: ./go.mod
|
||||
- name: start backend
|
||||
run: nohup go run ./main.go &
|
||||
run: nohup go run ./main.go > /tmp/backend.log 2>&1 &
|
||||
working-directory: ./
|
||||
- name: Wait for backend to be ready
|
||||
run: |
|
||||
echo "Waiting for backend server to start on port 8000..."
|
||||
for i in {1..60}; do
|
||||
if curl -s http://localhost:8000 > /dev/null 2>&1; then
|
||||
echo "Backend is ready!"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 60 ]; then
|
||||
echo "Backend failed to start within 60 seconds"
|
||||
echo "Backend logs:"
|
||||
cat /tmp/backend.log || echo "No backend logs available"
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting... ($i/60)"
|
||||
sleep 1
|
||||
done
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
@@ -114,50 +141,106 @@ jobs:
|
||||
wait-on-timeout: 210
|
||||
working-directory: ./web
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-screenshots
|
||||
path: ./web/cypress/screenshots
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: cypress-videos
|
||||
path: ./web/cypress/videos
|
||||
|
||||
release-and-push:
|
||||
name: Release And Push
|
||||
tag-release:
|
||||
name: Create Tag
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
|
||||
needs: [ frontend, backend, linter, e2e ]
|
||||
outputs:
|
||||
new-release-published: ${{ steps.semantic.outputs.new_release_published }}
|
||||
new-release-version: ${{ steps.semantic.outputs.new_release_version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create Tag with Semantic Release
|
||||
id: semantic
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
github-release:
|
||||
name: GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true'
|
||||
needs: [ tag-release ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Free disk space
|
||||
uses: jlumbroso/free-disk-space@v1.3.1
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Download frontend build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-build-${{ github.run_id }}
|
||||
path: ./web/build
|
||||
|
||||
- name: Prepare Go caches
|
||||
run: |
|
||||
echo "GOMODCACHE=$RUNNER_TEMP/gomod" >> $GITHUB_ENV
|
||||
echo "GOCACHE=$RUNNER_TEMP/gocache" >> $GITHUB_ENV
|
||||
go clean -cache -modcache -testcache -fuzzcache
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker-release:
|
||||
name: Docker Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true'
|
||||
needs: [ tag-release ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: -1
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Fetch Previous version
|
||||
id: get-previous-tag
|
||||
uses: actions-ecosystem/action-get-latest-tag@v1.6.0
|
||||
|
||||
- name: Release
|
||||
run: yarn global add semantic-release@17.4.4 && semantic-release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }}
|
||||
|
||||
- name: Fetch Current version
|
||||
id: get-current-tag
|
||||
uses: actions-ecosystem/action-get-latest-tag@v1.6.0
|
||||
|
||||
- name: Decide Should_Push Or Not
|
||||
id: should_push
|
||||
run: |
|
||||
old_version=${{steps.get-previous-tag.outputs.tag}}
|
||||
new_version=${{steps.get-current-tag.outputs.tag }}
|
||||
new_version=${{ needs.tag-release.outputs.new-release-version }}
|
||||
|
||||
old_array=(${old_version//\./ })
|
||||
new_array=(${new_version//\./ })
|
||||
@@ -196,7 +279,7 @@ jobs:
|
||||
target: STANDARD
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: casbin/casdoor:${{steps.get-current-tag.outputs.tag }},casbin/casdoor:latest
|
||||
tags: casbin/casdoor:${{ needs.tag-release.outputs.new-release-version }},casbin/casdoor:latest
|
||||
|
||||
- name: Push All In One Version to Docker Hub
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -206,7 +289,7 @@ jobs:
|
||||
target: ALLINONE
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: casbin/casdoor-all-in-one:${{steps.get-current-tag.outputs.tag }},casbin/casdoor-all-in-one:latest
|
||||
tags: casbin/casdoor-all-in-one:${{ needs.tag-release.outputs.new-release-version }},casbin/casdoor-all-in-one:latest
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.should_push.outputs.push=='true'
|
||||
@@ -219,8 +302,8 @@ jobs:
|
||||
if: steps.should_push.outputs.push=='true'
|
||||
run: |
|
||||
# Set the appVersion and version of the chart to the current tag
|
||||
sed -i "s/appVersion: .*/appVersion: ${{steps.get-current-tag.outputs.tag }}/g" ./charts/casdoor/Chart.yaml
|
||||
sed -i "s/version: .*/version: ${{steps.get-current-tag.outputs.tag }}/g" ./charts/casdoor/Chart.yaml
|
||||
sed -i "s/appVersion: .*/appVersion: ${{ needs.tag-release.outputs.new-release-version }}/g" ./charts/casdoor/Chart.yaml
|
||||
sed -i "s/version: .*/version: ${{ needs.tag-release.outputs.new-release-version }}/g" ./charts/casdoor/Chart.yaml
|
||||
|
||||
REGISTRY=oci://registry-1.docker.io/casbin
|
||||
cd charts/casdoor
|
||||
@@ -234,6 +317,6 @@ jobs:
|
||||
git config --global user.name "casbin-bot"
|
||||
git config --global user.email "bot@casbin.org"
|
||||
git add Chart.yaml index.yaml
|
||||
git commit -m "chore(helm): bump helm charts appVersion to ${{steps.get-current-tag.outputs.tag }}"
|
||||
git tag ${{steps.get-current-tag.outputs.tag }}
|
||||
git commit -m "chore(helm): bump helm charts appVersion to ${{ needs.tag-release.outputs.new-release-version }}"
|
||||
git tag ${{ needs.tag-release.outputs.new-release-version }}
|
||||
git push origin HEAD:master --follow-tags
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
||||
*.so
|
||||
*.dylib
|
||||
*.swp
|
||||
server_*
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
55
.goreleaser.yaml
Normal file
55
.goreleaser.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||
# Make sure to check the documentation at https://goreleaser.com
|
||||
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/need to use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
# you may remove this if you don't need go generate
|
||||
#- go generate ./...
|
||||
- go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go ./util/variable.go
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- src: 'web/build'
|
||||
dst: './web/build'
|
||||
- src: 'conf/app.conf'
|
||||
dst: './conf/app.conf'
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
34
Dockerfile
34
Dockerfile
@@ -1,14 +1,26 @@
|
||||
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
|
||||
WORKDIR /web
|
||||
|
||||
# Copy only dependency files first for better caching
|
||||
COPY ./web/package.json ./web/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000
|
||||
|
||||
# Copy source files and build
|
||||
COPY ./web .
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.20.12 AS BACK
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23.12 AS BACK
|
||||
WORKDIR /go/src/casdoor
|
||||
|
||||
# Copy only go.mod and go.sum first for dependency caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY . .
|
||||
|
||||
RUN go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go ./util/variable.go
|
||||
RUN ./build.sh
|
||||
RUN go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go > version_info.txt
|
||||
|
||||
FROM alpine:latest AS STANDARD
|
||||
LABEL MAINTAINER="https://casdoor.org/"
|
||||
@@ -34,35 +46,25 @@ WORKDIR /
|
||||
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/server_${BUILDX_ARCH} ./server
|
||||
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/swagger ./swagger
|
||||
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/conf/app.conf ./conf/app.conf
|
||||
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt
|
||||
COPY --from=FRONT --chown=$USER:$USER /web/build ./web/build
|
||||
|
||||
ENTRYPOINT ["/server"]
|
||||
|
||||
|
||||
FROM debian:latest AS db
|
||||
RUN apt update \
|
||||
&& apt install -y \
|
||||
mariadb-server \
|
||||
mariadb-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
FROM db AS ALLINONE
|
||||
FROM debian:latest AS ALLINONE
|
||||
LABEL MAINTAINER="https://casdoor.org/"
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y ca-certificates && update-ca-certificates
|
||||
RUN apt install -y ca-certificates lsof && update-ca-certificates
|
||||
|
||||
WORKDIR /
|
||||
COPY --from=BACK /go/src/casdoor/server_${BUILDX_ARCH} ./server
|
||||
COPY --from=BACK /go/src/casdoor/swagger ./swagger
|
||||
COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf
|
||||
COPY --from=BACK /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt
|
||||
COPY --from=FRONT /web/build ./web/build
|
||||
|
||||
ENTRYPOINT ["/bin/bash"]
|
||||
|
||||
190
README.md
190
README.md
@@ -1,102 +1,88 @@
|
||||
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
|
||||
<h3 align="center">An open-source UI-first Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA and RADIUS</h3>
|
||||
<p align="center">
|
||||
<a href="#badge">
|
||||
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/casbin/casdoor">
|
||||
<img alt="docker pull casbin/casdoor" src="https://img.shields.io/docker/pulls/casbin/casdoor.svg">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/actions/workflows/build.yml">
|
||||
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casdoor/casdoor/workflows/Build/badge.svg?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/releases/latest">
|
||||
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casdoor/casdoor.svg">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/casbin/casdoor">
|
||||
<img alt="Docker Image Version (latest semver)" src="https://img.shields.io/badge/Docker%20Hub-latest-brightgreen">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://goreportcard.com/report/github.com/casdoor/casdoor">
|
||||
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/casdoor/casdoor?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/casdoor/casdoor?style=flat-square" alt="license">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/issues">
|
||||
<img alt="GitHub issues" src="https://img.shields.io/github/issues/casdoor/casdoor?style=flat-square">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img alt="GitHub stars" src="https://img.shields.io/github/stars/casdoor/casdoor?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/network">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/casdoor/casdoor?style=flat-square">
|
||||
</a>
|
||||
<a href="https://crowdin.com/project/casdoor-site">
|
||||
<img alt="Crowdin" src="https://badges.crowdin.net/casdoor-site/localized.svg">
|
||||
</a>
|
||||
<a href="https://discord.gg/5rPsrAzK7S">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1022748306096537660?style=flat-square&logo=discord&label=discord&color=5865F2">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<sup>Sponsored by</sup>
|
||||
<br>
|
||||
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.casbin.org/img/stytch-white.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://cdn.casbin.org/img/stytch-charcoal.png">
|
||||
<img src="https://cdn.casbin.org/img/stytch-charcoal.png" width="275">
|
||||
</picture>
|
||||
</a><br/>
|
||||
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin"><b>Build auth with fraud prevention, faster.</b><br/> Try Stytch for API-first authentication, user & org management, multi-tenant SSO, MFA, device fingerprinting, and more.</a>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
## Online demo
|
||||
|
||||
- Read-only site: https://door.casdoor.com (any modification operation will fail)
|
||||
- Writable site: https://demo.casdoor.com (original data will be restored for every 5 minutes)
|
||||
|
||||
## Documentation
|
||||
|
||||
https://casdoor.org
|
||||
|
||||
## Install
|
||||
|
||||
- By source code: https://casdoor.org/docs/basic/server-installation
|
||||
- By Docker: https://casdoor.org/docs/basic/try-with-docker
|
||||
- By Kubernetes Helm: https://casdoor.org/docs/basic/try-with-helm
|
||||
|
||||
## How to connect to Casdoor?
|
||||
|
||||
https://casdoor.org/docs/how-to-connect/overview
|
||||
|
||||
## Casdoor Public API
|
||||
|
||||
- Docs: https://casdoor.org/docs/basic/public-api
|
||||
- Swagger: https://door.casdoor.com/swagger
|
||||
|
||||
## Integrations
|
||||
|
||||
https://casdoor.org/docs/category/integrations
|
||||
|
||||
## How to contact?
|
||||
|
||||
- Discord: https://discord.gg/5rPsrAzK7S
|
||||
- Contact: https://casdoor.org/help
|
||||
|
||||
## Contribute
|
||||
|
||||
For casdoor, if you have any questions, you can give Issues, or you can also directly start Pull Requests(but we recommend giving issues first to communicate with the community).
|
||||
|
||||
### I18n translation
|
||||
|
||||
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the `web/` directory, please remember to add what you have added to the `web/src/locales/en/data.json` file.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE)
|
||||
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
|
||||
<h3 align="center">An open-source UI-first Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA and RADIUS</h3>
|
||||
<p align="center">
|
||||
<a href="#badge">
|
||||
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/casbin/casdoor">
|
||||
<img alt="docker pull casbin/casdoor" src="https://img.shields.io/docker/pulls/casbin/casdoor.svg">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/actions/workflows/build.yml">
|
||||
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casdoor/casdoor/workflows/Build/badge.svg?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/releases/latest">
|
||||
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casdoor/casdoor.svg">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/casbin/casdoor">
|
||||
<img alt="Docker Image Version (latest semver)" src="https://img.shields.io/badge/Docker%20Hub-latest-brightgreen">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://goreportcard.com/report/github.com/casdoor/casdoor">
|
||||
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/casdoor/casdoor?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/casdoor/casdoor?style=flat-square" alt="license">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/issues">
|
||||
<img alt="GitHub issues" src="https://img.shields.io/github/issues/casdoor/casdoor?style=flat-square">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img alt="GitHub stars" src="https://img.shields.io/github/stars/casdoor/casdoor?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/casdoor/casdoor/network">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/casdoor/casdoor?style=flat-square">
|
||||
</a>
|
||||
<a href="https://crowdin.com/project/casdoor-site">
|
||||
<img alt="Crowdin" src="https://badges.crowdin.net/casdoor-site/localized.svg">
|
||||
</a>
|
||||
<a href="https://discord.gg/5rPsrAzK7S">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1022748306096537660?style=flat-square&logo=discord&label=discord&color=5865F2">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Online demo
|
||||
|
||||
- Read-only site: https://door.casdoor.com (any modification operation will fail)
|
||||
- Writable site: https://demo.casdoor.com (original data will be restored for every 5 minutes)
|
||||
|
||||
## Documentation
|
||||
|
||||
https://casdoor.org
|
||||
|
||||
## Install
|
||||
|
||||
- By source code: https://casdoor.org/docs/basic/server-installation
|
||||
- By Docker: https://casdoor.org/docs/basic/try-with-docker
|
||||
- By Kubernetes Helm: https://casdoor.org/docs/basic/try-with-helm
|
||||
|
||||
## How to connect to Casdoor?
|
||||
|
||||
https://casdoor.org/docs/how-to-connect/overview
|
||||
|
||||
## Casdoor Public API
|
||||
|
||||
- Docs: https://casdoor.org/docs/basic/public-api
|
||||
- Swagger: https://door.casdoor.com/swagger
|
||||
|
||||
## Integrations
|
||||
|
||||
https://casdoor.org/docs/category/integrations
|
||||
|
||||
## How to contact?
|
||||
|
||||
- Discord: https://discord.gg/5rPsrAzK7S
|
||||
- Contact: https://casdoor.org/help
|
||||
|
||||
## Contribute
|
||||
|
||||
For casdoor, if you have any questions, you can give Issues, or you can also directly start Pull Requests(but we recommend giving issues first to communicate with the community).
|
||||
|
||||
### I18n translation
|
||||
|
||||
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the `web/` directory, please remember to add what you have added to the `web/src/locales/en/data.json` file.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE)
|
||||
|
||||
@@ -46,7 +46,10 @@ p, *, *, POST, /api/login, *, *
|
||||
p, *, *, GET, /api/get-app-login, *, *
|
||||
p, *, *, POST, /api/logout, *, *
|
||||
p, *, *, GET, /api/logout, *, *
|
||||
p, *, *, POST, /api/sso-logout, *, *
|
||||
p, *, *, GET, /api/sso-logout, *, *
|
||||
p, *, *, POST, /api/callback, *, *
|
||||
p, *, *, POST, /api/device-auth, *, *
|
||||
p, *, *, GET, /api/get-account, *, *
|
||||
p, *, *, GET, /api/userinfo, *, *
|
||||
p, *, *, GET, /api/user, *, *
|
||||
@@ -56,18 +59,25 @@ p, *, *, GET, /api/get-qrcode, *, *
|
||||
p, *, *, GET, /api/get-webhook-event, *, *
|
||||
p, *, *, GET, /api/get-captcha-status, *, *
|
||||
p, *, *, *, /api/login/oauth, *, *
|
||||
p, *, *, POST, /api/oauth/register, *, *
|
||||
p, *, *, GET, /api/get-application, *, *
|
||||
p, *, *, GET, /api/get-organization-applications, *, *
|
||||
p, *, *, GET, /api/get-user, *, *
|
||||
p, *, *, GET, /api/get-user-application, *, *
|
||||
p, *, *, POST, /api/upload-users, *, *
|
||||
p, *, *, GET, /api/get-resources, *, *
|
||||
p, *, *, GET, /api/get-records, *, *
|
||||
p, *, *, GET, /api/get-product, *, *
|
||||
p, *, *, POST, /api/buy-product, *, *
|
||||
p, *, *, GET, /api/get-order, *, *
|
||||
p, *, *, GET, /api/get-orders, *, *
|
||||
p, *, *, GET, /api/get-user-orders, *, *
|
||||
p, *, *, GET, /api/get-payment, *, *
|
||||
p, *, *, POST, /api/update-payment, *, *
|
||||
p, *, *, POST, /api/invoice-payment, *, *
|
||||
p, *, *, POST, /api/notify-payment, *, *
|
||||
p, *, *, POST, /api/place-order, *, *
|
||||
p, *, *, POST, /api/cancel-order, *, *
|
||||
p, *, *, POST, /api/pay-order, *, *
|
||||
p, *, *, POST, /api/unlink, *, *
|
||||
p, *, *, POST, /api/set-password, *, *
|
||||
p, *, *, POST, /api/send-verification-code, *, *
|
||||
@@ -79,6 +89,9 @@ p, *, *, POST, /api/upload-resource, *, *
|
||||
p, *, *, GET, /.well-known/openid-configuration, *, *
|
||||
p, *, *, GET, /.well-known/webfinger, *, *
|
||||
p, *, *, *, /.well-known/jwks, *, *
|
||||
p, *, *, GET, /.well-known/:application/openid-configuration, *, *
|
||||
p, *, *, GET, /.well-known/:application/webfinger, *, *
|
||||
p, *, *, *, /.well-known/:application/jwks, *, *
|
||||
p, *, *, GET, /api/get-saml-login, *, *
|
||||
p, *, *, POST, /api/acs, *, *
|
||||
p, *, *, GET, /api/saml/metadata, *, *
|
||||
@@ -93,11 +106,15 @@ p, *, *, *, /api/metrics, *, *
|
||||
p, *, *, GET, /api/get-pricing, *, *
|
||||
p, *, *, GET, /api/get-plan, *, *
|
||||
p, *, *, GET, /api/get-subscription, *, *
|
||||
p, *, *, GET, /api/get-transactions, *, *
|
||||
p, *, *, GET, /api/get-transaction, *, *
|
||||
p, *, *, GET, /api/get-provider, *, *
|
||||
p, *, *, GET, /api/get-organization-names, *, *
|
||||
p, *, *, GET, /api/get-all-objects, *, *
|
||||
p, *, *, GET, /api/get-all-actions, *, *
|
||||
p, *, *, GET, /api/get-all-roles, *, *
|
||||
p, *, *, GET, /api/run-casbin-command, *, *
|
||||
p, *, *, POST, /api/refresh-engines, *, *
|
||||
p, *, *, GET, /api/get-invitation-info, *, *
|
||||
p, *, *, GET, /api/faceid-signin-begin, *, *
|
||||
`
|
||||
@@ -119,7 +136,15 @@ p, *, *, GET, /api/faceid-signin-begin, *, *
|
||||
}
|
||||
}
|
||||
|
||||
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
|
||||
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string, extraInfo map[string]interface{}) bool {
|
||||
if urlPath == "/api/mcp" {
|
||||
if detailPath, ok := extraInfo["detailPathUrl"].(string); ok {
|
||||
if detailPath == "initialize" || detailPath == "notifications/initialized" || detailPath == "ping" || detailPath == "tools/list" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if conf.IsDemoMode() {
|
||||
if !isAllowedInDemoMode(subOwner, subName, method, urlPath, objOwner, objName) {
|
||||
return false
|
||||
@@ -140,6 +165,10 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
|
||||
return false
|
||||
}
|
||||
|
||||
if user.IsGlobalAdmin() {
|
||||
return true
|
||||
}
|
||||
|
||||
if user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
|
||||
return true
|
||||
}
|
||||
@@ -150,12 +179,19 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !res {
|
||||
res, err = object.CheckApiPermission(util.GetId(subOwner, subName), objOwner, urlPath, method)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
|
||||
if method == "POST" {
|
||||
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" {
|
||||
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/sso-logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" || urlPath == "/api/refresh-engines" {
|
||||
return true
|
||||
} else if urlPath == "/api/update-user" {
|
||||
// Allow ordinary users to update their own information
|
||||
@@ -163,7 +199,7 @@ func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else if urlPath == "/api/upload-resource" {
|
||||
} else if urlPath == "/api/upload-resource" || urlPath == "/api/add-transaction" {
|
||||
if subOwner == "app" && subName == "app-casibase" {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -15,32 +15,51 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
openapiutil "github.com/alibabacloud-go/openapi-util/service"
|
||||
teaUtil "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
const AliyunCaptchaVerifyUrl = "http://afs.aliyuncs.com"
|
||||
const AliyunCaptchaVerifyUrl = "captcha.cn-shanghai.aliyuncs.com"
|
||||
|
||||
type captchaSuccessResponse struct {
|
||||
Code int `json:"Code"`
|
||||
Msg string `json:"Msg"`
|
||||
type VerifyCaptchaRequest struct {
|
||||
CaptchaVerifyParam *string `json:"CaptchaVerifyParam,omitempty" xml:"CaptchaVerifyParam,omitempty"`
|
||||
SceneId *string `json:"SceneId,omitempty" xml:"SceneId,omitempty"`
|
||||
}
|
||||
|
||||
type captchaFailResponse struct {
|
||||
Code string `json:"Code"`
|
||||
Message string `json:"Message"`
|
||||
type VerifyCaptchaResponseBodyResult struct {
|
||||
VerifyResult *bool `json:"VerifyResult,omitempty" xml:"VerifyResult,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyCaptchaResponseBody struct {
|
||||
Code *string `json:"Code,omitempty" xml:"Code,omitempty"`
|
||||
Message *string `json:"Message,omitempty" xml:"Message,omitempty"`
|
||||
// Id of the request
|
||||
RequestId *string `json:"RequestId,omitempty" xml:"RequestId,omitempty"`
|
||||
Result *VerifyCaptchaResponseBodyResult `json:"Result,omitempty" xml:"Result,omitempty" type:"Struct"`
|
||||
Success *bool `json:"Success,omitempty" xml:"Success,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyIntelligentCaptchaResponseBodyResult struct {
|
||||
VerifyCode *string `json:"VerifyCode,omitempty" xml:"VerifyCode,omitempty"`
|
||||
VerifyResult *bool `json:"VerifyResult,omitempty" xml:"VerifyResult,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyIntelligentCaptchaResponseBody struct {
|
||||
Code *string `json:"Code,omitempty" xml:"Code,omitempty"`
|
||||
Message *string `json:"Message,omitempty" xml:"Message,omitempty"`
|
||||
// Id of the request
|
||||
RequestId *string `json:"RequestId,omitempty" xml:"RequestId,omitempty"`
|
||||
Result *VerifyIntelligentCaptchaResponseBodyResult `json:"Result,omitempty" xml:"Result,omitempty" type:"Struct"`
|
||||
Success *bool `json:"Success,omitempty" xml:"Success,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyIntelligentCaptchaResponse struct {
|
||||
Headers map[string]*string `json:"headers,omitempty" xml:"headers,omitempty" require:"true"`
|
||||
StatusCode *int32 `json:"statusCode,omitempty" xml:"statusCode,omitempty" require:"true"`
|
||||
Body *VerifyIntelligentCaptchaResponseBody `json:"body,omitempty" xml:"body,omitempty" require:"true"`
|
||||
}
|
||||
type AliyunCaptchaProvider struct{}
|
||||
|
||||
func NewAliyunCaptchaProvider() *AliyunCaptchaProvider {
|
||||
@@ -48,68 +67,69 @@ func NewAliyunCaptchaProvider() *AliyunCaptchaProvider {
|
||||
return captcha
|
||||
}
|
||||
|
||||
func contentEscape(str string) string {
|
||||
str = strings.Replace(str, " ", "%20", -1)
|
||||
str = url.QueryEscape(str)
|
||||
return str
|
||||
}
|
||||
func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
|
||||
config := &openapi.Config{}
|
||||
|
||||
func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
|
||||
pathData, err := url.ParseQuery(token)
|
||||
config.Endpoint = tea.String(AliyunCaptchaVerifyUrl)
|
||||
config.ConnectTimeout = tea.Int(5000)
|
||||
config.ReadTimeout = tea.Int(5000)
|
||||
config.AccessKeyId = tea.String(clientId)
|
||||
config.AccessKeySecret = tea.String(clientSecret)
|
||||
|
||||
client := new(openapi.Client)
|
||||
err := client.Init(config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
pathData["Action"] = []string{"AuthenticateSig"}
|
||||
pathData["Format"] = []string{"json"}
|
||||
pathData["SignatureMethod"] = []string{"HMAC-SHA1"}
|
||||
pathData["SignatureNonce"] = []string{strconv.FormatInt(time.Now().UnixNano(), 10)}
|
||||
pathData["SignatureVersion"] = []string{"1.0"}
|
||||
pathData["Timestamp"] = []string{time.Now().UTC().Format("2006-01-02T15:04:05Z")}
|
||||
pathData["Version"] = []string{"2018-01-12"}
|
||||
request := VerifyCaptchaRequest{CaptchaVerifyParam: tea.String(token), SceneId: tea.String(clientId2)}
|
||||
|
||||
var keys []string
|
||||
for k := range pathData {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
sortQuery := ""
|
||||
for _, k := range keys {
|
||||
sortQuery += k + "=" + contentEscape(pathData[k][0]) + "&"
|
||||
}
|
||||
sortQuery = strings.TrimSuffix(sortQuery, "&")
|
||||
|
||||
stringToSign := fmt.Sprintf("GET&%s&%s", url.QueryEscape("/"), url.QueryEscape(sortQuery))
|
||||
|
||||
signature := util.GetHmacSha1(clientSecret+"&", stringToSign)
|
||||
|
||||
resp, err := http.Get(fmt.Sprintf("%s?%s&Signature=%s", AliyunCaptchaVerifyUrl, sortQuery, url.QueryEscape(signature)))
|
||||
err = teaUtil.ValidateModel(&request)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
runtime := &teaUtil.RuntimeOptions{}
|
||||
|
||||
body := map[string]interface{}{}
|
||||
if !tea.BoolValue(teaUtil.IsUnset(request.CaptchaVerifyParam)) {
|
||||
body["CaptchaVerifyParam"] = request.CaptchaVerifyParam
|
||||
}
|
||||
|
||||
if !tea.BoolValue(teaUtil.IsUnset(request.SceneId)) {
|
||||
body["SceneId"] = request.SceneId
|
||||
}
|
||||
|
||||
req := &openapi.OpenApiRequest{
|
||||
Body: openapiutil.ParseToMap(body),
|
||||
}
|
||||
params := &openapi.Params{
|
||||
Action: tea.String("VerifyIntelligentCaptcha"),
|
||||
Version: tea.String("2023-03-05"),
|
||||
Protocol: tea.String("HTTPS"),
|
||||
Pathname: tea.String("/"),
|
||||
Method: tea.String("POST"),
|
||||
AuthType: tea.String("AK"),
|
||||
Style: tea.String("RPC"),
|
||||
ReqBodyType: tea.String("formData"),
|
||||
BodyType: tea.String("json"),
|
||||
}
|
||||
|
||||
res := &VerifyIntelligentCaptchaResponse{}
|
||||
|
||||
resBody, err := client.CallApi(params, req, runtime)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return handleCaptchaResponse(body)
|
||||
}
|
||||
|
||||
func handleCaptchaResponse(body []byte) (bool, error) {
|
||||
captchaResp := &captchaSuccessResponse{}
|
||||
err := json.Unmarshal(body, captchaResp)
|
||||
err = tea.Convert(resBody, &res)
|
||||
if err != nil {
|
||||
captchaFailResp := &captchaFailResponse{}
|
||||
err = json.Unmarshal(body, captchaFailResp)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, errors.New(captchaFailResp.Message)
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
if res.Body.Result.VerifyResult != nil && *res.Body.Result.VerifyResult {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ func NewDefaultCaptchaProvider() *DefaultCaptchaProvider {
|
||||
return captcha
|
||||
}
|
||||
|
||||
func (captcha *DefaultCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
|
||||
func (captcha *DefaultCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
|
||||
return object.VerifyCaptcha(clientSecret, token), nil
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func NewGEETESTCaptchaProvider() *GEETESTCaptchaProvider {
|
||||
return captcha
|
||||
}
|
||||
|
||||
func (captcha *GEETESTCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
|
||||
func (captcha *GEETESTCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
|
||||
pathData, err := url.ParseQuery(token)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -32,7 +32,7 @@ func NewHCaptchaProvider() *HCaptchaProvider {
|
||||
return captcha
|
||||
}
|
||||
|
||||
func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
|
||||
func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
|
||||
reqData := url.Values{
|
||||
"secret": {clientSecret},
|
||||
"response": {token},
|
||||
|
||||
@@ -17,7 +17,7 @@ package captcha
|
||||
import "fmt"
|
||||
|
||||
type CaptchaProvider interface {
|
||||
VerifyCaptcha(token, clientSecret string) (bool, error)
|
||||
VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error)
|
||||
}
|
||||
|
||||
func GetCaptchaProvider(captchaType string) CaptchaProvider {
|
||||
@@ -43,11 +43,11 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
|
||||
return nil
|
||||
}
|
||||
|
||||
func VerifyCaptchaByCaptchaType(captchaType, token, clientSecret string) (bool, error) {
|
||||
func VerifyCaptchaByCaptchaType(captchaType, token, clientId, clientSecret, clientId2 string) (bool, error) {
|
||||
provider := GetCaptchaProvider(captchaType)
|
||||
if provider == nil {
|
||||
return false, fmt.Errorf("invalid captcha provider: %s", captchaType)
|
||||
}
|
||||
|
||||
return provider.VerifyCaptcha(token, clientSecret)
|
||||
return provider.VerifyCaptcha(token, clientId, clientSecret, clientId2)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func NewReCaptchaProvider() *ReCaptchaProvider {
|
||||
return captcha
|
||||
}
|
||||
|
||||
func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
|
||||
func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
|
||||
reqData := url.Values{
|
||||
"secret": {clientSecret},
|
||||
"response": {token},
|
||||
|
||||
@@ -32,7 +32,7 @@ func NewCloudflareTurnstileProvider() *CloudflareTurnstileProvider {
|
||||
return captcha
|
||||
}
|
||||
|
||||
func (captcha *CloudflareTurnstileProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
|
||||
func (captcha *CloudflareTurnstileProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
|
||||
reqData := url.Values{
|
||||
"secret": {clientSecret},
|
||||
"response": {token},
|
||||
|
||||
@@ -25,9 +25,13 @@ enableErrorMask = false
|
||||
enableGzip = true
|
||||
inactiveTimeoutMinutes =
|
||||
ldapServerPort = 389
|
||||
ldapsCertId = ""
|
||||
ldapsServerPort = 636
|
||||
radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
initDataFile = "./init_data.json"
|
||||
frontendBaseDir = "../casdoor"
|
||||
frontendBaseDir = "../cc_0"
|
||||
15
conf/conf.go
15
conf/conf.go
@@ -21,7 +21,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -29,7 +29,7 @@ func init() {
|
||||
presetConfigItems := []string{"httpport", "appname"}
|
||||
for _, key := range presetConfigItems {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
err := beego.AppConfig.Set(key, value)
|
||||
err := web.AppConfig.Set(key, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -42,12 +42,13 @@ func GetConfigString(key string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
res := beego.AppConfig.String(key)
|
||||
res, _ := web.AppConfig.String(key)
|
||||
if res == "" {
|
||||
if key == "staticBaseUrl" {
|
||||
res = "https://cdn.casbin.org"
|
||||
} else if key == "logConfig" {
|
||||
res = fmt.Sprintf("{\"filename\": \"logs/%s.log\", \"maxdays\":99999, \"perm\":\"0770\"}", beego.AppConfig.String("appname"))
|
||||
appname, _ := web.AppConfig.String("appname")
|
||||
res = fmt.Sprintf("{\"filename\": \"logs/%s.log\", \"maxdays\":99999, \"perm\":\"0770\"}", appname)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +67,11 @@ func GetConfigBool(key string) bool {
|
||||
func GetConfigInt64(key string) (int64, error) {
|
||||
value := GetConfigString(key)
|
||||
num, err := strconv.ParseInt(value, 10, 64)
|
||||
return num, err
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("GetConfigInt64(%s) error, %s", key, err.Error())
|
||||
}
|
||||
|
||||
return num, nil
|
||||
}
|
||||
|
||||
func GetConfigDataSourceName() string {
|
||||
|
||||
@@ -17,7 +17,7 @@ package conf
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type Quota struct {
|
||||
@@ -34,7 +34,7 @@ func init() {
|
||||
}
|
||||
|
||||
func initQuota() {
|
||||
res := beego.AppConfig.String("quota")
|
||||
res, _ := web.AppConfig.String("quota")
|
||||
if res != "" {
|
||||
err := json.Unmarshal([]byte(res), quota)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestGetConfString(t *testing.T) {
|
||||
os.Setenv("appname", "casbin")
|
||||
os.Setenv("key", "value")
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
|
||||
for _, scenery := range scenarios {
|
||||
@@ -62,7 +62,7 @@ func TestGetConfInt(t *testing.T) {
|
||||
// do some set up job
|
||||
os.Setenv("httpport", "8001")
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
|
||||
for _, scenery := range scenarios {
|
||||
@@ -83,7 +83,7 @@ func TestGetConfBool(t *testing.T) {
|
||||
{"Should be return false", "copyrequestbody", true},
|
||||
}
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
for _, scenery := range scenarios {
|
||||
t.Run(scenery.description, func(t *testing.T) {
|
||||
@@ -102,7 +102,7 @@ func TestGetConfigQuota(t *testing.T) {
|
||||
{"default", &Quota{-1, -1, -1, -1}},
|
||||
}
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
for _, scenery := range scenarios {
|
||||
quota := GetConfigQuota()
|
||||
@@ -115,10 +115,10 @@ func TestGetConfigLogs(t *testing.T) {
|
||||
description string
|
||||
expected string
|
||||
}{
|
||||
{"Default log config", `{"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}`},
|
||||
{"Default log config", `{"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}`},
|
||||
}
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
for _, scenery := range scenarios {
|
||||
quota := GetConfigString("logConfig")
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -32,6 +33,7 @@ const (
|
||||
ResponseTypeIdToken = "id_token"
|
||||
ResponseTypeSaml = "saml"
|
||||
ResponseTypeCas = "cas"
|
||||
ResponseTypeDevice = "device"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
@@ -41,6 +43,7 @@ type Response struct {
|
||||
Name string `json:"name"`
|
||||
Data interface{} `json:"data"`
|
||||
Data2 interface{} `json:"data2"`
|
||||
Data3 interface{} `json:"data3"`
|
||||
}
|
||||
|
||||
type Captcha struct {
|
||||
@@ -78,11 +81,6 @@ type LaravelResponse struct {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /signup [post]
|
||||
func (c *ApiController) Signup() {
|
||||
if c.GetSessionUsername() != "" {
|
||||
c.ResponseError(c.T("account:Please sign out first"), c.GetSessionUsername())
|
||||
return
|
||||
}
|
||||
|
||||
var authForm form.AuthForm
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)
|
||||
if err != nil {
|
||||
@@ -139,6 +137,8 @@ func (c *ApiController) Signup() {
|
||||
invitationName = invitation.Name
|
||||
}
|
||||
|
||||
userEmailVerified := false
|
||||
|
||||
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
|
||||
var checkResult *object.VerifyResult
|
||||
checkResult, err = object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
|
||||
@@ -150,6 +150,8 @@ func (c *ApiController) Signup() {
|
||||
c.ResponseError(checkResult.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
userEmailVerified = true
|
||||
}
|
||||
|
||||
var checkPhone string
|
||||
@@ -191,7 +193,7 @@ func (c *ApiController) Signup() {
|
||||
|
||||
userType := "normal-user"
|
||||
if authForm.Plan != "" && authForm.Pricing != "" {
|
||||
err = object.CheckPricingAndPlan(authForm.Organization, authForm.Pricing, authForm.Plan)
|
||||
err = object.CheckPricingAndPlan(authForm.Organization, authForm.Pricing, authForm.Plan, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -212,7 +214,7 @@ func (c *ApiController) Signup() {
|
||||
Tag: authForm.Tag,
|
||||
Education: authForm.Education,
|
||||
Avatar: organization.DefaultAvatar,
|
||||
Email: authForm.Email,
|
||||
Email: strings.ToLower(authForm.Email),
|
||||
Phone: authForm.Phone,
|
||||
CountryCode: authForm.CountryCode,
|
||||
Address: []string{},
|
||||
@@ -228,6 +230,9 @@ func (c *ApiController) Signup() {
|
||||
Karma: 0,
|
||||
Invitation: invitationName,
|
||||
InvitationCode: authForm.InvitationCode,
|
||||
EmailVerified: userEmailVerified,
|
||||
RegisterType: "Application Signup",
|
||||
RegisterSource: fmt.Sprintf("%s/%s", authForm.Organization, application.Name),
|
||||
}
|
||||
|
||||
if len(organization.Tags) > 0 {
|
||||
@@ -249,7 +254,11 @@ func (c *ApiController) Signup() {
|
||||
user.Groups = []string{invitation.SignupGroup}
|
||||
}
|
||||
|
||||
affected, err := object.AddUser(user)
|
||||
if application.DefaultGroup != "" && user.Groups == nil {
|
||||
user.Groups = []string{application.DefaultGroup}
|
||||
}
|
||||
|
||||
affected, err := object.AddUser(user, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -275,9 +284,10 @@ func (c *ApiController) Signup() {
|
||||
}
|
||||
}
|
||||
|
||||
if application.HasPromptPage() && user.Type == "normal-user" {
|
||||
// The prompt page needs the user to be signed in
|
||||
if user.Type == "normal-user" {
|
||||
c.SetSessionUsername(user.GetId())
|
||||
} else if user.Type == "paid-user" {
|
||||
c.SetSession("paidUsername", user.GetId())
|
||||
}
|
||||
|
||||
if authForm.Email != "" {
|
||||
@@ -302,6 +312,29 @@ func (c *ApiController) Signup() {
|
||||
userId := user.GetId()
|
||||
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
|
||||
|
||||
// Check if this is an OAuth flow and automatically generate code
|
||||
clientId := c.Ctx.Input.Query("clientId")
|
||||
responseType := c.Ctx.Input.Query("responseType")
|
||||
redirectUri := c.Ctx.Input.Query("redirectUri")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
state := c.Ctx.Input.Query("state")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
codeChallenge := c.Ctx.Input.Query("code_challenge")
|
||||
|
||||
// If OAuth parameters are present, generate OAuth code and return it
|
||||
if clientId != "" && responseType == ResponseTypeCode {
|
||||
code, err := object.GetOAuthCode(userId, clientId, "", "password", responseType, redirectUri, scope, state, nonce, codeChallenge, "", c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp := codeToResponse(code)
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(userId)
|
||||
}
|
||||
|
||||
@@ -316,9 +349,9 @@ func (c *ApiController) Signup() {
|
||||
// @router /logout [post]
|
||||
func (c *ApiController) Logout() {
|
||||
// https://openid.net/specs/openid-connect-rpinitiated-1_0-final.html
|
||||
accessToken := c.Input().Get("id_token_hint")
|
||||
redirectUri := c.Input().Get("post_logout_redirect_uri")
|
||||
state := c.Input().Get("state")
|
||||
accessToken := c.GetString("id_token_hint")
|
||||
redirectUri := c.GetString("post_logout_redirect_uri")
|
||||
state := c.GetString("state")
|
||||
|
||||
user := c.GetSessionUsername()
|
||||
|
||||
@@ -331,8 +364,12 @@ func (c *ApiController) Logout() {
|
||||
|
||||
c.ClearUserSession()
|
||||
c.ClearTokenSession()
|
||||
owner, username := util.GetOwnerAndNameFromId(user)
|
||||
_, err := object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID())
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID(context.Background()))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -379,9 +416,13 @@ func (c *ApiController) Logout() {
|
||||
c.ClearUserSession()
|
||||
c.ClearTokenSession()
|
||||
// TODO https://github.com/casdoor/casdoor/pull/1494#discussion_r1095675265
|
||||
owner, username := util.GetOwnerAndNameFromId(user)
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID())
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID(context.Background()))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -411,6 +452,108 @@ func (c *ApiController) Logout() {
|
||||
}
|
||||
}
|
||||
|
||||
// SsoLogout
|
||||
// @Title SsoLogout
|
||||
// @Tag Login API
|
||||
// @Description logout the current user from all applications or current session only
|
||||
// @Param logoutAll query string false "Whether to logout from all sessions. Accepted values: 'true', '1', or empty (default: true). Any other value means false."
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /sso-logout [get,post]
|
||||
func (c *ApiController) SsoLogout() {
|
||||
user := c.GetSessionUsername()
|
||||
|
||||
if user == "" {
|
||||
c.ResponseOk()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user wants to logout from all sessions or just current session
|
||||
// Default is true for backward compatibility
|
||||
logoutAll := c.Ctx.Input.Query("logoutAll")
|
||||
logoutAllSessions := logoutAll == "" || logoutAll == "true" || logoutAll == "1"
|
||||
|
||||
c.ClearUserSession()
|
||||
c.ClearTokenSession()
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
currentSessionId := c.Ctx.Input.CruSession.SessionID(context.Background())
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), currentSessionId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var tokens []*object.Token
|
||||
var sessionIds []string
|
||||
|
||||
// Get tokens for notification (needed for both session-level and full logout)
|
||||
// This enables subsystems to identify and invalidate corresponding access tokens
|
||||
// Note: Tokens must be retrieved BEFORE expiration to include their hashes in the notification
|
||||
tokens, err = object.GetTokensByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if logoutAllSessions {
|
||||
// Logout from all sessions: expire all tokens and delete all sessions
|
||||
_, err = object.ExpireTokenByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sessions, err := object.GetUserSessions(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
sessionIds = append(sessionIds, session.SessionId...)
|
||||
}
|
||||
object.DeleteBeegoSession(sessionIds)
|
||||
|
||||
_, err = object.DeleteAllUserSessions(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out from all applications", user)
|
||||
} else {
|
||||
// Logout from current session only
|
||||
sessionIds = []string{currentSessionId}
|
||||
|
||||
// Only delete the current session's Beego session
|
||||
object.DeleteBeegoSession(sessionIds)
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out from current session", user)
|
||||
}
|
||||
|
||||
// Send SSO logout notifications to all notification providers in the user's signup application
|
||||
// Now includes session-level information for targeted logout
|
||||
userObj, err := object.GetUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if userObj != nil {
|
||||
err = object.SendSsoLogoutNotifications(userObj, sessionIds, tokens)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// GetAccount
|
||||
// @Title GetAccount
|
||||
// @Tag Account API
|
||||
@@ -424,7 +567,7 @@ func (c *ApiController) GetAccount() {
|
||||
return
|
||||
}
|
||||
|
||||
managedAccounts := c.Input().Get("managedAccounts")
|
||||
managedAccounts := c.Ctx.Input.Query("managedAccounts")
|
||||
if managedAccounts == "1" {
|
||||
user, err = object.ExtendManagedAccountsWithUser(user)
|
||||
if err != nil {
|
||||
@@ -458,6 +601,10 @@ func (c *ApiController) GetAccount() {
|
||||
return
|
||||
}
|
||||
|
||||
if organization != nil && len(organization.CountryCodes) == 1 && u != nil && u.CountryCode == "" {
|
||||
u.CountryCode = organization.CountryCodes[0]
|
||||
}
|
||||
|
||||
accessToken := c.GetSessionToken()
|
||||
if accessToken == "" {
|
||||
accessToken, err = object.GetAccessTokenByUser(user, c.Ctx.Request.Host)
|
||||
@@ -538,9 +685,54 @@ func (c *ApiController) GetUserinfo2() {
|
||||
// @router /get-captcha [get]
|
||||
// @Success 200 {object} object.Userinfo The Response object
|
||||
func (c *ApiController) GetCaptcha() {
|
||||
applicationId := c.Input().Get("applicationId")
|
||||
isCurrentProvider := c.Input().Get("isCurrentProvider")
|
||||
applicationId := c.Ctx.Input.Query("applicationId")
|
||||
isCurrentProvider := c.Ctx.Input.Query("isCurrentProvider")
|
||||
|
||||
// When isCurrentProvider == "true", the frontend passes a provider ID instead of an application ID.
|
||||
// In that case, skip application lookup and rule evaluation, and just return the provider config.
|
||||
shouldSkipCaptcha := false
|
||||
|
||||
if isCurrentProvider != "true" {
|
||||
application, err := object.GetApplication(applicationId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), applicationId))
|
||||
return
|
||||
}
|
||||
|
||||
// Check the CAPTCHA rule to determine if CAPTCHA should be shown
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
|
||||
// For Internet-Only rule, we can determine on the backend if CAPTCHA should be shown
|
||||
// For other rules (Dynamic, Always), we need to return the CAPTCHA config
|
||||
for _, providerItem := range application.Providers {
|
||||
if providerItem.Provider == nil || providerItem.Provider.Category != "Captcha" {
|
||||
continue
|
||||
}
|
||||
|
||||
// For "None" rule, skip CAPTCHA
|
||||
if providerItem.Rule == "None" || providerItem.Rule == "" {
|
||||
shouldSkipCaptcha = true
|
||||
} else if providerItem.Rule == "Internet-Only" {
|
||||
// For Internet-Only rule, check if the client is from intranet
|
||||
if !util.IsInternetIp(clientIp) {
|
||||
// Client is from intranet, skip CAPTCHA
|
||||
shouldSkipCaptcha = true
|
||||
}
|
||||
}
|
||||
|
||||
break // Only check the first CAPTCHA provider
|
||||
}
|
||||
|
||||
if shouldSkipCaptcha {
|
||||
c.ResponseOk(Captcha{Type: "none"})
|
||||
return
|
||||
}
|
||||
}
|
||||
captchaProvider, err := object.GetCaptchaProviderByApplication(applicationId, isCurrentProvider, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Adapter The Response object
|
||||
// @router /get-adapters [get]
|
||||
func (c *ApiController) GetAdapters() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
adapters, err := object.GetAdapters(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetAdapters() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
adapters, err := object.GetPaginationAdapters(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +73,7 @@ func (c *ApiController) GetAdapters() {
|
||||
// @Success 200 {object} object.Adapter The Response object
|
||||
// @router /get-adapter [get]
|
||||
func (c *ApiController) GetAdapter() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
adapter, err := object.GetAdapter(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) GetAdapter() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-adapter [post]
|
||||
func (c *ApiController) UpdateAdapter() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var adapter object.Adapter
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &adapter)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -32,14 +32,14 @@ import (
|
||||
// @router /get-applications [get]
|
||||
func (c *ApiController) GetApplications() {
|
||||
userId := c.GetSessionUsername()
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
var err error
|
||||
if limit == "" || page == "" {
|
||||
var applications []*object.Application
|
||||
@@ -61,7 +61,7 @@ func (c *ApiController) GetApplications() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
application, err := object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -82,7 +82,7 @@ func (c *ApiController) GetApplications() {
|
||||
// @router /get-application [get]
|
||||
func (c *ApiController) GetApplication() {
|
||||
userId := c.GetSessionUsername()
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
application, err := object.GetApplication(id)
|
||||
if err != nil {
|
||||
@@ -90,7 +90,7 @@ func (c *ApiController) GetApplication() {
|
||||
return
|
||||
}
|
||||
|
||||
if c.Input().Get("withKey") != "" && application != nil && application.Cert != "" {
|
||||
if c.Ctx.Input.Query("withKey") != "" && application != nil && application.Cert != "" {
|
||||
cert, err := object.GetCert(util.GetId(application.Owner, application.Cert))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -125,7 +125,7 @@ func (c *ApiController) GetApplication() {
|
||||
// @router /get-user-application [get]
|
||||
func (c *ApiController) GetUserApplication() {
|
||||
userId := c.GetSessionUsername()
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
user, err := object.GetUser(id)
|
||||
if err != nil {
|
||||
@@ -159,14 +159,14 @@ func (c *ApiController) GetUserApplication() {
|
||||
// @router /get-organization-applications [get]
|
||||
func (c *ApiController) GetOrganizationApplications() {
|
||||
userId := c.GetSessionUsername()
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if organization == "" {
|
||||
c.ResponseError(c.T("general:Missing parameter") + ": organization")
|
||||
@@ -196,7 +196,7 @@ func (c *ApiController) GetOrganizationApplications() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
applications, err := object.GetPaginationOrganizationApplications(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -223,7 +223,7 @@ func (c *ApiController) GetOrganizationApplications() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-application [post]
|
||||
func (c *ApiController) UpdateApplication() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var application object.Application
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &application)
|
||||
@@ -237,7 +237,7 @@ func (c *ApiController) UpdateApplication() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application))
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application, c.IsGlobalAdmin(), c.GetAcceptLanguage()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
@@ -22,12 +23,16 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/captcha"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/form"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
@@ -53,6 +58,16 @@ func tokenToResponse(token *object.Token) *Response {
|
||||
|
||||
// HandleLoggedIn ...
|
||||
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
|
||||
if user.IsForbidden {
|
||||
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsDeleted {
|
||||
c.ResponseError(c.T("check:The user has been deleted and cannot be used to sign in, please contact the administrator"))
|
||||
return
|
||||
}
|
||||
|
||||
userId := user.GetId()
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
@@ -62,6 +77,16 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
return
|
||||
}
|
||||
|
||||
if application.DisableSignin {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s has disabled users to signin"), application.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if application.OrganizationObj != nil && application.OrganizationObj.DisableSignin {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The organization: %s has disabled users to signin"), application.Organization))
|
||||
return
|
||||
}
|
||||
|
||||
allowed, err := object.CheckLoginPermission(userId, application)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@@ -75,7 +100,8 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
// check user's tag
|
||||
if !user.IsGlobalAdmin() && !user.IsAdmin && len(application.Tags) > 0 {
|
||||
// only users with the tag that is listed in the application tags can login
|
||||
if !util.InSlice(application.Tags, user.Tag) {
|
||||
// supports comma-separated tags in user.Tag (e.g., "default-policy,project-admin")
|
||||
if !util.HasTagInSlice(application.Tags, user.Tag) {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:User's tag: %s is not listed in the application's tags"), user.Tag))
|
||||
return
|
||||
}
|
||||
@@ -113,6 +139,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"), user.Name, application.Name))
|
||||
return
|
||||
} else {
|
||||
c.SetSession("paidUsername", user.GetId())
|
||||
// let the paid-user select plan
|
||||
c.ResponseOk("SelectPlan", pricing)
|
||||
return
|
||||
@@ -124,29 +151,30 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
if form.Type == ResponseTypeLogin {
|
||||
c.SetSessionUsername(userId)
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||
resp = &Response{Status: "ok", Msg: "", Data: userId, Data2: user.NeedUpdatePassword}
|
||||
resp = &Response{Status: "ok", Msg: "", Data: userId, Data3: user.NeedUpdatePassword}
|
||||
} else if form.Type == ResponseTypeCode {
|
||||
clientId := c.Input().Get("clientId")
|
||||
responseType := c.Input().Get("responseType")
|
||||
redirectUri := c.Input().Get("redirectUri")
|
||||
scope := c.Input().Get("scope")
|
||||
state := c.Input().Get("state")
|
||||
nonce := c.Input().Get("nonce")
|
||||
challengeMethod := c.Input().Get("code_challenge_method")
|
||||
codeChallenge := c.Input().Get("code_challenge")
|
||||
clientId := c.Ctx.Input.Query("clientId")
|
||||
responseType := c.Ctx.Input.Query("responseType")
|
||||
redirectUri := c.Ctx.Input.Query("redirectUri")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
state := c.Ctx.Input.Query("state")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
challengeMethod := c.Ctx.Input.Query("code_challenge_method")
|
||||
codeChallenge := c.Ctx.Input.Query("code_challenge")
|
||||
resource := c.Ctx.Input.Query("resource")
|
||||
|
||||
if challengeMethod != "S256" && challengeMethod != "null" && challengeMethod != "" {
|
||||
c.ResponseError(c.T("auth:Challenge method should be S256"))
|
||||
return
|
||||
}
|
||||
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
code, err := object.GetOAuthCode(userId, clientId, form.Provider, form.SigninMethod, responseType, redirectUri, scope, state, nonce, codeChallenge, resource, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp = codeToResponse(code)
|
||||
resp.Data2 = user.NeedUpdatePassword
|
||||
resp.Data3 = user.NeedUpdatePassword
|
||||
if application.EnableSigninSession || application.HasPromptPage() {
|
||||
// The prompt page needs the user to be signed in
|
||||
c.SetSessionUsername(userId)
|
||||
@@ -155,20 +183,46 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
if !object.IsGrantTypeValid(form.Type, application.GrantTypes) {
|
||||
resp = &Response{Status: "error", Msg: fmt.Sprintf("error: grant_type: %s is not supported in this application", form.Type), Data: ""}
|
||||
} else {
|
||||
scope := c.Input().Get("scope")
|
||||
nonce := c.Input().Get("nonce")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
|
||||
resp = tokenToResponse(token)
|
||||
|
||||
resp.Data2 = user.NeedUpdatePassword
|
||||
resp.Data3 = user.NeedUpdatePassword
|
||||
}
|
||||
} else if form.Type == ResponseTypeDevice {
|
||||
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:UserCode Expired"))
|
||||
return
|
||||
}
|
||||
|
||||
authCacheCast := authCache.(object.DeviceAuthCache)
|
||||
if authCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
|
||||
c.ResponseError(c.T("auth:UserCode Expired"))
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuthCacheDeviceCode, ok := object.DeviceAuthMap.Load(authCacheCast.UserName)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:DeviceCode Invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuthCacheDeviceCodeCast := deviceAuthCacheDeviceCode.(object.DeviceAuthCache)
|
||||
deviceAuthCacheDeviceCodeCast.UserName = user.Name
|
||||
deviceAuthCacheDeviceCodeCast.UserSignIn = true
|
||||
|
||||
object.DeviceAuthMap.Store(authCacheCast.UserName, deviceAuthCacheDeviceCodeCast)
|
||||
|
||||
resp = &Response{Status: "ok", Msg: "", Data: userId, Data3: user.NeedUpdatePassword}
|
||||
} else if form.Type == ResponseTypeSaml { // saml flow
|
||||
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]interface{}{"redirectUrl": redirectUrl, "method": method, "needUpdatePassword": user.NeedUpdatePassword}}
|
||||
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]interface{}{"redirectUrl": redirectUrl, "method": method}, Data3: user.NeedUpdatePassword}
|
||||
|
||||
if application.EnableSigninSession || application.HasPromptPage() {
|
||||
// The prompt page needs the user to be signed in
|
||||
@@ -176,7 +230,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
}
|
||||
} else if form.Type == ResponseTypeCas {
|
||||
// not oauth but CAS SSO protocol
|
||||
service := c.Input().Get("service")
|
||||
service := c.Ctx.Input.Query("service")
|
||||
resp = wrapErrorResponse(nil)
|
||||
if service != "" {
|
||||
st, err := object.GenerateCasToken(userId, service)
|
||||
@@ -195,9 +249,36 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
resp = wrapErrorResponse(fmt.Errorf("unknown response type: %s", form.Type))
|
||||
}
|
||||
|
||||
// if user did not check auto signin
|
||||
if resp.Status == "ok" && !form.AutoSignin {
|
||||
c.setExpireForSession()
|
||||
// For all successful logins, set the session expiration; if auto signin is not checked, cap it at 24 hours.
|
||||
if resp.Status == "ok" {
|
||||
expireInHours := application.CookieExpireInHours
|
||||
|
||||
if expireInHours == 0 {
|
||||
expireInHours = 720
|
||||
}
|
||||
|
||||
if !form.AutoSignin && expireInHours > 24 {
|
||||
expireInHours = 24
|
||||
}
|
||||
c.setExpireForSession(expireInHours)
|
||||
}
|
||||
|
||||
if application.EnableExclusiveSignin {
|
||||
sessions, err := object.GetUserAppSessions(user.Owner, user.Name, application.Name)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
for _, sid := range session.SessionId {
|
||||
err := web.GlobalSessions.GetProvider().SessionDestroy(context.Background(), sid)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resp.Status == "ok" {
|
||||
@@ -205,7 +286,9 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
Owner: user.Owner,
|
||||
Name: user.Name,
|
||||
Application: application.Name,
|
||||
SessionId: []string{c.Ctx.Input.CruSession.SessionID()},
|
||||
SessionId: []string{c.Ctx.Input.CruSession.SessionID(context.Background())},
|
||||
|
||||
ExclusiveSignin: application.EnableExclusiveSignin,
|
||||
})
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@@ -228,13 +311,14 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-app-login [get]
|
||||
func (c *ApiController) GetApplicationLogin() {
|
||||
clientId := c.Input().Get("clientId")
|
||||
responseType := c.Input().Get("responseType")
|
||||
redirectUri := c.Input().Get("redirectUri")
|
||||
scope := c.Input().Get("scope")
|
||||
state := c.Input().Get("state")
|
||||
id := c.Input().Get("id")
|
||||
loginType := c.Input().Get("type")
|
||||
clientId := c.Ctx.Input.Query("clientId")
|
||||
responseType := c.Ctx.Input.Query("responseType")
|
||||
redirectUri := c.Ctx.Input.Query("redirectUri")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
state := c.Ctx.Input.Query("state")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
loginType := c.Ctx.Input.Query("type")
|
||||
userCode := c.Ctx.Input.Query("userCode")
|
||||
|
||||
var application *object.Application
|
||||
var msg string
|
||||
@@ -261,6 +345,19 @@ func (c *ApiController) GetApplicationLogin() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else if loginType == "device" {
|
||||
deviceAuthCache, ok := object.DeviceAuthMap.Load(userCode)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:UserCode Invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
|
||||
application, err = object.GetApplication(deviceAuthCacheCast.ApplicationId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
@@ -305,6 +402,42 @@ func isProxyProviderType(providerType string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func checkMfaEnable(c *ApiController, user *object.User, organization *object.Organization, verificationType string) bool {
|
||||
if object.IsNeedPromptMfa(organization, user) {
|
||||
// The prompt page needs the user to be signed in
|
||||
c.SetSessionUsername(user.GetId())
|
||||
c.ResponseOk(object.RequiredMfa)
|
||||
return true
|
||||
}
|
||||
|
||||
if user.IsMfaEnabled() {
|
||||
currentTime := util.String2Time(util.GetCurrentTime())
|
||||
mfaRememberDeadline := util.String2Time(user.MfaRememberDeadline)
|
||||
if user.MfaRememberDeadline != "" && mfaRememberDeadline.After(currentTime) {
|
||||
return false
|
||||
}
|
||||
c.setMfaUserSession(user.GetId())
|
||||
mfaList := object.GetAllMfaProps(user, true)
|
||||
mfaAllowList := []*object.MfaProps{}
|
||||
mfaRememberInHours := organization.MfaRememberInHours
|
||||
for _, prop := range mfaList {
|
||||
if prop.MfaType == verificationType || !prop.Enabled {
|
||||
continue
|
||||
}
|
||||
prop.MfaRememberInHours = mfaRememberInHours
|
||||
mfaAllowList = append(mfaAllowList, prop)
|
||||
}
|
||||
if len(mfaAllowList) >= 1 {
|
||||
c.SetSession("verificationCodeType", verificationType)
|
||||
c.Ctx.Input.CruSession.SessionRelease(context.Background(), c.Ctx.ResponseWriter)
|
||||
c.ResponseOk(object.NextMfa, mfaAllowList)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Login ...
|
||||
// @Title Login
|
||||
// @Tag Login API
|
||||
@@ -330,14 +463,9 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if authForm.Username != "" {
|
||||
if authForm.Type == ResponseTypeLogin {
|
||||
if c.GetSessionUsername() != "" {
|
||||
c.ResponseError(c.T("account:Please sign out first"), c.GetSessionUsername())
|
||||
return
|
||||
}
|
||||
}
|
||||
verificationType := ""
|
||||
|
||||
if authForm.Username != "" {
|
||||
var user *object.User
|
||||
if authForm.SigninMethod == "Face ID" {
|
||||
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
||||
@@ -365,11 +493,27 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
faceIdProvider, err := object.GetFaceIdProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
}
|
||||
|
||||
if faceIdProvider == nil {
|
||||
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ok, err := user.CheckUserFace(authForm.FaceIdImage, faceIdProvider)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
c.ResponseError(i18n.Translate(c.GetAcceptLanguage(), "check:Face data does not exist, cannot log in"))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if authForm.Password == "" {
|
||||
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@@ -409,6 +553,8 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), authForm.CountryCode))
|
||||
return
|
||||
}
|
||||
} else if verificationCodeType == object.VerifyTypeEmail {
|
||||
checkDest = authForm.Username
|
||||
}
|
||||
|
||||
// check result through Email or Phone
|
||||
@@ -424,6 +570,20 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if verificationCodeType == object.VerifyTypePhone {
|
||||
verificationType = "sms"
|
||||
} else {
|
||||
verificationType = "email"
|
||||
if !user.EmailVerified {
|
||||
user.EmailVerified = true
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"email_verified"}, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var application *object.Application
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||
@@ -444,8 +604,11 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(c.T("auth:The login method: login with LDAP is not enabled for the application"))
|
||||
return
|
||||
}
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
|
||||
var enableCaptcha bool
|
||||
if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username); err != nil {
|
||||
if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username, clientIp); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else if enableCaptcha {
|
||||
@@ -460,7 +623,7 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
var isHuman bool
|
||||
isHuman, err = captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
|
||||
isHuman, err = captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, captchaProvider.ClientId, authForm.ClientSecret, captchaProvider.ClientId2)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -514,16 +677,7 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(err.Error())
|
||||
}
|
||||
|
||||
if object.IsNeedPromptMfa(organization, user) {
|
||||
// The prompt page needs the user to be signed in
|
||||
c.SetSessionUsername(user.GetId())
|
||||
c.ResponseOk(object.RequiredMfa)
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsMfaEnabled() {
|
||||
c.setMfaUserSession(user.GetId())
|
||||
c.ResponseOk(object.NextMfa, user.GetPreferredMfaProps(true))
|
||||
if checkMfaEnable(c, user, organization, verificationType) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -564,6 +718,10 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), authForm.Provider))
|
||||
return
|
||||
}
|
||||
|
||||
providerItem := application.GetProviderItem(provider.Name)
|
||||
if !providerItem.IsProviderVisible() {
|
||||
@@ -571,6 +729,7 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
userInfo := &idp.UserInfo{}
|
||||
var token *oauth2.Token
|
||||
if provider.Category == "SAML" {
|
||||
// SAML
|
||||
userInfo, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
|
||||
@@ -581,6 +740,7 @@ func (c *ApiController) Login() {
|
||||
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
|
||||
// OAuth
|
||||
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
|
||||
idpInfo.CodeVerifier = authForm.CodeVerifier
|
||||
var idProvider idp.IdProvider
|
||||
idProvider, err = idp.GetIdProvider(idpInfo, authForm.RedirectUri)
|
||||
if err != nil {
|
||||
@@ -594,13 +754,13 @@ func (c *ApiController) Login() {
|
||||
|
||||
setHttpClient(idProvider, provider.Type)
|
||||
|
||||
if authForm.State != conf.GetConfigString("authState") && authForm.State != application.Name {
|
||||
stateApplicationName := strings.Split(authForm.State, "-org-")[0]
|
||||
if authForm.State != conf.GetConfigString("authState") && stateApplicationName != application.Name {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:State expected: %s, but got: %s"), conf.GetConfigString("authState"), authForm.State))
|
||||
return
|
||||
}
|
||||
|
||||
// https://github.com/golang/oauth2/issues/123#issuecomment-103715338
|
||||
var token *oauth2.Token
|
||||
token, err = idProvider.GetToken(authForm.Code)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -617,6 +777,17 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if provider.EmailRegex != "" {
|
||||
reg, err := regexp.Compile(provider.EmailRegex)
|
||||
if err != nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error()))
|
||||
return
|
||||
}
|
||||
if !reg.MatchString(userInfo.Email) {
|
||||
c.ResponseError(fmt.Sprintf(c.T("check:Email is invalid")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if authForm.Method == "signup" {
|
||||
@@ -638,20 +809,21 @@ func (c *ApiController) Login() {
|
||||
|
||||
if user != nil && !user.IsDeleted {
|
||||
// Sign in via OAuth (want to sign up but already have account)
|
||||
|
||||
if user.IsForbidden {
|
||||
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
||||
}
|
||||
// sync info from 3rd-party if possible
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if checkMfaEnable(c, user, organization, verificationType) {
|
||||
return
|
||||
}
|
||||
|
||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||
|
||||
c.Ctx.Input.SetParam("recordUserId", user.GetId())
|
||||
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
|
||||
} else if provider.Category == "OAuth" || provider.Category == "Web3" || provider.Category == "SAML" {
|
||||
// Sign up via OAuth
|
||||
if application.EnableLinkWithEmail {
|
||||
if userInfo.Email != "" {
|
||||
@@ -673,21 +845,44 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil || user.IsDeleted {
|
||||
// Try to find existing user by username (case-insensitive)
|
||||
// This allows OAuth providers (e.g., Wecom) to automatically associate with
|
||||
// existing users when usernames match, particularly useful for enterprise
|
||||
// scenarios where signup is disabled and users already exist in Casdoor
|
||||
if user == nil && userInfo.Username != "" {
|
||||
user, err = object.GetUserByFields(application.Organization, userInfo.Username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
if !application.EnableSignUp {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support"), provider.Type, userInfo.Username, userInfo.DisplayName))
|
||||
return
|
||||
}
|
||||
|
||||
if !providerItem.CanSignUp {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up"), provider.Type, userInfo.Username, userInfo.DisplayName, provider.Type))
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up"), provider.Type, userInfo.Username, userInfo.DisplayName, provider.Type))
|
||||
return
|
||||
}
|
||||
|
||||
if application.IsSignupItemRequired("Invitation code") {
|
||||
c.ResponseError(c.T("check:Invitation code cannot be blank"))
|
||||
// Check and validate invitation code
|
||||
invitation, msg := object.CheckInvitationCode(application, organization, &authForm, c.GetAcceptLanguage())
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
}
|
||||
invitationName := ""
|
||||
if invitation != nil {
|
||||
invitationName = invitation.Name
|
||||
}
|
||||
|
||||
// Handle UseEmailAsUsername for OAuth and Web3
|
||||
if organization.UseEmailAsUsername && userInfo.Email != "" {
|
||||
userInfo.Username = userInfo.Email
|
||||
}
|
||||
|
||||
// Handle username conflicts
|
||||
var tmpUser *object.User
|
||||
@@ -749,10 +944,21 @@ func (c *ApiController) Login() {
|
||||
IsDeleted: false,
|
||||
SignupApplication: application.Name,
|
||||
Properties: properties,
|
||||
Invitation: invitationName,
|
||||
InvitationCode: authForm.InvitationCode,
|
||||
RegisterType: "Application Signup",
|
||||
RegisterSource: fmt.Sprintf("%s/%s", application.Organization, application.Name),
|
||||
}
|
||||
|
||||
// Set group from invitation code if available, otherwise use provider's signup group
|
||||
if invitation != nil && invitation.SignupGroup != "" {
|
||||
user.Groups = []string{invitation.SignupGroup}
|
||||
} else if providerItem.SignupGroup != "" {
|
||||
user.Groups = []string{providerItem.SignupGroup}
|
||||
}
|
||||
|
||||
var affected bool
|
||||
affected, err = object.AddUser(user)
|
||||
affected, err = object.AddUser(user, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -763,9 +969,10 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if providerItem.SignupGroup != "" {
|
||||
user.Groups = []string{providerItem.SignupGroup}
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"groups"}, false)
|
||||
// Increment invitation usage count
|
||||
if invitation != nil {
|
||||
invitation.UsedCount += 1
|
||||
_, err = object.UpdateInvitation(invitation.GetId(), invitation, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -774,7 +981,7 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
// sync info from 3rd-party if possible
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -822,7 +1029,7 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
// sync info from 3rd-party if possible
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -853,18 +1060,66 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
var application *object.Application
|
||||
if authForm.ClientId == "" {
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||
} else {
|
||||
application, err = object.GetApplicationByClientId(authForm.ClientId)
|
||||
}
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
|
||||
return
|
||||
}
|
||||
|
||||
var organization *object.Organization
|
||||
organization, err = object.GetOrganization(util.GetId("admin", application.Organization))
|
||||
if err != nil {
|
||||
c.ResponseError(c.T(err.Error()))
|
||||
}
|
||||
|
||||
if authForm.Passcode != "" {
|
||||
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
|
||||
if authForm.MfaType == c.GetSession("verificationCodeType") {
|
||||
c.ResponseError("Invalid multi-factor authentication type")
|
||||
return
|
||||
}
|
||||
user.CountryCode = user.GetCountryCode(user.CountryCode)
|
||||
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetMfaProps(authForm.MfaType, false))
|
||||
if mfaUtil == nil {
|
||||
c.ResponseError("Invalid multi-factor authentication type")
|
||||
return
|
||||
}
|
||||
|
||||
err = mfaUtil.Verify(authForm.Passcode)
|
||||
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Passcode)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !passed {
|
||||
err = mfaUtil.Verify(authForm.Passcode)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if authForm.EnableMfaRemember {
|
||||
mfaRememberInSeconds := organization.MfaRememberInHours * 3600
|
||||
currentTime := util.String2Time(util.GetCurrentTime())
|
||||
duration := time.Duration(mfaRememberInSeconds) * time.Second
|
||||
user.MfaRememberDeadline = util.Time2String(currentTime.Add(duration))
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"mfa_remember_deadline"}, user.IsAdmin)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
c.SetSession("verificationCodeType", "")
|
||||
} else if authForm.RecoveryCode != "" {
|
||||
err = object.MfaRecover(user, authForm.RecoveryCode)
|
||||
if err != nil {
|
||||
@@ -876,18 +1131,6 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
var application *object.Application
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
|
||||
return
|
||||
}
|
||||
|
||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||
c.setMfaUserSession("")
|
||||
|
||||
@@ -907,6 +1150,10 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if authForm.Provider == "" {
|
||||
authForm.Provider = authForm.ProviderBack
|
||||
}
|
||||
|
||||
user := c.getCurrentUser()
|
||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||
|
||||
@@ -917,13 +1164,25 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
}
|
||||
|
||||
if authForm.Language != "" {
|
||||
user := c.getCurrentUser()
|
||||
if user != nil {
|
||||
user.Language = authForm.Language
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"language"}, user.IsAdmin)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) GetSamlLogin() {
|
||||
providerId := c.Input().Get("id")
|
||||
relayState := c.Input().Get("relayState")
|
||||
providerId := c.Ctx.Input.Query("id")
|
||||
relayState := c.Ctx.Input.Query("relayState")
|
||||
authURL, method, err := object.GenerateSamlRequest(providerId, relayState, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -933,8 +1192,8 @@ func (c *ApiController) GetSamlLogin() {
|
||||
}
|
||||
|
||||
func (c *ApiController) HandleSamlLogin() {
|
||||
relayState := c.Input().Get("RelayState")
|
||||
samlResponse := c.Input().Get("SAMLResponse")
|
||||
relayState := c.Ctx.Input.Query("RelayState")
|
||||
samlResponse := c.Ctx.Input.Query("SAMLResponse")
|
||||
decode, err := base64.StdEncoding.DecodeString(relayState)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -966,9 +1225,9 @@ func (c *ApiController) HandleOfficialAccountEvent() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
signature := c.Input().Get("signature")
|
||||
timestamp := c.Input().Get("timestamp")
|
||||
nonce := c.Input().Get("nonce")
|
||||
signature := c.Ctx.Input.Query("signature")
|
||||
timestamp := c.Ctx.Input.Query("timestamp")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
var data struct {
|
||||
MsgType string `xml:"MsgType"`
|
||||
Event string `xml:"Event"`
|
||||
@@ -986,7 +1245,7 @@ func (c *ApiController) HandleOfficialAccountEvent() {
|
||||
return
|
||||
}
|
||||
if data.Ticket == "" {
|
||||
c.ResponseError(err.Error())
|
||||
c.ResponseError("empty ticket")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -996,10 +1255,11 @@ func (c *ApiController) HandleOfficialAccountEvent() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if data.Ticket == "" {
|
||||
c.ResponseError("empty ticket")
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), providerId))
|
||||
return
|
||||
}
|
||||
|
||||
if !idp.VerifyWechatSignature(provider.Content, nonce, timestamp, signature) {
|
||||
c.ResponseError("invalid signature")
|
||||
return
|
||||
@@ -1025,7 +1285,7 @@ func (c *ApiController) HandleOfficialAccountEvent() {
|
||||
// @Param ticket query string true "The eventId of QRCode"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
func (c *ApiController) GetWebhookEventType() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
|
||||
idp.Lock.RLock()
|
||||
_, ok := idp.WechatCacheMap[ticket]
|
||||
@@ -1045,12 +1305,17 @@ func (c *ApiController) GetWebhookEventType() {
|
||||
// @Param id query string true "The id ( owner/name ) of provider"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
func (c *ApiController) GetQRCode() {
|
||||
providerId := c.Input().Get("id")
|
||||
providerId := c.Ctx.Input.Query("id")
|
||||
provider, err := object.GetProvider(providerId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), providerId))
|
||||
return
|
||||
}
|
||||
|
||||
code, ticket, err := idp.GetWechatOfficialAccountQRCode(provider.ClientId2, provider.ClientSecret2, providerId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -1068,29 +1333,28 @@ func (c *ApiController) GetQRCode() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-captcha-status [get]
|
||||
func (c *ApiController) GetCaptchaStatus() {
|
||||
organization := c.Input().Get("organization")
|
||||
userId := c.Input().Get("userId")
|
||||
user, err := object.GetUserByFields(organization, userId)
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
applicationName := c.Ctx.Input.Query("application")
|
||||
|
||||
application, err := object.GetApplication(fmt.Sprintf("admin/%s", applicationName))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
captchaEnabled := false
|
||||
if user != nil {
|
||||
var failedSigninLimit int
|
||||
failedSigninLimit, _, err = object.GetFailedSigninConfigByUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user.SigninWrongTimes >= failedSigninLimit {
|
||||
captchaEnabled = true
|
||||
}
|
||||
if application == nil {
|
||||
c.ResponseError("application not found")
|
||||
return
|
||||
}
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
captchaEnabled, err := object.CheckToEnableCaptcha(application, organization, userId, clientIp)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(captchaEnabled)
|
||||
return
|
||||
}
|
||||
|
||||
// Callback
|
||||
@@ -1106,3 +1370,75 @@ func (c *ApiController) Callback() {
|
||||
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
|
||||
c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl)
|
||||
}
|
||||
|
||||
// DeviceAuth
|
||||
// @Title DeviceAuth
|
||||
// @Tag Device Authorization Endpoint
|
||||
// @Description Endpoint for the device authorization flow
|
||||
// @router /device-auth [post]
|
||||
// @Success 200 {object} object.DeviceAuthResponse The Response object
|
||||
func (c *ApiController) DeviceAuth() {
|
||||
clientId := c.Ctx.Input.Query("client_id")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
application, err := object.GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
c.Data["json"] = object.TokenError{
|
||||
Error: err.Error(),
|
||||
ErrorDescription: err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.Data["json"] = object.TokenError{
|
||||
Error: c.T("token:Invalid client_id"),
|
||||
ErrorDescription: c.T("token:Invalid client_id"),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
deviceCode := util.GenerateId()
|
||||
userCode := util.GetRandomName()
|
||||
|
||||
generateTime := 0
|
||||
for {
|
||||
if generateTime > 5 {
|
||||
c.Data["json"] = object.TokenError{
|
||||
Error: "userCode gen",
|
||||
ErrorDescription: c.T("token:Invalid client_id"),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
_, ok := object.DeviceAuthMap.Load(userCode)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
generateTime++
|
||||
}
|
||||
|
||||
deviceAuthCache := object.DeviceAuthCache{
|
||||
UserSignIn: false,
|
||||
UserName: "",
|
||||
Scope: scope,
|
||||
ApplicationId: application.GetId(),
|
||||
RequestAt: time.Now(),
|
||||
}
|
||||
|
||||
userAuthCache := object.DeviceAuthCache{
|
||||
UserSignIn: false,
|
||||
UserName: deviceCode,
|
||||
Scope: scope,
|
||||
ApplicationId: application.GetId(),
|
||||
RequestAt: time.Now(),
|
||||
}
|
||||
|
||||
object.DeviceAuthMap.Store(deviceCode, deviceAuthCache)
|
||||
object.DeviceAuthMap.Store(userCode, userAuthCache)
|
||||
|
||||
c.Data["json"] = object.GetDeviceAuthResponse(deviceCode, userCode, c.Ctx.Request.Host)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/logs"
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
// ApiController
|
||||
// controller for handlers under /api uri
|
||||
type ApiController struct {
|
||||
beego.Controller
|
||||
web.Controller
|
||||
}
|
||||
|
||||
// RootController
|
||||
@@ -104,6 +105,13 @@ func (c *ApiController) getCurrentUser() *object.User {
|
||||
|
||||
// GetSessionUsername ...
|
||||
func (c *ApiController) GetSessionUsername() string {
|
||||
// prefer username stored in Beego context by ApiFilter
|
||||
if ctxUser := c.Ctx.Input.GetData("currentUserId"); ctxUser != nil {
|
||||
if username, ok := ctxUser.(string); ok {
|
||||
return username
|
||||
}
|
||||
}
|
||||
|
||||
// check if user session expired
|
||||
sessionData := c.GetSessionData()
|
||||
|
||||
@@ -122,6 +130,26 @@ func (c *ApiController) GetSessionUsername() string {
|
||||
return user.(string)
|
||||
}
|
||||
|
||||
// GetPaidUsername ...
|
||||
func (c *ApiController) GetPaidUsername() string {
|
||||
// check if user session expired
|
||||
sessionData := c.GetSessionData()
|
||||
|
||||
if sessionData != nil &&
|
||||
sessionData.ExpireTime != 0 &&
|
||||
sessionData.ExpireTime < time.Now().Unix() {
|
||||
c.ClearUserSession()
|
||||
return ""
|
||||
}
|
||||
|
||||
user := c.GetSession("paidUsername")
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return user.(string)
|
||||
}
|
||||
|
||||
func (c *ApiController) GetSessionToken() string {
|
||||
accessToken := c.GetSession("accessToken")
|
||||
if accessToken == nil {
|
||||
@@ -148,6 +176,7 @@ func (c *ApiController) GetSessionApplication() *object.Application {
|
||||
func (c *ApiController) ClearUserSession() {
|
||||
c.SetSessionUsername("")
|
||||
c.SetSessionData(nil)
|
||||
_ = c.SessionRegenerateID()
|
||||
}
|
||||
|
||||
func (c *ApiController) ClearTokenSession() {
|
||||
@@ -216,16 +245,19 @@ func (c *ApiController) setMfaUserSession(userId string) {
|
||||
}
|
||||
|
||||
func (c *ApiController) getMfaUserSession() string {
|
||||
userId := c.Ctx.Input.CruSession.Get(object.MfaSessionUserId)
|
||||
userId := c.Ctx.Input.CruSession.Get(context.Background(), object.MfaSessionUserId)
|
||||
if userId == nil {
|
||||
return ""
|
||||
}
|
||||
return userId.(string)
|
||||
}
|
||||
|
||||
func (c *ApiController) setExpireForSession() {
|
||||
func (c *ApiController) setExpireForSession(cookieExpireInHours int64) {
|
||||
timestamp := time.Now().Unix()
|
||||
timestamp += 3600 * 24
|
||||
if cookieExpireInHours == 0 {
|
||||
cookieExpireInHours = 720
|
||||
}
|
||||
timestamp += 3600 * cookieExpireInHours
|
||||
c.SetSessionData(&SessionData{
|
||||
ExpireTime: timestamp,
|
||||
})
|
||||
|
||||
@@ -41,8 +41,8 @@ func queryUnescape(service string) string {
|
||||
}
|
||||
|
||||
func (c *RootController) CasValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
service := c.Input().Get("service")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
service := c.Ctx.Input.Query("service")
|
||||
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
|
||||
if service == "" || ticket == "" {
|
||||
c.Ctx.Output.Body([]byte("no\n"))
|
||||
@@ -60,8 +60,8 @@ func (c *RootController) CasValidate() {
|
||||
}
|
||||
|
||||
func (c *RootController) CasServiceValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
format := c.Ctx.Input.Query("format")
|
||||
if !strings.HasPrefix(ticket, "ST") {
|
||||
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
|
||||
}
|
||||
@@ -75,8 +75,8 @@ func (c *RootController) CasProxyValidate() {
|
||||
}
|
||||
|
||||
func (c *RootController) CasP3ServiceValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
format := c.Ctx.Input.Query("format")
|
||||
if !strings.HasPrefix(ticket, "ST") {
|
||||
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
|
||||
}
|
||||
@@ -84,10 +84,10 @@ func (c *RootController) CasP3ServiceValidate() {
|
||||
}
|
||||
|
||||
func (c *RootController) CasP3ProxyValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
service := c.Input().Get("service")
|
||||
pgtUrl := c.Input().Get("pgtUrl")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
format := c.Ctx.Input.Query("format")
|
||||
service := c.Ctx.Input.Query("service")
|
||||
pgtUrl := c.Ctx.Input.Query("pgtUrl")
|
||||
|
||||
serviceResponse := object.CasServiceResponse{
|
||||
Xmlns: "http://www.yale.edu/tp/cas",
|
||||
@@ -161,9 +161,9 @@ func (c *RootController) CasP3ProxyValidate() {
|
||||
}
|
||||
|
||||
func (c *RootController) CasProxy() {
|
||||
pgt := c.Input().Get("pgt")
|
||||
targetService := c.Input().Get("targetService")
|
||||
format := c.Input().Get("format")
|
||||
pgt := c.Ctx.Input.Query("pgt")
|
||||
targetService := c.Ctx.Input.Query("targetService")
|
||||
format := c.Ctx.Input.Query("format")
|
||||
if pgt == "" || targetService == "" {
|
||||
c.sendCasProxyResponseErr(InvalidRequest, "pgt and targetService must exist", format)
|
||||
return
|
||||
@@ -200,7 +200,7 @@ func (c *RootController) CasProxy() {
|
||||
|
||||
func (c *RootController) SamlValidate() {
|
||||
c.Ctx.Output.Header("Content-Type", "text/xml; charset=utf-8")
|
||||
target := c.Input().Get("TARGET")
|
||||
target := c.Ctx.Input.Query("TARGET")
|
||||
body := c.Ctx.Input.RequestBody
|
||||
envelopRequest := struct {
|
||||
XMLName xml.Name `xml:"Envelope"`
|
||||
|
||||
@@ -34,11 +34,23 @@ import (
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /enforce [post]
|
||||
func (c *ApiController) Enforce() {
|
||||
permissionId := c.Input().Get("permissionId")
|
||||
modelId := c.Input().Get("modelId")
|
||||
resourceId := c.Input().Get("resourceId")
|
||||
enforcerId := c.Input().Get("enforcerId")
|
||||
owner := c.Input().Get("owner")
|
||||
permissionId := c.Ctx.Input.Query("permissionId")
|
||||
modelId := c.Ctx.Input.Query("modelId")
|
||||
resourceId := c.Ctx.Input.Query("resourceId")
|
||||
enforcerId := c.Ctx.Input.Query("enforcerId")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
params := []string{permissionId, modelId, resourceId, enforcerId, owner}
|
||||
nonEmpty := 0
|
||||
for _, param := range params {
|
||||
if param != "" {
|
||||
nonEmpty++
|
||||
}
|
||||
}
|
||||
if nonEmpty > 1 {
|
||||
c.ResponseError("Only one of the parameters (permissionId, modelId, resourceId, enforcerId, owner) should be provided")
|
||||
return
|
||||
}
|
||||
|
||||
if len(c.Ctx.Input.RequestBody) == 0 {
|
||||
c.ResponseError("The request body should not be empty")
|
||||
@@ -107,7 +119,11 @@ func (c *ApiController) Enforce() {
|
||||
|
||||
permissions := []*object.Permission{}
|
||||
if modelId != "" {
|
||||
owner, modelName := util.GetOwnerAndNameFromId(modelId)
|
||||
owner, modelName, err := util.GetOwnerAndNameFromIdWithError(modelId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
permissions, err = object.GetPermissionsByModel(owner, modelName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -164,10 +180,22 @@ func (c *ApiController) Enforce() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /batch-enforce [post]
|
||||
func (c *ApiController) BatchEnforce() {
|
||||
permissionId := c.Input().Get("permissionId")
|
||||
modelId := c.Input().Get("modelId")
|
||||
enforcerId := c.Input().Get("enforcerId")
|
||||
owner := c.Input().Get("owner")
|
||||
permissionId := c.Ctx.Input.Query("permissionId")
|
||||
modelId := c.Ctx.Input.Query("modelId")
|
||||
enforcerId := c.Ctx.Input.Query("enforcerId")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
params := []string{permissionId, modelId, enforcerId, owner}
|
||||
nonEmpty := 0
|
||||
for _, param := range params {
|
||||
if param != "" {
|
||||
nonEmpty++
|
||||
}
|
||||
}
|
||||
if nonEmpty > 1 {
|
||||
c.ResponseError("Only one of the parameters (permissionId, modelId, enforcerId, owner) should be provided")
|
||||
return
|
||||
}
|
||||
|
||||
var requests [][]string
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &requests)
|
||||
@@ -231,7 +259,11 @@ func (c *ApiController) BatchEnforce() {
|
||||
|
||||
permissions := []*object.Permission{}
|
||||
if modelId != "" {
|
||||
owner, modelName := util.GetOwnerAndNameFromId(modelId)
|
||||
owner, modelName, err := util.GetOwnerAndNameFromIdWithError(modelId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
permissions, err = object.GetPermissionsByModel(owner, modelName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -271,8 +303,15 @@ func (c *ApiController) BatchEnforce() {
|
||||
c.ResponseOk(res, keyRes)
|
||||
}
|
||||
|
||||
// GetAllObjects
|
||||
// @Title GetAllObjects
|
||||
// @Tag Enforcer API
|
||||
// @Description Get all objects for a user (Casbin API)
|
||||
// @Param userId query string false "user id like built-in/admin"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-all-objects [get]
|
||||
func (c *ApiController) GetAllObjects() {
|
||||
userId := c.Input().Get("userId")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
if userId == "" {
|
||||
userId = c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
@@ -290,8 +329,15 @@ func (c *ApiController) GetAllObjects() {
|
||||
c.ResponseOk(objects)
|
||||
}
|
||||
|
||||
// GetAllActions
|
||||
// @Title GetAllActions
|
||||
// @Tag Enforcer API
|
||||
// @Description Get all actions for a user (Casbin API)
|
||||
// @Param userId query string false "user id like built-in/admin"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-all-actions [get]
|
||||
func (c *ApiController) GetAllActions() {
|
||||
userId := c.Input().Get("userId")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
if userId == "" {
|
||||
userId = c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
@@ -309,8 +355,15 @@ func (c *ApiController) GetAllActions() {
|
||||
c.ResponseOk(actions)
|
||||
}
|
||||
|
||||
// GetAllRoles
|
||||
// @Title GetAllRoles
|
||||
// @Tag Enforcer API
|
||||
// @Description Get all roles for a user (Casbin API)
|
||||
// @Param userId query string false "user id like built-in/admin"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-all-roles [get]
|
||||
func (c *ApiController) GetAllRoles() {
|
||||
userId := c.Input().Get("userId")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
if userId == "" {
|
||||
userId = c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
|
||||
312
controllers/casbin_cli_api.go
Normal file
312
controllers/casbin_cli_api.go
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CLIVersionInfo struct {
|
||||
Version string
|
||||
BinaryPath string
|
||||
BinaryTime time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
cliVersionCache = make(map[string]*CLIVersionInfo)
|
||||
cliVersionMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// cleanOldMEIFolders cleans up old _MEIXXX folders from the Casdoor temp directory
|
||||
// that are older than 24 hours. These folders are created by PyInstaller when
|
||||
// executing casbin-python-cli and can accumulate over time.
|
||||
func cleanOldMEIFolders() {
|
||||
tempDir := "temp"
|
||||
cutoffTime := time.Now().Add(-24 * time.Hour)
|
||||
|
||||
entries, err := os.ReadDir(tempDir)
|
||||
if err != nil {
|
||||
// Log error but don't fail - cleanup is best-effort
|
||||
// This is expected if temp directory doesn't exist yet
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
// Check if the entry is a directory and matches the _MEI pattern
|
||||
if !entry.IsDir() || !strings.HasPrefix(entry.Name(), "_MEI") {
|
||||
continue
|
||||
}
|
||||
|
||||
dirPath := filepath.Join(tempDir, entry.Name())
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the folder is older than 24 hours
|
||||
if info.ModTime().Before(cutoffTime) {
|
||||
// Try to remove the directory
|
||||
err = os.RemoveAll(dirPath)
|
||||
if err != nil {
|
||||
// Log but continue with other folders
|
||||
fmt.Printf("failed to remove old MEI folder %s: %v\n", dirPath, err)
|
||||
} else {
|
||||
fmt.Printf("removed old MEI folder: %s\n", dirPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getCLIVersion
|
||||
// @Title getCLIVersion
|
||||
// @Description Get CLI version with cache mechanism
|
||||
// @Param language string The language of CLI (go/java/rust etc.)
|
||||
// @Return string The version string of CLI
|
||||
// @Return error Error if CLI execution fails
|
||||
func getCLIVersion(language string) (string, error) {
|
||||
binaryName := fmt.Sprintf("casbin-%s-cli", language)
|
||||
|
||||
binaryPath, err := exec.LookPath(binaryName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("executable file not found: %v", err)
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(binaryPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get binary info: %v", err)
|
||||
}
|
||||
|
||||
cliVersionMutex.RLock()
|
||||
if info, exists := cliVersionCache[language]; exists {
|
||||
if info.BinaryPath == binaryPath && info.BinaryTime == fileInfo.ModTime() {
|
||||
cliVersionMutex.RUnlock()
|
||||
return info.Version, nil
|
||||
}
|
||||
}
|
||||
cliVersionMutex.RUnlock()
|
||||
|
||||
// Clean up old _MEI folders before running the command
|
||||
cleanOldMEIFolders()
|
||||
|
||||
cmd := exec.Command(binaryName, "--version")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get CLI version: %v", err)
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(string(output))
|
||||
|
||||
cliVersionMutex.Lock()
|
||||
cliVersionCache[language] = &CLIVersionInfo{
|
||||
Version: version,
|
||||
BinaryPath: binaryPath,
|
||||
BinaryTime: fileInfo.ModTime(),
|
||||
}
|
||||
cliVersionMutex.Unlock()
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
||||
tempFiles := []string{}
|
||||
newArgs := []string{}
|
||||
for i := 0; i < len(args); i++ {
|
||||
if (args[i] == "-m" || args[i] == "-p") && i+1 < len(args) {
|
||||
pattern := fmt.Sprintf("casbin_temp_%s_*.conf", args[i])
|
||||
tempFile, err := os.CreateTemp("", pattern)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
_, err = tempFile.WriteString(args[i+1])
|
||||
if err != nil {
|
||||
tempFile.Close()
|
||||
return nil, nil, fmt.Errorf("failed to write to temp file: %v", err)
|
||||
}
|
||||
|
||||
tempFile.Close()
|
||||
tempFiles = append(tempFiles, tempFile.Name())
|
||||
newArgs = append(newArgs, args[i], tempFile.Name())
|
||||
i++
|
||||
} else {
|
||||
newArgs = append(newArgs, args[i])
|
||||
}
|
||||
}
|
||||
return tempFiles, newArgs, nil
|
||||
}
|
||||
|
||||
// RunCasbinCommand
|
||||
// @Title RunCasbinCommand
|
||||
// @Tag Enforcer API
|
||||
// @Description Call Casbin CLI commands
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /run-casbin-command [get]
|
||||
func (c *ApiController) RunCasbinCommand() {
|
||||
if err := validateIdentifier(c); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
language := c.Ctx.Input.Query("language")
|
||||
argString := c.Ctx.Input.Query("args")
|
||||
|
||||
if language == "" {
|
||||
language = "go"
|
||||
}
|
||||
// use "casbin-go-cli" by default, can be also "casbin-java-cli", "casbin-node-cli", etc.
|
||||
// the pre-built binary of "casbin-go-cli" can be found at: https://github.com/casbin/casbin-go-cli/releases
|
||||
binaryName := fmt.Sprintf("casbin-%s-cli", language)
|
||||
|
||||
_, err := exec.LookPath(binaryName)
|
||||
if err != nil {
|
||||
c.ResponseError(fmt.Sprintf("executable file: %s not found in PATH", binaryName))
|
||||
return
|
||||
}
|
||||
|
||||
// RBAC model & policy example:
|
||||
// https://door.casdoor.com/api/run-casbin-command?language=go&args=["enforce", "-m", "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = g(r.sub, p.sub) %26%26 r.obj == p.obj %26%26 r.act == p.act", "-p", "p, alice, data1, read\np, bob, data2, write\np, data2_admin, data2, read\np, data2_admin, data2, write\ng, alice, data2_admin", "alice", "data1", "read"]
|
||||
// Casbin CLI usage:
|
||||
// https://github.com/jcasbin/casbin-java-cli?tab=readme-ov-file#get-started
|
||||
var args []string
|
||||
err = json.Unmarshal([]byte(argString), &args)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Generate cache key for this command
|
||||
cacheKey, err := generateCacheKey(language, args)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check if result is cached
|
||||
if cachedOutput, found := getCachedCommandResult(cacheKey); found {
|
||||
c.ResponseOk(cachedOutput)
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) > 0 && args[0] == "--version" {
|
||||
version, err := getCLIVersion(language)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(version)
|
||||
return
|
||||
}
|
||||
|
||||
tempFiles, processedArgs, err := processArgsToTempFiles(args)
|
||||
defer func() {
|
||||
for _, file := range tempFiles {
|
||||
os.Remove(file)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up old _MEI folders before running the command
|
||||
// This is especially important for Python CLI which creates these folders
|
||||
cleanOldMEIFolders()
|
||||
|
||||
command := exec.Command(binaryName, processedArgs...)
|
||||
outputBytes, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
errorString := err.Error()
|
||||
if outputBytes != nil {
|
||||
output := string(outputBytes)
|
||||
errorString = fmt.Sprintf("%s, error: %s", output, err.Error())
|
||||
}
|
||||
|
||||
c.ResponseError(errorString)
|
||||
return
|
||||
}
|
||||
|
||||
output := string(outputBytes)
|
||||
output = strings.TrimSuffix(output, "\n")
|
||||
|
||||
// Store result in cache
|
||||
setCachedCommandResult(cacheKey, output)
|
||||
|
||||
c.ResponseOk(output)
|
||||
}
|
||||
|
||||
// validateIdentifier
|
||||
// @Title validateIdentifier
|
||||
// @Description Validate the request hash and timestamp
|
||||
// @Param hash string The SHA-256 hash string
|
||||
// @Return error Returns error if validation fails, nil if successful
|
||||
func validateIdentifier(c *ApiController) error {
|
||||
language := c.Ctx.Input.Query("language")
|
||||
args := c.Ctx.Input.Query("args")
|
||||
hash := c.Ctx.Input.Query("m")
|
||||
timestamp := c.Ctx.Input.Query("t")
|
||||
|
||||
if hash == "" || timestamp == "" || language == "" || args == "" {
|
||||
return fmt.Errorf("invalid identifier")
|
||||
}
|
||||
|
||||
requestTime, err := time.Parse(time.RFC3339, timestamp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid identifier")
|
||||
}
|
||||
timeDiff := time.Since(requestTime)
|
||||
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
|
||||
return fmt.Errorf("invalid identifier")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"language": language,
|
||||
"args": args,
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var paramParts []string
|
||||
for _, k := range keys {
|
||||
paramParts = append(paramParts, fmt.Sprintf("%s=%s", k, params[k]))
|
||||
}
|
||||
paramString := strings.Join(paramParts, "&")
|
||||
|
||||
version := "casbin-editor-v1"
|
||||
rawString := fmt.Sprintf("%s|%s|%s", version, timestamp, paramString)
|
||||
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(rawString))
|
||||
|
||||
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
|
||||
if calculatedHash != strings.ToLower(hash) {
|
||||
return fmt.Errorf("invalid identifier")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
100
controllers/casbin_cli_api_cache.go
Normal file
100
controllers/casbin_cli_api_cache.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CommandCacheEntry struct {
|
||||
Output string
|
||||
CachedTime time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
commandCache = make(map[string]*CommandCacheEntry)
|
||||
commandCacheMutex sync.RWMutex
|
||||
cacheTTL = 5 * time.Minute
|
||||
cleanupInProgress = false
|
||||
cleanupMutex sync.Mutex
|
||||
)
|
||||
|
||||
// generateCacheKey creates a unique cache key based on language and arguments
|
||||
func generateCacheKey(language string, args []string) (string, error) {
|
||||
argsJSON, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal args: %v", err)
|
||||
}
|
||||
data := fmt.Sprintf("%s:%s", language, string(argsJSON))
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(hash[:]), nil
|
||||
}
|
||||
|
||||
// cleanExpiredCacheEntries removes expired entries from the cache
|
||||
func cleanExpiredCacheEntries() {
|
||||
commandCacheMutex.Lock()
|
||||
defer commandCacheMutex.Unlock()
|
||||
|
||||
for key, entry := range commandCache {
|
||||
if time.Since(entry.CachedTime) >= cacheTTL {
|
||||
delete(commandCache, key)
|
||||
}
|
||||
}
|
||||
|
||||
cleanupMutex.Lock()
|
||||
cleanupInProgress = false
|
||||
cleanupMutex.Unlock()
|
||||
}
|
||||
|
||||
// getCachedCommandResult retrieves cached command result if available and not expired
|
||||
func getCachedCommandResult(cacheKey string) (string, bool) {
|
||||
commandCacheMutex.RLock()
|
||||
defer commandCacheMutex.RUnlock()
|
||||
|
||||
if entry, exists := commandCache[cacheKey]; exists {
|
||||
if time.Since(entry.CachedTime) < cacheTTL {
|
||||
return entry.Output, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// setCachedCommandResult stores command result in cache and performs periodic cleanup
|
||||
func setCachedCommandResult(cacheKey string, output string) {
|
||||
commandCacheMutex.Lock()
|
||||
commandCache[cacheKey] = &CommandCacheEntry{
|
||||
Output: output,
|
||||
CachedTime: time.Now(),
|
||||
}
|
||||
shouldCleanup := len(commandCache)%100 == 0
|
||||
commandCacheMutex.Unlock()
|
||||
|
||||
// Periodically clean expired entries (every 100 cache sets)
|
||||
if shouldCleanup {
|
||||
cleanupMutex.Lock()
|
||||
if !cleanupInProgress {
|
||||
cleanupInProgress = true
|
||||
cleanupMutex.Unlock()
|
||||
go cleanExpiredCacheEntries()
|
||||
} else {
|
||||
cleanupMutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Cert The Response object
|
||||
// @router /get-certs [get]
|
||||
func (c *ApiController) GetCerts() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
certs, err := object.GetMaskedCerts(object.GetCerts(owner))
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetCerts() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
certs, err := object.GetMaskedCerts(object.GetPaginationCerts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -72,12 +72,12 @@ func (c *ApiController) GetCerts() {
|
||||
// @Success 200 {array} object.Cert The Response object
|
||||
// @router /get-global-certs [get]
|
||||
func (c *ApiController) GetGlobalCerts() {
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
certs, err := object.GetMaskedCerts(object.GetGlobalCerts())
|
||||
@@ -95,7 +95,7 @@ func (c *ApiController) GetGlobalCerts() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
certs, err := object.GetMaskedCerts(object.GetPaginationGlobalCerts(paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -114,7 +114,7 @@ func (c *ApiController) GetGlobalCerts() {
|
||||
// @Success 200 {object} object.Cert The Response object
|
||||
// @router /get-cert [get]
|
||||
func (c *ApiController) GetCert() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
cert, err := object.GetCert(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -133,7 +133,7 @@ func (c *ApiController) GetCert() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-cert [post]
|
||||
func (c *ApiController) UpdateCert() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var cert object.Cert
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
|
||||
|
||||
541
controllers/cli_downloader.go
Normal file
541
controllers/cli_downloader.go
Normal file
@@ -0,0 +1,541 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
const (
|
||||
javaCliRepo = "https://api.github.com/repos/jcasbin/casbin-java-cli/releases/latest"
|
||||
goCliRepo = "https://api.github.com/repos/casbin/casbin-go-cli/releases/latest"
|
||||
rustCliRepo = "https://api.github.com/repos/casbin-rs/casbin-rust-cli/releases/latest"
|
||||
pythonCliRepo = "https://api.github.com/repos/casbin/casbin-python-cli/releases/latest"
|
||||
dotnetCliRepo = "https://api.github.com/repos/casbin-net/casbin-dotnet-cli/releases/latest"
|
||||
downloadFolder = "bin"
|
||||
)
|
||||
|
||||
type ReleaseInfo struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Assets []struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"browser_download_url"`
|
||||
} `json:"assets"`
|
||||
}
|
||||
|
||||
// @Title getBinaryNames
|
||||
// @Description Get binary names for different platforms and architectures
|
||||
// @Success 200 {map[string]string} map[string]string "Binary names map"
|
||||
func getBinaryNames() map[string]string {
|
||||
const (
|
||||
golang = "go"
|
||||
java = "java"
|
||||
rust = "rust"
|
||||
python = "python"
|
||||
dotnet = "dotnet"
|
||||
)
|
||||
|
||||
arch := runtime.GOARCH
|
||||
archMap := map[string]struct{ goArch, rustArch string }{
|
||||
"amd64": {"x86_64", "x86_64"},
|
||||
"arm64": {"arm64", "aarch64"},
|
||||
}
|
||||
|
||||
archNames, ok := archMap[arch]
|
||||
if !ok {
|
||||
archNames = struct{ goArch, rustArch string }{arch, arch}
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return map[string]string{
|
||||
golang: fmt.Sprintf("casbin-go-cli_Windows_%s.zip", archNames.goArch),
|
||||
java: "casbin-java-cli.jar",
|
||||
rust: fmt.Sprintf("casbin-rust-cli-%s-pc-windows-gnu", archNames.rustArch),
|
||||
python: fmt.Sprintf("casbin-python-cli-windows-%s.exe", archNames.goArch),
|
||||
dotnet: fmt.Sprintf("casbin-dotnet-cli-windows-%s.exe", archNames.goArch),
|
||||
}
|
||||
case "darwin":
|
||||
return map[string]string{
|
||||
golang: fmt.Sprintf("casbin-go-cli_Darwin_%s.tar.gz", archNames.goArch),
|
||||
java: "casbin-java-cli.jar",
|
||||
rust: fmt.Sprintf("casbin-rust-cli-%s-apple-darwin", archNames.rustArch),
|
||||
python: fmt.Sprintf("casbin-python-cli-darwin-%s", archNames.goArch),
|
||||
dotnet: fmt.Sprintf("casbin-dotnet-cli-darwin-%s", archNames.goArch),
|
||||
}
|
||||
case "linux":
|
||||
return map[string]string{
|
||||
golang: fmt.Sprintf("casbin-go-cli_Linux_%s.tar.gz", archNames.goArch),
|
||||
java: "casbin-java-cli.jar",
|
||||
rust: fmt.Sprintf("casbin-rust-cli-%s-unknown-linux-gnu", archNames.rustArch),
|
||||
python: fmt.Sprintf("casbin-python-cli-linux-%s", archNames.goArch),
|
||||
dotnet: fmt.Sprintf("casbin-dotnet-cli-linux-%s", archNames.goArch),
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// @Title getFinalBinaryName
|
||||
// @Description Get final binary name for specific language
|
||||
// @Param lang string true "Language type (go/java/rust)"
|
||||
// @Success 200 {string} string "Final binary name"
|
||||
func getFinalBinaryName(lang string) string {
|
||||
switch lang {
|
||||
case "go":
|
||||
if runtime.GOOS == "windows" {
|
||||
return "casbin-go-cli.exe"
|
||||
}
|
||||
return "casbin-go-cli"
|
||||
case "java":
|
||||
return "casbin-java-cli.jar"
|
||||
case "rust":
|
||||
if runtime.GOOS == "windows" {
|
||||
return "casbin-rust-cli.exe"
|
||||
}
|
||||
return "casbin-rust-cli"
|
||||
case "python":
|
||||
if runtime.GOOS == "windows" {
|
||||
return "casbin-python-cli.exe"
|
||||
}
|
||||
return "casbin-python-cli"
|
||||
case "dotnet":
|
||||
if runtime.GOOS == "windows" {
|
||||
return "casbin-dotnet-cli.exe"
|
||||
}
|
||||
return "casbin-dotnet-cli"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// @Title getLatestCLIURL
|
||||
// @Description Get latest CLI download URL from GitHub
|
||||
// @Param repoURL string true "GitHub repository URL"
|
||||
// @Param language string true "Language type"
|
||||
// @Success 200 {string} string "Download URL and version"
|
||||
func getLatestCLIURL(repoURL string, language string) (string, string, error) {
|
||||
client := proxy.GetHttpClient(repoURL)
|
||||
resp, err := client.Get(repoURL)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to fetch release info: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var release ReleaseInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
binaryNames := getBinaryNames()
|
||||
if binaryNames == nil {
|
||||
return "", "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
binaryName := binaryNames[language]
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == binaryName {
|
||||
return asset.URL, release.TagName, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("no suitable binary found for OS: %s, language: %s", runtime.GOOS, language)
|
||||
}
|
||||
|
||||
// @Title extractGoCliFile
|
||||
// @Description Extract the Go CLI file
|
||||
// @Param filePath string true "The file path"
|
||||
// @Success 200 {string} string "The extracted file path"
|
||||
// @router /extractGoCliFile [post]
|
||||
func extractGoCliFile(filePath string) error {
|
||||
tempDir := filepath.Join(downloadFolder, "temp")
|
||||
if err := os.MkdirAll(tempDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := unzipFile(filePath, tempDir); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := untarFile(filePath, tempDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
execName := "casbin-go-cli"
|
||||
if runtime.GOOS == "windows" {
|
||||
execName += ".exe"
|
||||
}
|
||||
|
||||
var execPath string
|
||||
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if info.Name() == execName {
|
||||
execPath = path
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
finalPath := filepath.Join(downloadFolder, execName)
|
||||
if err := os.Rename(execPath, finalPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
// @Title unzipFile
|
||||
// @Description Unzip the file
|
||||
// @Param zipPath string true "The zip file path"
|
||||
// @Param destDir string true "The destination directory"
|
||||
// @Success 200 {string} string "The extracted file path"
|
||||
// @router /unzipFile [post]
|
||||
func unzipFile(zipPath, destDir string) error {
|
||||
r, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
fpath := filepath.Join(destDir, f.Name)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
outFile.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, rc)
|
||||
outFile.Close()
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Title untarFile
|
||||
// @Description Untar the file
|
||||
// @Param tarPath string true "The tar file path"
|
||||
// @Param destDir string true "The destination directory"
|
||||
// @Success 200 {string} string "The extracted file path"
|
||||
// @router /untarFile [post]
|
||||
func untarFile(tarPath, destDir string) error {
|
||||
file, err := os.Open(tarPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(destDir, header.Name)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
outFile, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(outFile, tr); err != nil {
|
||||
outFile.Close()
|
||||
return err
|
||||
}
|
||||
outFile.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Title createJavaCliWrapper
|
||||
// @Description Create the Java CLI wrapper
|
||||
// @Param binPath string true "The binary path"
|
||||
// @Success 200 {string} string "The created file path"
|
||||
// @router /createJavaCliWrapper [post]
|
||||
func createJavaCliWrapper(binPath string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Create a Windows CMD file
|
||||
cmdPath := filepath.Join(binPath, "casbin-java-cli.cmd")
|
||||
cmdContent := fmt.Sprintf(`@echo off
|
||||
java -jar "%s\casbin-java-cli.jar" %%*`, binPath)
|
||||
|
||||
err := os.WriteFile(cmdPath, []byte(cmdContent), 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Create Unix shell script
|
||||
shPath := filepath.Join(binPath, "casbin-java-cli")
|
||||
shContent := fmt.Sprintf(`#!/bin/sh
|
||||
java -jar "%s/casbin-java-cli.jar" "$@"`, binPath)
|
||||
|
||||
err := os.WriteFile(shPath, []byte(shContent), 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Title downloadCLI
|
||||
// @Description Download and setup CLI tools
|
||||
// @Success 200 {error} error "Error if any"
|
||||
func downloadCLI() error {
|
||||
pathEnv := os.Getenv("PATH")
|
||||
binPath, err := filepath.Abs(downloadFolder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path to download directory: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(pathEnv, binPath) {
|
||||
newPath := fmt.Sprintf("%s%s%s", binPath, string(os.PathListSeparator), pathEnv)
|
||||
if err := os.Setenv("PATH", newPath); err != nil {
|
||||
return fmt.Errorf("failed to update PATH environment variable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(downloadFolder, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create download directory: %v", err)
|
||||
}
|
||||
|
||||
repos := map[string]string{
|
||||
"java": javaCliRepo,
|
||||
"go": goCliRepo,
|
||||
"rust": rustCliRepo,
|
||||
"python": pythonCliRepo,
|
||||
"dotnet": dotnetCliRepo,
|
||||
}
|
||||
|
||||
for lang, repo := range repos {
|
||||
cliURL, version, err := getLatestCLIURL(repo, lang)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to get %s CLI URL: %v\n", lang, err)
|
||||
continue
|
||||
}
|
||||
|
||||
originalPath := filepath.Join(downloadFolder, getBinaryNames()[lang])
|
||||
fmt.Printf("downloading %s CLI: %s\n", lang, cliURL)
|
||||
|
||||
client := proxy.GetHttpClient(cliURL)
|
||||
resp, err := client.Get(cliURL)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
|
||||
continue
|
||||
}
|
||||
|
||||
func() {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(originalPath), 0o755); err != nil {
|
||||
fmt.Printf("failed to create directory for %s CLI: %v\n", lang, err)
|
||||
return
|
||||
}
|
||||
|
||||
tmpFile := originalPath + ".tmp"
|
||||
out, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create or write %s CLI: %v\n", lang, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
out.Close()
|
||||
os.Remove(tmpFile)
|
||||
}()
|
||||
|
||||
if _, err = io.Copy(out, resp.Body); err != nil ||
|
||||
out.Close() != nil ||
|
||||
os.Rename(tmpFile, originalPath) != nil {
|
||||
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
if lang == "go" {
|
||||
if err := extractGoCliFile(originalPath); err != nil {
|
||||
fmt.Printf("failed to extract Go CLI: %v\n", err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
finalPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
|
||||
if err := os.Rename(originalPath, finalPath); err != nil {
|
||||
fmt.Printf("failed to rename %s CLI: %v\n", lang, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
execPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
|
||||
if err := os.Chmod(execPath, 0o755); err != nil {
|
||||
fmt.Printf("failed to set %s CLI execution permission: %v\n", lang, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("downloaded %s CLI version: %s\n", lang, version)
|
||||
|
||||
if lang == "java" {
|
||||
if err := createJavaCliWrapper(binPath); err != nil {
|
||||
fmt.Printf("failed to create Java CLI wrapper: %v\n", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Title RefreshEngines
|
||||
// @Tag CLI API
|
||||
// @Description Refresh all CLI engines
|
||||
// @Param m query string true "Hash for request validation"
|
||||
// @Param t query string true "Timestamp for request validation"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /refresh-engines [post]
|
||||
func (c *ApiController) RefreshEngines() {
|
||||
if !web.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
c.ResponseError("refresh engines is only available in demo mode")
|
||||
return
|
||||
}
|
||||
|
||||
hash := c.Ctx.Input.Query("m")
|
||||
timestamp := c.Ctx.Input.Query("t")
|
||||
|
||||
if hash == "" || timestamp == "" {
|
||||
c.ResponseError("invalid identifier")
|
||||
return
|
||||
}
|
||||
|
||||
requestTime, err := time.Parse(time.RFC3339, timestamp)
|
||||
if err != nil {
|
||||
c.ResponseError("invalid identifier")
|
||||
return
|
||||
}
|
||||
|
||||
timeDiff := time.Since(requestTime)
|
||||
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
|
||||
c.ResponseError("invalid identifier")
|
||||
return
|
||||
}
|
||||
|
||||
version := "casbin-editor-v1"
|
||||
rawString := fmt.Sprintf("%s|%s", version, timestamp)
|
||||
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(rawString))
|
||||
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
|
||||
|
||||
if calculatedHash != strings.ToLower(hash) {
|
||||
c.ResponseError("invalid identifier")
|
||||
return
|
||||
}
|
||||
|
||||
err = downloadCLI()
|
||||
if err != nil {
|
||||
c.ResponseError(fmt.Sprintf("failed to refresh engines: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(map[string]string{
|
||||
"status": "success",
|
||||
"message": "CLI engines updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// @Title ScheduleCLIUpdater
|
||||
// @Description Start periodic CLI update scheduler
|
||||
func ScheduleCLIUpdater() {
|
||||
if !web.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
err := downloadCLI()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to update CLI: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("CLI updated successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @Title DownloadCLI
|
||||
// @Description Download the CLI
|
||||
// @Success 200 {string} string "The downloaded file path"
|
||||
// @router /downloadCLI [post]
|
||||
func DownloadCLI() error {
|
||||
return downloadCLI()
|
||||
}
|
||||
|
||||
// @Title InitCLIDownloader
|
||||
// @Description Initialize CLI downloader and start update scheduler
|
||||
func InitCLIDownloader() {
|
||||
if !web.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
return
|
||||
}
|
||||
|
||||
util.SafeGoroutine(func() {
|
||||
err := DownloadCLI()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to initialize CLI downloader: %v\n", err)
|
||||
}
|
||||
|
||||
ScheduleCLIUpdater()
|
||||
})
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
xormadapter "github.com/casdoor/xorm-adapter/v3"
|
||||
@@ -32,13 +32,13 @@ import (
|
||||
// @Success 200 {array} object.Enforcer
|
||||
// @router /get-enforcers [get]
|
||||
func (c *ApiController) GetEnforcers() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
enforcers, err := object.GetEnforcers(owner)
|
||||
@@ -56,7 +56,7 @@ func (c *ApiController) GetEnforcers() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
enforcers, err := object.GetPaginationEnforcers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -75,8 +75,8 @@ func (c *ApiController) GetEnforcers() {
|
||||
// @Success 200 {object} object.Enforcer
|
||||
// @router /get-enforcer [get]
|
||||
func (c *ApiController) GetEnforcer() {
|
||||
id := c.Input().Get("id")
|
||||
loadModelCfg := c.Input().Get("loadModelCfg")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
loadModelCfg := c.Ctx.Input.Query("loadModelCfg")
|
||||
|
||||
enforcer, err := object.GetEnforcer(id)
|
||||
if err != nil {
|
||||
@@ -84,10 +84,12 @@ func (c *ApiController) GetEnforcer() {
|
||||
return
|
||||
}
|
||||
|
||||
if loadModelCfg == "true" && enforcer.Model != "" {
|
||||
err := enforcer.LoadModelCfg()
|
||||
if err != nil {
|
||||
return
|
||||
if enforcer != nil {
|
||||
if loadModelCfg == "true" && enforcer.Model != "" {
|
||||
err = enforcer.LoadModelCfg()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +105,7 @@ func (c *ApiController) GetEnforcer() {
|
||||
// @Success 200 {object} object.Enforcer
|
||||
// @router /update-enforcer [post]
|
||||
func (c *ApiController) UpdateEnforcer() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
enforcer := object.Enforcer{}
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &enforcer)
|
||||
@@ -154,9 +156,17 @@ func (c *ApiController) DeleteEnforcer() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetPolicies
|
||||
// @Title GetPolicies
|
||||
// @Tag Enforcer API
|
||||
// @Description get policies
|
||||
// @Param id query string true "The id ( owner/name ) of enforcer"
|
||||
// @Param adapterId query string false "The adapter id"
|
||||
// @Success 200 {array} xormadapter.CasbinRule
|
||||
// @router /get-policies [get]
|
||||
func (c *ApiController) GetPolicies() {
|
||||
id := c.Input().Get("id")
|
||||
adapterId := c.Input().Get("adapterId")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
adapterId := c.Ctx.Input.Query("adapterId")
|
||||
|
||||
if adapterId != "" {
|
||||
adapter, err := object.GetAdapter(adapterId)
|
||||
@@ -165,7 +175,7 @@ func (c *ApiController) GetPolicies() {
|
||||
return
|
||||
}
|
||||
if adapter == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("the adapter: %s is not found"), adapterId))
|
||||
c.ResponseError(fmt.Sprintf(c.T("enforcer:the adapter: %s is not found"), adapterId))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -188,8 +198,43 @@ func (c *ApiController) GetPolicies() {
|
||||
c.ResponseOk(policies)
|
||||
}
|
||||
|
||||
// GetFilteredPolicies
|
||||
// @Title GetFilteredPolicies
|
||||
// @Tag Enforcer API
|
||||
// @Description get filtered policies with support for multiple filters via POST body
|
||||
// @Param id query string true "The id ( owner/name ) of enforcer"
|
||||
// @Param body body []object.Filter true "Array of filter objects for multiple filters"
|
||||
// @Success 200 {array} xormadapter.CasbinRule
|
||||
// @router /get-filtered-policies [post]
|
||||
func (c *ApiController) GetFilteredPolicies() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var filters []object.Filter
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &filters)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
filteredPolicies, err := object.GetFilteredPoliciesMulti(id, filters)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(filteredPolicies)
|
||||
}
|
||||
|
||||
// UpdatePolicy
|
||||
// @Title UpdatePolicy
|
||||
// @Tag Enforcer API
|
||||
// @Description update policy
|
||||
// @Param id query string true "The id ( owner/name ) of enforcer"
|
||||
// @Param body body []xormadapter.CasbinRule true "Array containing old and new policy"
|
||||
// @Success 200 {object} Response
|
||||
// @router /update-policy [post]
|
||||
func (c *ApiController) UpdatePolicy() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var policies []xormadapter.CasbinRule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &policies)
|
||||
@@ -207,8 +252,16 @@ func (c *ApiController) UpdatePolicy() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddPolicy
|
||||
// @Title AddPolicy
|
||||
// @Tag Enforcer API
|
||||
// @Description add policy
|
||||
// @Param id query string true "The id ( owner/name ) of enforcer"
|
||||
// @Param body body xormadapter.CasbinRule true "The policy to add"
|
||||
// @Success 200 {object} Response
|
||||
// @router /add-policy [post]
|
||||
func (c *ApiController) AddPolicy() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var policy xormadapter.CasbinRule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &policy)
|
||||
@@ -226,8 +279,16 @@ func (c *ApiController) AddPolicy() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// RemovePolicy
|
||||
// @Title RemovePolicy
|
||||
// @Tag Enforcer API
|
||||
// @Description remove policy
|
||||
// @Param id query string true "The id ( owner/name ) of enforcer"
|
||||
// @Param body body xormadapter.CasbinRule true "The policy to remove"
|
||||
// @Success 200 {object} Response
|
||||
// @router /remove-policy [post]
|
||||
func (c *ApiController) RemovePolicy() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var policy xormadapter.CasbinRule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &policy)
|
||||
|
||||
@@ -33,8 +33,8 @@ import (
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /faceid-signin-begin [get]
|
||||
func (c *ApiController) FaceIDSigninBegin() {
|
||||
userOwner := c.Input().Get("owner")
|
||||
userName := c.Input().Get("name")
|
||||
userOwner := c.Ctx.Input.Query("owner")
|
||||
userName := c.Ctx.Input.Query("name")
|
||||
|
||||
user, err := object.GetUserByFields(userOwner, userName)
|
||||
if err != nil {
|
||||
|
||||
175
controllers/form.go
Normal file
175
controllers/form.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetGlobalForms
|
||||
// @Title GetGlobalForms
|
||||
// @Tag Form API
|
||||
// @Description get global forms
|
||||
// @Success 200 {array} object.Form The Response object
|
||||
// @router /get-global-forms [get]
|
||||
func (c *ApiController) GetGlobalForms() {
|
||||
forms, err := object.GetGlobalForms()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedForms(forms, true))
|
||||
}
|
||||
|
||||
// GetForms
|
||||
// @Title GetForms
|
||||
// @Tag Form API
|
||||
// @Description get forms
|
||||
// @Param owner query string true "The owner of form"
|
||||
// @Success 200 {array} object.Form The Response object
|
||||
// @router /get-forms [get]
|
||||
func (c *ApiController) GetForms() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
forms, err := object.GetForms(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedForms(forms, true))
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetFormCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
forms, err := object.GetPaginationForms(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(forms, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetForm
|
||||
// @Title GetForm
|
||||
// @Tag Form API
|
||||
// @Description get form
|
||||
// @Param id query string true "The id (owner/name) of form"
|
||||
// @Success 200 {object} object.Form The Response object
|
||||
// @router /get-form [get]
|
||||
func (c *ApiController) GetForm() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
form, err := object.GetForm(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedForm(form, true))
|
||||
}
|
||||
|
||||
// UpdateForm
|
||||
// @Title UpdateForm
|
||||
// @Tag Form API
|
||||
// @Description update form
|
||||
// @Param id query string true "The id (owner/name) of the form"
|
||||
// @Param body body object.Form true "The details of the form"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-form [post]
|
||||
func (c *ApiController) UpdateForm() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var form object.Form
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
success, err := object.UpdateForm(id, &form)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(success)
|
||||
}
|
||||
|
||||
// AddForm
|
||||
// @Title AddForm
|
||||
// @Tag Form API
|
||||
// @Description add form
|
||||
// @Param body body object.Form true "The details of the form"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-form [post]
|
||||
func (c *ApiController) AddForm() {
|
||||
var form object.Form
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
success, err := object.AddForm(&form)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(success)
|
||||
}
|
||||
|
||||
// DeleteForm
|
||||
// @Title DeleteForm
|
||||
// @Tag Form API
|
||||
// @Description delete form
|
||||
// @Param body body object.Form true "The details of the form"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-form [post]
|
||||
func (c *ApiController) DeleteForm() {
|
||||
var form object.Form
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
success, err := object.DeleteForm(&form)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(success)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import "github.com/casdoor/casdoor/object"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-dashboard [get]
|
||||
func (c *ApiController) GetDashboard() {
|
||||
owner := c.Input().Get("owner")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
data, err := object.GetDashboard(owner)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,8 +15,9 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -29,14 +30,14 @@ import (
|
||||
// @Success 200 {array} object.Group The Response object
|
||||
// @router /get-groups [get]
|
||||
func (c *ApiController) GetGroups() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
withTree := c.Input().Get("withTree")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
withTree := c.Ctx.Input.Query("withTree")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
groups, err := object.GetGroups(owner)
|
||||
@@ -65,20 +66,38 @@ func (c *ApiController) GetGroups() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
groups, err := object.GetPaginationGroups(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else {
|
||||
err = object.ExtendGroupsWithUsers(groups)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
groupsHaveChildrenMap, err := object.GetGroupsHaveChildrenMap(groups)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
_, ok := groupsHaveChildrenMap[group.GetId()]
|
||||
if ok {
|
||||
group.HaveChildren = true
|
||||
}
|
||||
|
||||
c.ResponseOk(groups, paginator.Nums())
|
||||
parent, ok := groupsHaveChildrenMap[fmt.Sprintf("%s/%s", group.Owner, group.ParentId)]
|
||||
if ok {
|
||||
group.ParentName = parent.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
err = object.ExtendGroupsWithUsers(groups)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(groups, paginator.Nums())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +109,7 @@ func (c *ApiController) GetGroups() {
|
||||
// @Success 200 {object} object.Group The Response object
|
||||
// @router /get-group [get]
|
||||
func (c *ApiController) GetGroup() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
group, err := object.GetGroup(id)
|
||||
if err != nil {
|
||||
@@ -116,7 +135,7 @@ func (c *ApiController) GetGroup() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-group [post]
|
||||
func (c *ApiController) UpdateGroup() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var group object.Group
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &group)
|
||||
|
||||
60
controllers/group_upload.go
Normal file
60
controllers/group_upload.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func (c *ApiController) UploadGroups() {
|
||||
userId := c.GetSessionUsername()
|
||||
owner, user, err := util.GetOwnerAndNameFromIdWithError(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Ctx.Request.FormFile("file")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
|
||||
path := util.GetUploadXlsxPath(fileId)
|
||||
defer os.Remove(path)
|
||||
|
||||
err = saveFile(path, &file)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := object.UploadGroups(owner, path)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if affected {
|
||||
c.ResponseOk()
|
||||
} else {
|
||||
c.ResponseError(c.T("general:Failed to import groups"))
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,10 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +32,13 @@ import (
|
||||
// @Success 200 {array} object.Invitation The Response object
|
||||
// @router /get-invitations [get]
|
||||
func (c *ApiController) GetInvitations() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
invitations, err := object.GetInvitations(owner)
|
||||
@@ -54,7 +56,7 @@ func (c *ApiController) GetInvitations() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
invitations, err := object.GetPaginationInvitations(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +75,7 @@ func (c *ApiController) GetInvitations() {
|
||||
// @Success 200 {object} object.Invitation The Response object
|
||||
// @router /get-invitation [get]
|
||||
func (c *ApiController) GetInvitation() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
invitation, err := object.GetInvitation(id)
|
||||
if err != nil {
|
||||
@@ -92,14 +94,18 @@ func (c *ApiController) GetInvitation() {
|
||||
// @Success 200 {object} object.Invitation The Response object
|
||||
// @router /get-invitation-info [get]
|
||||
func (c *ApiController) GetInvitationCodeInfo() {
|
||||
code := c.Input().Get("code")
|
||||
applicationId := c.Input().Get("applicationId")
|
||||
code := c.Ctx.Input.Query("code")
|
||||
applicationId := c.Ctx.Input.Query("applicationId")
|
||||
|
||||
application, err := object.GetApplication(applicationId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), applicationId))
|
||||
return
|
||||
}
|
||||
|
||||
invitation, msg := object.GetInvitationByCode(code, application.Organization, c.GetAcceptLanguage())
|
||||
if msg != "" {
|
||||
@@ -119,7 +125,7 @@ func (c *ApiController) GetInvitationCodeInfo() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-invitation [post]
|
||||
func (c *ApiController) UpdateInvitation() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var invitation object.Invitation
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &invitation)
|
||||
@@ -178,7 +184,7 @@ func (c *ApiController) DeleteInvitation() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /verify-invitation [get]
|
||||
func (c *ApiController) VerifyInvitation() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
payment, attachInfo, err := object.VerifyInvitation(id)
|
||||
if err != nil {
|
||||
@@ -188,3 +194,90 @@ func (c *ApiController) VerifyInvitation() {
|
||||
|
||||
c.ResponseOk(payment, attachInfo)
|
||||
}
|
||||
|
||||
// SendInvitation
|
||||
// @Title VerifyInvitation
|
||||
// @Tag Invitation API
|
||||
// @Description verify invitation
|
||||
// @Param id query string true "The id ( owner/name ) of the invitation"
|
||||
// @Param body body []string true "The details of the invitation"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /send-invitation [post]
|
||||
func (c *ApiController) SendInvitation() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var destinations []string
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &destinations)
|
||||
|
||||
if !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
invitation, err := object.GetInvitation(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if invitation == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("invitation:Invitation %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
|
||||
organization, err := object.GetOrganization(fmt.Sprintf("admin/%s", invitation.Owner))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if organization == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The organization: %s does not exist"), invitation.Owner))
|
||||
return
|
||||
}
|
||||
|
||||
var application *object.Application
|
||||
if invitation.Application != "" {
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s-org-%s", invitation.Application, invitation.Owner))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
application, err = object.GetApplicationByOrganizationName(invitation.Owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The organization: %s should have one application at least"), invitation.Owner))
|
||||
return
|
||||
}
|
||||
|
||||
if application.IsShared {
|
||||
application.Name = fmt.Sprintf("%s-org-%s", application.Name, invitation.Owner)
|
||||
}
|
||||
|
||||
provider, err := application.GetEmailProvider("Invitation")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("verification:please add an Email provider to the \"Providers\" list for the application: %s"), invitation.Owner))
|
||||
return
|
||||
}
|
||||
|
||||
content := provider.Metadata
|
||||
|
||||
content = strings.ReplaceAll(content, "%code", invitation.Code)
|
||||
content = strings.ReplaceAll(content, "%link", invitation.GetInvitationLink(c.Ctx.Request.Host, application.Name))
|
||||
|
||||
err = object.SendEmail(provider, provider.Title, content, destinations, organization.DisplayName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -27,10 +28,10 @@ type LdapResp struct {
|
||||
ExistUuids []string `json:"existUuids"`
|
||||
}
|
||||
|
||||
//type LdapRespGroup struct {
|
||||
// type LdapRespGroup struct {
|
||||
// GroupId string
|
||||
// GroupName string
|
||||
//}
|
||||
// }
|
||||
|
||||
type LdapSyncResp struct {
|
||||
Exist []object.LdapUser `json:"exist"`
|
||||
@@ -45,14 +46,22 @@ type LdapSyncResp struct {
|
||||
// @Success 200 {object} controllers.LdapResp The Response object
|
||||
// @router /get-ldap-users [get]
|
||||
func (c *ApiController) GetLdapUsers() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
_, ldapId := util.GetOwnerAndNameFromId(id)
|
||||
_, ldapId, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
ldapServer, err := object.GetLdap(ldapId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if ldapServer == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The LDAP: %s does not exist"), ldapId))
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := ldapServer.GetLdapConn()
|
||||
if err != nil {
|
||||
@@ -61,18 +70,18 @@ func (c *ApiController) GetLdapUsers() {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
//groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
|
||||
//if err != nil {
|
||||
// groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
|
||||
// if err != nil {
|
||||
// c.ResponseError(err.Error())
|
||||
// return
|
||||
//}
|
||||
// }
|
||||
|
||||
//for _, group := range groupsMap {
|
||||
// for _, group := range groupsMap {
|
||||
// resp.Groups = append(resp.Groups, LdapRespGroup{
|
||||
// GroupId: group.GidNumber,
|
||||
// GroupName: group.Cn,
|
||||
// })
|
||||
//}
|
||||
// }
|
||||
|
||||
users, err := conn.GetLdapUsers(ldapServer)
|
||||
if err != nil {
|
||||
@@ -105,7 +114,7 @@ func (c *ApiController) GetLdapUsers() {
|
||||
// @Success 200 {array} object.Ldap The Response object
|
||||
// @router /get-ldaps [get]
|
||||
func (c *ApiController) GetLdaps() {
|
||||
owner := c.Input().Get("owner")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
c.ResponseOk(object.GetMaskedLdaps(object.GetLdaps(owner)))
|
||||
}
|
||||
@@ -118,14 +127,18 @@ func (c *ApiController) GetLdaps() {
|
||||
// @Success 200 {object} object.Ldap The Response object
|
||||
// @router /get-ldap [get]
|
||||
func (c *ApiController) GetLdap() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
if util.IsStringsEmpty(id) {
|
||||
c.ResponseError(c.T("general:Missing parameter"))
|
||||
return
|
||||
}
|
||||
|
||||
_, name := util.GetOwnerAndNameFromId(id)
|
||||
_, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
ldap, err := object.GetLdap(name)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -253,11 +266,15 @@ func (c *ApiController) DeleteLdap() {
|
||||
// @Success 200 {object} controllers.LdapSyncResp The Response object
|
||||
// @router /sync-ldap-users [post]
|
||||
func (c *ApiController) SyncLdapUsers() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
owner, ldapId := util.GetOwnerAndNameFromId(id)
|
||||
owner, ldapId, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
var users []object.LdapUser
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &users)
|
||||
err = json.Unmarshal(c.Ctx.Input.RequestBody, &users)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -269,7 +286,11 @@ func (c *ApiController) SyncLdapUsers() {
|
||||
return
|
||||
}
|
||||
|
||||
exist, failed, _ := object.SyncLdapUsers(owner, users, ldapId)
|
||||
exist, failed, err := object.SyncLdapUsers(owner, users, ldapId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(&LdapSyncResp{
|
||||
Exist: exist,
|
||||
|
||||
@@ -22,13 +22,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
MfaRecoveryCodesSession = "mfa_recovery_codes"
|
||||
MfaCountryCodeSession = "mfa_country_code"
|
||||
MfaDestSession = "mfa_dest"
|
||||
MfaTotpSecretSession = "mfa_totp_secret"
|
||||
)
|
||||
|
||||
// MfaSetupInitiate
|
||||
// @Title MfaSetupInitiate
|
||||
// @Tag MFA API
|
||||
@@ -65,19 +58,28 @@ func (c *ApiController) MfaSetupInitiate() {
|
||||
return
|
||||
}
|
||||
|
||||
mfaProps, err := MfaUtil.Initiate(user.GetId())
|
||||
organization, err := object.GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
issuer := ""
|
||||
if organization != nil && organization.DisplayName != "" {
|
||||
issuer = organization.DisplayName
|
||||
} else if organization != nil {
|
||||
issuer = organization.Name
|
||||
}
|
||||
|
||||
mfaProps, err := MfaUtil.Initiate(user.GetId(), issuer)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
recoveryCode := uuid.NewString()
|
||||
c.SetSession(MfaRecoveryCodesSession, recoveryCode)
|
||||
if mfaType == object.TotpType {
|
||||
c.SetSession(MfaTotpSecretSession, mfaProps.Secret)
|
||||
}
|
||||
|
||||
mfaProps.RecoveryCodes = []string{recoveryCode}
|
||||
mfaProps.MfaRememberInHours = organization.MfaRememberInHours
|
||||
|
||||
resp := mfaProps
|
||||
c.ResponseOk(resp)
|
||||
@@ -94,6 +96,9 @@ func (c *ApiController) MfaSetupInitiate() {
|
||||
func (c *ApiController) MfaSetupVerify() {
|
||||
mfaType := c.Ctx.Request.Form.Get("mfaType")
|
||||
passcode := c.Ctx.Request.Form.Get("passcode")
|
||||
secret := c.Ctx.Request.Form.Get("secret")
|
||||
dest := c.Ctx.Request.Form.Get("dest")
|
||||
countryCode := c.Ctx.Request.Form.Get("countryCode")
|
||||
|
||||
if mfaType == "" || passcode == "" {
|
||||
c.ResponseError("missing auth type or passcode")
|
||||
@@ -104,32 +109,50 @@ func (c *ApiController) MfaSetupVerify() {
|
||||
MfaType: mfaType,
|
||||
}
|
||||
if mfaType == object.TotpType {
|
||||
secret := c.GetSession(MfaTotpSecretSession)
|
||||
if secret == nil {
|
||||
if secret == "" {
|
||||
c.ResponseError("totp secret is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = secret.(string)
|
||||
config.Secret = secret
|
||||
} else if mfaType == object.SmsType {
|
||||
dest := c.GetSession(MfaDestSession)
|
||||
if dest == nil {
|
||||
if dest == "" {
|
||||
c.ResponseError("destination is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = dest.(string)
|
||||
countryCode := c.GetSession(MfaCountryCodeSession)
|
||||
if countryCode == nil {
|
||||
config.Secret = dest
|
||||
if countryCode == "" {
|
||||
c.ResponseError("country code is missing")
|
||||
return
|
||||
}
|
||||
config.CountryCode = countryCode.(string)
|
||||
config.CountryCode = countryCode
|
||||
} else if mfaType == object.EmailType {
|
||||
dest := c.GetSession(MfaDestSession)
|
||||
if dest == nil {
|
||||
if dest == "" {
|
||||
c.ResponseError("destination is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = dest.(string)
|
||||
config.Secret = dest
|
||||
} else if mfaType == object.RadiusType {
|
||||
if dest == "" {
|
||||
c.ResponseError("RADIUS username is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = dest
|
||||
if secret == "" {
|
||||
c.ResponseError("RADIUS provider is missing")
|
||||
return
|
||||
}
|
||||
config.URL = secret
|
||||
} else if mfaType == object.PushType {
|
||||
if dest == "" {
|
||||
c.ResponseError("push notification receiver is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = dest
|
||||
if secret == "" {
|
||||
c.ResponseError("push notification provider is missing")
|
||||
return
|
||||
}
|
||||
config.URL = secret
|
||||
}
|
||||
|
||||
mfaUtil := object.GetMfaUtil(mfaType, config)
|
||||
@@ -159,6 +182,10 @@ func (c *ApiController) MfaSetupEnable() {
|
||||
owner := c.Ctx.Request.Form.Get("owner")
|
||||
name := c.Ctx.Request.Form.Get("name")
|
||||
mfaType := c.Ctx.Request.Form.Get("mfaType")
|
||||
secret := c.Ctx.Request.Form.Get("secret")
|
||||
dest := c.Ctx.Request.Form.Get("dest")
|
||||
countryCode := c.Ctx.Request.Form.Get("secret")
|
||||
recoveryCodes := c.Ctx.Request.Form.Get("recoveryCodes")
|
||||
|
||||
user, err := object.GetUser(util.GetId(owner, name))
|
||||
if err != nil {
|
||||
@@ -176,43 +203,61 @@ func (c *ApiController) MfaSetupEnable() {
|
||||
}
|
||||
|
||||
if mfaType == object.TotpType {
|
||||
secret := c.GetSession(MfaTotpSecretSession)
|
||||
if secret == nil {
|
||||
if secret == "" {
|
||||
c.ResponseError("totp secret is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = secret.(string)
|
||||
config.Secret = secret
|
||||
} else if mfaType == object.EmailType {
|
||||
if user.Email == "" {
|
||||
dest := c.GetSession(MfaDestSession)
|
||||
if dest == nil {
|
||||
if dest == "" {
|
||||
c.ResponseError("destination is missing")
|
||||
return
|
||||
}
|
||||
user.Email = dest.(string)
|
||||
user.Email = dest
|
||||
}
|
||||
} else if mfaType == object.SmsType {
|
||||
if user.Phone == "" {
|
||||
dest := c.GetSession(MfaDestSession)
|
||||
if dest == nil {
|
||||
if dest == "" {
|
||||
c.ResponseError("destination is missing")
|
||||
return
|
||||
}
|
||||
user.Phone = dest.(string)
|
||||
countryCode := c.GetSession(MfaCountryCodeSession)
|
||||
if countryCode == nil {
|
||||
user.Phone = dest
|
||||
if countryCode == "" {
|
||||
c.ResponseError("country code is missing")
|
||||
return
|
||||
}
|
||||
user.CountryCode = countryCode.(string)
|
||||
user.CountryCode = countryCode
|
||||
}
|
||||
} else if mfaType == object.RadiusType {
|
||||
if dest == "" {
|
||||
c.ResponseError("RADIUS username is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = dest
|
||||
if secret == "" {
|
||||
c.ResponseError("RADIUS provider is missing")
|
||||
return
|
||||
}
|
||||
config.URL = secret
|
||||
} else if mfaType == object.PushType {
|
||||
if dest == "" {
|
||||
c.ResponseError("push notification receiver is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = dest
|
||||
if secret == "" {
|
||||
c.ResponseError("push notification provider is missing")
|
||||
return
|
||||
}
|
||||
config.URL = secret
|
||||
}
|
||||
recoveryCodes := c.GetSession(MfaRecoveryCodesSession)
|
||||
if recoveryCodes == nil {
|
||||
|
||||
if recoveryCodes == "" {
|
||||
c.ResponseError("recovery codes is missing")
|
||||
return
|
||||
}
|
||||
config.RecoveryCodes = []string{recoveryCodes.(string)}
|
||||
config.RecoveryCodes = []string{recoveryCodes}
|
||||
|
||||
mfaUtil := object.GetMfaUtil(mfaType, config)
|
||||
if mfaUtil == nil {
|
||||
@@ -226,14 +271,6 @@ func (c *ApiController) MfaSetupEnable() {
|
||||
return
|
||||
}
|
||||
|
||||
c.DelSession(MfaRecoveryCodesSession)
|
||||
if mfaType == object.TotpType {
|
||||
c.DelSession(MfaTotpSecretSession)
|
||||
} else {
|
||||
c.DelSession(MfaCountryCodeSession)
|
||||
c.DelSession(MfaDestSession)
|
||||
}
|
||||
|
||||
c.ResponseOk(http.StatusText(http.StatusOK))
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Model The Response object
|
||||
// @router /get-models [get]
|
||||
func (c *ApiController) GetModels() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
models, err := object.GetModels(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetModels() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
models, err := object.GetPaginationModels(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +73,7 @@ func (c *ApiController) GetModels() {
|
||||
// @Success 200 {object} object.Model The Response object
|
||||
// @router /get-model [get]
|
||||
func (c *ApiController) GetModel() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
model, err := object.GetModel(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) GetModel() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-model [post]
|
||||
func (c *ApiController) UpdateModel() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var model object.Model
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
|
||||
|
||||
74
controllers/oauth_dcr.go
Normal file
74
controllers/oauth_dcr.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
// DynamicClientRegister
|
||||
// @Title DynamicClientRegister
|
||||
// @Tag OAuth API
|
||||
// @Description Register a new OAuth 2.0 client dynamically (RFC 7591)
|
||||
// @Param organization query string false "The organization name (defaults to built-in)"
|
||||
// @Param body body object.DynamicClientRegistrationRequest true "Client registration request"
|
||||
// @Success 201 {object} object.DynamicClientRegistrationResponse
|
||||
// @Failure 400 {object} object.DcrError
|
||||
// @router /api/oauth/register [post]
|
||||
func (c *ApiController) DynamicClientRegister() {
|
||||
var req object.DynamicClientRegistrationRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
|
||||
if err != nil {
|
||||
c.Ctx.Output.Status = http.StatusBadRequest
|
||||
c.Data["json"] = object.DcrError{
|
||||
Error: "invalid_client_metadata",
|
||||
ErrorDescription: "invalid request body: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// Get organization from query parameter or default to built-in
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
if organization == "" {
|
||||
organization = "built-in"
|
||||
}
|
||||
|
||||
// Register the client
|
||||
response, dcrErr, err := object.RegisterDynamicClient(&req, organization)
|
||||
if err != nil {
|
||||
c.Ctx.Output.Status = http.StatusInternalServerError
|
||||
c.Data["json"] = object.DcrError{
|
||||
Error: "server_error",
|
||||
ErrorDescription: err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if dcrErr != nil {
|
||||
c.Ctx.Output.Status = http.StatusBadRequest
|
||||
c.Data["json"] = dcrErr
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// Return 201 Created
|
||||
c.Ctx.Output.Status = http.StatusCreated
|
||||
c.Data["json"] = response
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
// GetOidcDiscovery
|
||||
// @Title GetOidcDiscovery
|
||||
// @Tag OIDC API
|
||||
// @Description Get Oidc Discovery
|
||||
// @Success 200 {object} object.OidcDiscovery
|
||||
// @router /.well-known/openid-configuration [get]
|
||||
func (c *RootController) GetOidcDiscovery() {
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOidcDiscovery(host)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetJwks
|
||||
// @Title GetJwks
|
||||
// @Tag OIDC API
|
||||
// @Success 200 {object} jose.JSONWebKey
|
||||
// @router /.well-known/jwks [get]
|
||||
func (c *RootController) GetJwks() {
|
||||
jwks, err := object.GetJsonWebKeySet()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = jwks
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetWebFinger
|
||||
// @Title GetWebFinger
|
||||
// @Tag OIDC API
|
||||
// @Param resource query string true "resource"
|
||||
// @Success 200 {object} object.WebFinger
|
||||
// @router /.well-known/webfinger [get]
|
||||
func (c *RootController) GetWebFinger() {
|
||||
resource := c.Input().Get("resource")
|
||||
rels := []string{}
|
||||
host := c.Ctx.Request.Host
|
||||
|
||||
for key, value := range c.Input() {
|
||||
if strings.HasPrefix(key, "rel") {
|
||||
rels = append(rels, value...)
|
||||
}
|
||||
}
|
||||
|
||||
webfinger, err := object.GetWebFinger(resource, rels, host)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = webfinger
|
||||
c.Ctx.Output.ContentType("application/jrd+json")
|
||||
c.ServeJSON()
|
||||
}
|
||||
195
controllers/order.go
Normal file
195
controllers/order.go
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetOrders
|
||||
// @Title GetOrders
|
||||
// @Tag Order API
|
||||
// @Description get orders
|
||||
// @Param owner query string true "The owner of orders"
|
||||
// @Success 200 {array} object.Order The Response object
|
||||
// @router /get-orders [get]
|
||||
func (c *ApiController) GetOrders() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
var orders []*object.Order
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
orders, err = object.GetUserOrders(owner, value)
|
||||
} else {
|
||||
orders, err = object.GetOrders(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
orders, err = object.GetUserOrders(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(orders)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
count, err := object.GetOrderCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
orders, err := object.GetPaginationOrders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(orders, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserOrders
|
||||
// @Title GetUserOrders
|
||||
// @Tag Order API
|
||||
// @Description get orders for a user
|
||||
// @Param owner query string true "The owner of orders"
|
||||
// @Param user query string true "The username of the user"
|
||||
// @Success 200 {array} object.Order The Response object
|
||||
// @router /get-user-orders [get]
|
||||
func (c *ApiController) GetUserOrders() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
user := c.Ctx.Input.Query("user")
|
||||
|
||||
orders, err := object.GetUserOrders(owner, user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(orders)
|
||||
}
|
||||
|
||||
// GetOrder
|
||||
// @Title GetOrder
|
||||
// @Tag Order API
|
||||
// @Description get order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Success 200 {object} object.Order The Response object
|
||||
// @router /get-order [get]
|
||||
func (c *ApiController) GetOrder() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(order)
|
||||
}
|
||||
|
||||
// UpdateOrder
|
||||
// @Title UpdateOrder
|
||||
// @Tag Order API
|
||||
// @Description update order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Param body body object.Order true "The details of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-order [post]
|
||||
func (c *ApiController) UpdateOrder() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrder(id, &order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddOrder
|
||||
// @Title AddOrder
|
||||
// @Tag Order API
|
||||
// @Description add order
|
||||
// @Param body body object.Order true "The details of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-order [post]
|
||||
func (c *ApiController) AddOrder() {
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddOrder(&order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteOrder
|
||||
// @Title DeleteOrder
|
||||
// @Tag Order API
|
||||
// @Description delete order
|
||||
// @Param body body object.Order true "The details of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-order [post]
|
||||
func (c *ApiController) DeleteOrder() {
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteOrder(&order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
160
controllers/order_pay.go
Normal file
160
controllers/order_pay.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// PlaceOrder
|
||||
// @Title PlaceOrder
|
||||
// @Tag Order API
|
||||
// @Description place an order for a product
|
||||
// @Param productId query string true "The id ( owner/name ) of the product"
|
||||
// @Param pricingName query string false "The name of the pricing (for subscription)"
|
||||
// @Param planName query string false "The name of the plan (for subscription)"
|
||||
// @Param customPrice query number false "Custom price for recharge products"
|
||||
// @Param userName query string false "The username to place order for (admin only)"
|
||||
// @Success 200 {object} object.Order The Response object
|
||||
// @router /place-order [post]
|
||||
func (c *ApiController) PlaceOrder() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
paidUserName := c.Ctx.Input.Query("userName")
|
||||
|
||||
var req struct {
|
||||
ProductInfos []object.ProductInfo `json:"productInfos"`
|
||||
}
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
productInfos := req.ProductInfos
|
||||
if len(productInfos) == 0 {
|
||||
c.ResponseError(c.T("product:Product list cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
var userId string
|
||||
if paidUserName != "" {
|
||||
userId = util.GetId(owner, paidUserName)
|
||||
if userId != c.GetSessionUsername() && !c.IsAdmin() && userId != c.GetPaidUsername() {
|
||||
c.ResponseError(c.T("general:Only admin user can specify user"))
|
||||
return
|
||||
}
|
||||
|
||||
c.SetSession("paidUsername", "")
|
||||
} else {
|
||||
userId = c.GetSessionUsername()
|
||||
}
|
||||
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return
|
||||
}
|
||||
|
||||
order, err := object.PlaceOrder(owner, productInfos, user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(order)
|
||||
}
|
||||
|
||||
// PayOrder
|
||||
// @Title PayOrder
|
||||
// @Tag Order API
|
||||
// @Description pay an existing order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Param providerName query string true "The name of the provider"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /pay-order [post]
|
||||
func (c *ApiController) PayOrder() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
host := c.Ctx.Request.Host
|
||||
providerName := c.Ctx.Input.Query("providerName")
|
||||
paymentEnv := c.Ctx.Input.Query("paymentEnv")
|
||||
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if order == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The order: %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.GetSessionUsername()
|
||||
orderUserId := util.GetId(order.Owner, order.User)
|
||||
if userId != orderUserId && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
payment, attachInfo, err := object.PayOrder(providerName, host, paymentEnv, order, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(payment, attachInfo)
|
||||
}
|
||||
|
||||
// CancelOrder
|
||||
// @Title CancelOrder
|
||||
// @Tag Order API
|
||||
// @Description cancel an order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /cancel-order [post]
|
||||
func (c *ApiController) CancelOrder() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if order == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The order: %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.GetSessionUsername()
|
||||
orderUserId := util.GetId(order.Owner, order.User)
|
||||
if userId != orderUserId && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.CancelOrder(order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,14 +30,14 @@ import (
|
||||
// @Success 200 {array} object.Organization The Response object
|
||||
// @router /get-organizations [get]
|
||||
func (c *ApiController) GetOrganizations() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organizationName := c.Input().Get("organizationName")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organizationName := c.Ctx.Input.Query("organizationName")
|
||||
|
||||
isGlobalAdmin := c.IsGlobalAdmin()
|
||||
if limit == "" || page == "" {
|
||||
@@ -71,7 +71,7 @@ func (c *ApiController) GetOrganizations() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
organizations, err := object.GetMaskedOrganizations(object.GetPaginationOrganizations(owner, organizationName, paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -91,13 +91,17 @@ func (c *ApiController) GetOrganizations() {
|
||||
// @Success 200 {object} object.Organization The Response object
|
||||
// @router /get-organization [get]
|
||||
func (c *ApiController) GetOrganization() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
organization, err := object.GetMaskedOrganization(object.GetOrganization(id))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if organization != nil && organization.MfaRememberInHours == 0 {
|
||||
organization.MfaRememberInHours = 12
|
||||
}
|
||||
|
||||
c.ResponseOk(organization)
|
||||
}
|
||||
|
||||
@@ -110,7 +114,7 @@ func (c *ApiController) GetOrganization() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-organization [post]
|
||||
func (c *ApiController) UpdateOrganization() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var organization object.Organization
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization)
|
||||
@@ -124,7 +128,13 @@ func (c *ApiController) UpdateOrganization() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
|
||||
isGlobalAdmin, _ := c.isGlobalAdmin()
|
||||
|
||||
if organization.BalanceCurrency == "" {
|
||||
organization.BalanceCurrency = "USD"
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -159,6 +169,10 @@ func (c *ApiController) AddOrganization() {
|
||||
return
|
||||
}
|
||||
|
||||
if organization.BalanceCurrency == "" {
|
||||
organization.BalanceCurrency = "USD"
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -191,7 +205,7 @@ func (c *ApiController) DeleteOrganization() {
|
||||
// @router /get-default-application [get]
|
||||
func (c *ApiController) GetDefaultApplication() {
|
||||
userId := c.GetSessionUsername()
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
application, err := object.GetDefaultApplication(id)
|
||||
if err != nil {
|
||||
@@ -211,7 +225,7 @@ func (c *ApiController) GetDefaultApplication() {
|
||||
// @Success 200 {array} object.Organization The Response object
|
||||
// @router /get-organization-names [get]
|
||||
func (c *ApiController) GetOrganizationNames() {
|
||||
owner := c.Input().Get("owner")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
organizationNames, err := object.GetOrganizationsByFields(owner, []string{"name", "display_name"}...)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,16 +30,35 @@ import (
|
||||
// @Success 200 {array} object.Payment The Response object
|
||||
// @router /get-payments [get]
|
||||
func (c *ApiController) GetPayments() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
payments, err := object.GetPayments(owner)
|
||||
var payments []*object.Payment
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
payments, err = object.GetUserPayments(owner, value)
|
||||
} else {
|
||||
payments, err = object.GetPayments(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
payments, err = object.GetUserPayments(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,13 +67,23 @@ func (c *ApiController) GetPayments() {
|
||||
c.ResponseOk(payments)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
count, err := object.GetPaymentCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
payments, err := object.GetPaginationPayments(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -75,8 +104,8 @@ func (c *ApiController) GetPayments() {
|
||||
// @Success 200 {array} object.Payment The Response object
|
||||
// @router /get-user-payments [get]
|
||||
func (c *ApiController) GetUserPayments() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
user := c.Ctx.Input.Query("user")
|
||||
|
||||
payments, err := object.GetUserPayments(owner, user)
|
||||
if err != nil {
|
||||
@@ -95,7 +124,7 @@ func (c *ApiController) GetUserPayments() {
|
||||
// @Success 200 {object} object.Payment The Response object
|
||||
// @router /get-payment [get]
|
||||
func (c *ApiController) GetPayment() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
payment, err := object.GetPayment(id)
|
||||
if err != nil {
|
||||
@@ -115,7 +144,7 @@ func (c *ApiController) GetPayment() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-payment [post]
|
||||
func (c *ApiController) UpdatePayment() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var payment object.Payment
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &payment)
|
||||
@@ -179,7 +208,7 @@ func (c *ApiController) NotifyPayment() {
|
||||
|
||||
body := c.Ctx.Input.RequestBody
|
||||
|
||||
payment, err := object.NotifyPayment(body, owner, paymentName)
|
||||
payment, err := object.NotifyPayment(body, owner, paymentName, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -196,7 +225,7 @@ func (c *ApiController) NotifyPayment() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /invoice-payment [post]
|
||||
func (c *ApiController) InvoicePayment() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
payment, err := object.GetPayment(id)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Permission The Response object
|
||||
// @router /get-permissions [get]
|
||||
func (c *ApiController) GetPermissions() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
permissions, err := object.GetPermissions(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetPermissions() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
permissions, err := object.GetPaginationPermissions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -94,7 +94,7 @@ func (c *ApiController) GetPermissionsBySubmitter() {
|
||||
// @Success 200 {array} object.Permission The Response object
|
||||
// @router /get-permissions-by-role [get]
|
||||
func (c *ApiController) GetPermissionsByRole() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
permissions, err := object.GetPermissionsByRole(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -112,7 +112,7 @@ func (c *ApiController) GetPermissionsByRole() {
|
||||
// @Success 200 {object} object.Permission The Response object
|
||||
// @router /get-permission [get]
|
||||
func (c *ApiController) GetPermission() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
permission, err := object.GetPermission(id)
|
||||
if err != nil {
|
||||
@@ -132,7 +132,7 @@ func (c *ApiController) GetPermission() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-permission [post]
|
||||
func (c *ApiController) UpdatePermission() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var permission object.Permission
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permission)
|
||||
|
||||
@@ -24,7 +24,11 @@ import (
|
||||
|
||||
func (c *ApiController) UploadPermissions() {
|
||||
userId := c.GetSessionUsername()
|
||||
owner, user := util.GetOwnerAndNameFromId(userId)
|
||||
owner, user, err := util.GetOwnerAndNameFromIdWithError(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Ctx.Request.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -49,6 +53,6 @@ func (c *ApiController) UploadPermissions() {
|
||||
if affected {
|
||||
c.ResponseOk()
|
||||
} else {
|
||||
c.ResponseError(c.T("user_upload:Failed to import users"))
|
||||
c.ResponseError(c.T("general:Failed to import users"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Plan The Response object
|
||||
// @router /get-plans [get]
|
||||
func (c *ApiController) GetPlans() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
plans, err := object.GetPlans(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetPlans() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
plan, err := object.GetPaginatedPlans(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -74,8 +74,8 @@ func (c *ApiController) GetPlans() {
|
||||
// @Success 200 {object} object.Plan The Response object
|
||||
// @router /get-plan [get]
|
||||
func (c *ApiController) GetPlan() {
|
||||
id := c.Input().Get("id")
|
||||
includeOption := c.Input().Get("includeOption") == "true"
|
||||
id := c.Ctx.Input.Query("id")
|
||||
includeOption := c.Ctx.Input.Query("includeOption") == "true"
|
||||
|
||||
plan, err := object.GetPlan(id)
|
||||
if err != nil {
|
||||
@@ -107,7 +107,7 @@ func (c *ApiController) GetPlan() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-plan [post]
|
||||
func (c *ApiController) UpdatePlan() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
owner := util.GetOwnerFromId(id)
|
||||
var plan object.Plan
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Pricing The Response object
|
||||
// @router /get-pricings [get]
|
||||
func (c *ApiController) GetPricings() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
pricings, err := object.GetPricings(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetPricings() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
pricing, err := object.GetPaginatedPricings(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +73,7 @@ func (c *ApiController) GetPricings() {
|
||||
// @Success 200 {object} object.Pricing The Response object
|
||||
// @router /get-pricing [get]
|
||||
func (c *ApiController) GetPricing() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
pricing, err := object.GetPricing(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) GetPricing() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-pricing [post]
|
||||
func (c *ApiController) UpdatePricing() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var pricing object.Pricing
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &pricing)
|
||||
|
||||
@@ -16,10 +16,8 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -32,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Product The Response object
|
||||
// @router /get-products [get]
|
||||
func (c *ApiController) GetProducts() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
products, err := object.GetProducts(owner)
|
||||
@@ -56,7 +54,7 @@ func (c *ApiController) GetProducts() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
products, err := object.GetPaginationProducts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -75,7 +73,7 @@ func (c *ApiController) GetProducts() {
|
||||
// @Success 200 {object} object.Product The Response object
|
||||
// @router /get-product [get]
|
||||
func (c *ApiController) GetProduct() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
product, err := object.GetProduct(id)
|
||||
if err != nil {
|
||||
@@ -101,7 +99,7 @@ func (c *ApiController) GetProduct() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-product [post]
|
||||
func (c *ApiController) UpdateProduct() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var product object.Product
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
|
||||
@@ -151,64 +149,3 @@ func (c *ApiController) DeleteProduct() {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// BuyProduct
|
||||
// @Title BuyProduct
|
||||
// @Tag Product API
|
||||
// @Description buy product
|
||||
// @Param id query string true "The id ( owner/name ) of the product"
|
||||
// @Param providerName query string true "The name of the provider"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /buy-product [post]
|
||||
func (c *ApiController) BuyProduct() {
|
||||
id := c.Input().Get("id")
|
||||
host := c.Ctx.Request.Host
|
||||
providerName := c.Input().Get("providerName")
|
||||
paymentEnv := c.Input().Get("paymentEnv")
|
||||
customPriceStr := c.Input().Get("customPrice")
|
||||
if customPriceStr == "" {
|
||||
customPriceStr = "0"
|
||||
}
|
||||
|
||||
customPrice, err := strconv.ParseFloat(customPriceStr, 64)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// buy `pricingName/planName` for `paidUserName`
|
||||
pricingName := c.Input().Get("pricingName")
|
||||
planName := c.Input().Get("planName")
|
||||
paidUserName := c.Input().Get("userName")
|
||||
owner, _ := util.GetOwnerAndNameFromId(id)
|
||||
userId := util.GetId(owner, paidUserName)
|
||||
if paidUserName != "" && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("general:Only admin user can specify user"))
|
||||
return
|
||||
}
|
||||
if paidUserName == "" {
|
||||
userId = c.GetSessionUsername()
|
||||
}
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return
|
||||
}
|
||||
|
||||
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv, customPrice)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(payment, attachInfo)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// GetPrometheusInfo
|
||||
@@ -37,3 +38,17 @@ func (c *ApiController) GetPrometheusInfo() {
|
||||
|
||||
c.ResponseOk(prometheusInfo)
|
||||
}
|
||||
|
||||
// GetMetrics
|
||||
// @Title GetMetrics
|
||||
// @Tag System API
|
||||
// @Description get Prometheus metrics
|
||||
// @Success 200 {string} Prometheus metrics in text format
|
||||
// @router /metrics [get]
|
||||
func (c *ApiController) GetMetrics() {
|
||||
_, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
promhttp.Handler().ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Provider The Response object
|
||||
// @router /get-providers [get]
|
||||
func (c *ApiController) GetProviders() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
ok, isMaskEnabled := c.IsMaskedEnabled()
|
||||
if !ok {
|
||||
@@ -59,7 +59,7 @@ func (c *ApiController) GetProviders() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
paginationProviders, err := object.GetPaginationProviders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -78,12 +78,12 @@ func (c *ApiController) GetProviders() {
|
||||
// @Success 200 {array} object.Provider The Response object
|
||||
// @router /get-global-providers [get]
|
||||
func (c *ApiController) GetGlobalProviders() {
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
ok, isMaskEnabled := c.IsMaskedEnabled()
|
||||
if !ok {
|
||||
@@ -106,7 +106,7 @@ func (c *ApiController) GetGlobalProviders() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
paginationGlobalProviders, err := object.GetPaginationGlobalProviders(paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -126,7 +126,7 @@ func (c *ApiController) GetGlobalProviders() {
|
||||
// @Success 200 {object} object.Provider The Response object
|
||||
// @router /get-provider [get]
|
||||
func (c *ApiController) GetProvider() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
ok, isMaskEnabled := c.IsMaskedEnabled()
|
||||
if !ok {
|
||||
@@ -164,7 +164,7 @@ func (c *ApiController) requireProviderPermission(provider *object.Provider) boo
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-provider [post]
|
||||
func (c *ApiController) UpdateProvider() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var provider object.Provider
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider)
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
"github.com/casvisor/casvisor-go-sdk/casvisorsdk"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -38,13 +38,13 @@ func (c *ApiController) GetRecords() {
|
||||
return
|
||||
}
|
||||
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organizationName := c.Input().Get("organizationName")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organizationName := c.Ctx.Input.Query("organizationName")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
records, err := object.GetRecords()
|
||||
@@ -66,7 +66,7 @@ func (c *ApiController) GetRecords() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
records, err := object.GetPaginationRecords(paginator.Offset(), limit, field, value, sortField, sortOrder, filterRecord)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -20,10 +20,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -43,14 +44,14 @@ import (
|
||||
// @Success 200 {array} object.Resource The Response object
|
||||
// @router /get-resources [get]
|
||||
func (c *ApiController) GetResources() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
user := c.Ctx.Input.Query("user")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
isOrgAdmin, ok := c.IsOrgAdmin()
|
||||
if !ok {
|
||||
@@ -92,7 +93,7 @@ func (c *ApiController) GetResources() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
resources, err := object.GetPaginationResources(owner, user, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -111,7 +112,7 @@ func (c *ApiController) GetResources() {
|
||||
// @Success 200 {object} object.Resource The Response object
|
||||
// @router /get-resource [get]
|
||||
func (c *ApiController) GetResource() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
resource, err := object.GetResource(id)
|
||||
if err != nil {
|
||||
@@ -131,7 +132,7 @@ func (c *ApiController) GetResource() {
|
||||
// @Success 200 {object} controllers.Response Success or error
|
||||
// @router /update-resource [post]
|
||||
func (c *ApiController) UpdateResource() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var resource object.Resource
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &resource)
|
||||
@@ -177,9 +178,11 @@ func (c *ApiController) DeleteResource() {
|
||||
}
|
||||
|
||||
if resource.Provider != "" {
|
||||
c.Input().Set("provider", resource.Provider)
|
||||
inputs, _ := c.Input()
|
||||
inputs.Set("provider", resource.Provider)
|
||||
}
|
||||
c.Input().Set("fullFilePath", resource.Name)
|
||||
inputs, _ := c.Input()
|
||||
inputs.Set("fullFilePath", resource.Name)
|
||||
provider, err := c.GetProviderFromContext("Storage")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -187,6 +190,11 @@ func (c *ApiController) DeleteResource() {
|
||||
}
|
||||
_, resource.Name = refineFullFilePath(resource.Name)
|
||||
|
||||
tag := c.Ctx.Input.Query("tag")
|
||||
if tag == "Direct" {
|
||||
resource.Name = path.Join(provider.PathPrefix, resource.Name)
|
||||
}
|
||||
|
||||
err = object.DeleteFile(provider, resource.Name, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -212,14 +220,14 @@ func (c *ApiController) DeleteResource() {
|
||||
// @Success 200 {object} object.Resource FileUrl, objectKey
|
||||
// @router /upload-resource [post]
|
||||
func (c *ApiController) UploadResource() {
|
||||
owner := c.Input().Get("owner")
|
||||
username := c.Input().Get("user")
|
||||
application := c.Input().Get("application")
|
||||
tag := c.Input().Get("tag")
|
||||
parent := c.Input().Get("parent")
|
||||
fullFilePath := c.Input().Get("fullFilePath")
|
||||
createdTime := c.Input().Get("createdTime")
|
||||
description := c.Input().Get("description")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
username := c.Ctx.Input.Query("user")
|
||||
application := c.Ctx.Input.Query("application")
|
||||
tag := c.Ctx.Input.Query("tag")
|
||||
parent := c.Ctx.Input.Query("parent")
|
||||
fullFilePath := c.Ctx.Input.Query("fullFilePath")
|
||||
createdTime := c.Ctx.Input.Query("createdTime")
|
||||
description := c.Ctx.Input.Query("description")
|
||||
|
||||
file, header, err := c.GetFile("file")
|
||||
if err != nil {
|
||||
@@ -358,7 +366,7 @@ func (c *ApiController) UploadResource() {
|
||||
}
|
||||
|
||||
applicationObj.TermsOfUse = fileUrl
|
||||
_, err = object.UpdateApplication(applicationId, applicationObj)
|
||||
_, err = object.UpdateApplication(applicationId, applicationObj, true, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Role The Response object
|
||||
// @router /get-roles [get]
|
||||
func (c *ApiController) GetRoles() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
roles, err := object.GetRoles(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetRoles() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
roles, err := object.GetPaginationRoles(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +73,7 @@ func (c *ApiController) GetRoles() {
|
||||
// @Success 200 {object} object.Role The Response object
|
||||
// @router /get-role [get]
|
||||
func (c *ApiController) GetRole() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
role, err := object.GetRole(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) GetRole() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-role [post]
|
||||
func (c *ApiController) UpdateRole() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var role object.Role
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
|
||||
|
||||
@@ -24,7 +24,11 @@ import (
|
||||
|
||||
func (c *ApiController) UploadRoles() {
|
||||
userId := c.GetSessionUsername()
|
||||
owner, user := util.GetOwnerAndNameFromId(userId)
|
||||
owner, user, err := util.GetOwnerAndNameFromIdWithError(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Ctx.Request.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -49,6 +53,6 @@ func (c *ApiController) UploadRoles() {
|
||||
if affected {
|
||||
c.ResponseOk()
|
||||
} else {
|
||||
c.ResponseError(c.T("user_upload:Failed to import users"))
|
||||
c.ResponseError(c.T("general:Failed to import users"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@ package controllers
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func (c *ApiController) GetSamlMeta() {
|
||||
host := c.Ctx.Request.Host
|
||||
paramApp := c.Input().Get("application")
|
||||
paramApp := c.Ctx.Input.Query("application")
|
||||
application, err := object.GetApplication(paramApp)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -57,10 +58,13 @@ func (c *ApiController) HandleSamlRedirect() {
|
||||
owner := c.Ctx.Input.Param(":owner")
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
|
||||
relayState := c.Input().Get("RelayState")
|
||||
samlRequest := c.Input().Get("SAMLRequest")
|
||||
relayState := c.Ctx.Input.Query("RelayState")
|
||||
samlRequest := c.Ctx.Input.Query("SAMLRequest")
|
||||
username := c.Ctx.Input.Query("username")
|
||||
loginHint := c.Ctx.Input.Query("login_hint")
|
||||
|
||||
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host)
|
||||
relayState = url.QueryEscape(relayState)
|
||||
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host, username, loginHint)
|
||||
|
||||
c.Redirect(targetURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ import (
|
||||
)
|
||||
|
||||
func (c *RootController) HandleScim() {
|
||||
_, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
path := c.Ctx.Request.URL.Path
|
||||
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
||||
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
||||
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) SendEmail() {
|
||||
|
||||
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
||||
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
||||
err = object.DailSmtpServer(provider)
|
||||
err = object.TestSmtpServer(provider)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -140,8 +140,11 @@ func (c *ApiController) SendEmail() {
|
||||
}
|
||||
content = strings.Replace(content, "%{user.friendlyName}", userString, 1)
|
||||
|
||||
matchContent := object.ResetLinkReg.Find([]byte(content))
|
||||
content = strings.Replace(content, string(matchContent), "", -1)
|
||||
|
||||
for _, receiver := range emailForm.Receivers {
|
||||
err = object.SendEmail(provider, emailForm.Title, content, receiver, emailForm.Sender)
|
||||
err = object.SendEmail(provider, emailForm.Title, content, []string{receiver}, emailForm.Sender)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +32,13 @@ import (
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /get-sessions [get]
|
||||
func (c *ApiController) GetSessions() {
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
sessions, err := object.GetSessions(owner)
|
||||
@@ -53,7 +55,7 @@ func (c *ApiController) GetSessions() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
sessions, err := object.GetPaginationSessions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -68,11 +70,11 @@ func (c *ApiController) GetSessions() {
|
||||
// @Title GetSingleSession
|
||||
// @Tag Session API
|
||||
// @Description Get session for one user in one application.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Param sessionPkId query string true "The session ID in format: organization/user/application (e.g., built-in/admin/app-built-in)"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /get-session [get]
|
||||
func (c *ApiController) GetSingleSession() {
|
||||
id := c.Input().Get("sessionPkId")
|
||||
id := c.Ctx.Input.Query("sessionPkId")
|
||||
|
||||
session, err := object.GetSingleSession(id)
|
||||
if err != nil {
|
||||
@@ -87,8 +89,8 @@ func (c *ApiController) GetSingleSession() {
|
||||
// @Title UpdateSession
|
||||
// @Tag Session API
|
||||
// @Description Update session for one user in one application.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @Param body body object.Session true "The session object to update"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-session [post]
|
||||
func (c *ApiController) UpdateSession() {
|
||||
var session object.Session
|
||||
@@ -106,9 +108,8 @@ func (c *ApiController) UpdateSession() {
|
||||
// @Title AddSession
|
||||
// @Tag Session API
|
||||
// @Description Add session for one user in one application. If there are other existing sessions, join the session into the list.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Param sessionId query string true "sessionId to be added"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @Param body body object.Session true "The session object to add"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-session [post]
|
||||
func (c *ApiController) AddSession() {
|
||||
var session object.Session
|
||||
@@ -126,8 +127,8 @@ func (c *ApiController) AddSession() {
|
||||
// @Title DeleteSession
|
||||
// @Tag Session API
|
||||
// @Description Delete session for one user in one application.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @Param body body object.Session true "The session object to delete"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-session [post]
|
||||
func (c *ApiController) DeleteSession() {
|
||||
var session object.Session
|
||||
@@ -137,7 +138,21 @@ func (c *ApiController) DeleteSession() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSession(util.GetSessionId(session.Owner, session.Name, session.Application)))
|
||||
curSessionId := c.Ctx.Input.CruSession.SessionID(context.Background())
|
||||
|
||||
sessionId := c.Ctx.Input.Query("sessionId")
|
||||
if curSessionId == sessionId && sessionId != "" {
|
||||
c.ResponseError(fmt.Sprintf(c.T("session:session id %s is the current session and cannot be deleted"), curSessionId))
|
||||
return
|
||||
}
|
||||
|
||||
if sessionId != "" {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSessionId(util.GetSessionId(session.Owner, session.Name, session.Application), sessionId))
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSession(util.GetSessionId(session.Owner, session.Name, session.Application), curSessionId))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -145,13 +160,13 @@ func (c *ApiController) DeleteSession() {
|
||||
// @Title IsSessionDuplicated
|
||||
// @Tag Session API
|
||||
// @Description Check if there are other different sessions for one user in one application.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Param sessionId query string true "sessionId to be checked"
|
||||
// @Param sessionPkId query string true "The session ID in format: organization/user/application (e.g., built-in/admin/app-built-in)"
|
||||
// @Param sessionId query string true "The specific session ID to check"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /is-session-duplicated [get]
|
||||
func (c *ApiController) IsSessionDuplicated() {
|
||||
id := c.Input().Get("sessionPkId")
|
||||
sessionId := c.Input().Get("sessionId")
|
||||
id := c.Ctx.Input.Query("sessionPkId")
|
||||
sessionId := c.Ctx.Input.Query("sessionId")
|
||||
|
||||
isUserSessionDuplicated, err := object.IsSessionDuplicated(id, sessionId)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,8 +16,9 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,16 +31,35 @@ import (
|
||||
// @Success 200 {array} object.Subscription The Response object
|
||||
// @router /get-subscriptions [get]
|
||||
func (c *ApiController) GetSubscriptions() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
subscriptions, err := object.GetSubscriptions(owner)
|
||||
var subscriptions []*object.Subscription
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
subscriptions, err = object.GetSubscriptionsByUser(owner, value)
|
||||
} else {
|
||||
subscriptions, err = object.GetSubscriptions(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
subscriptions, err = object.GetSubscriptionsByUser(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,13 +68,23 @@ func (c *ApiController) GetSubscriptions() {
|
||||
c.ResponseOk(subscriptions)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
count, err := object.GetSubscriptionCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
subscription, err := object.GetPaginationSubscriptions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +103,7 @@ func (c *ApiController) GetSubscriptions() {
|
||||
// @Success 200 {object} object.Subscription The Response object
|
||||
// @router /get-subscription [get]
|
||||
func (c *ApiController) GetSubscription() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
subscription, err := object.GetSubscription(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +123,7 @@ func (c *ApiController) GetSubscription() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-subscription [post]
|
||||
func (c *ApiController) UpdateSubscription() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var subscription object.Subscription
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &subscription)
|
||||
@@ -121,6 +151,26 @@ func (c *ApiController) AddSubscription() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if plan restricts user to one subscription
|
||||
if subscription.Plan != "" {
|
||||
plan, err := object.GetPlan(util.GetId(subscription.Owner, subscription.Plan))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if plan != nil && plan.IsExclusive {
|
||||
hasSubscription, err := object.HasActiveSubscriptionForPlan(subscription.Owner, subscription.User, subscription.Plan)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if hasSubscription {
|
||||
c.ResponseError(fmt.Sprintf("User already has an active subscription for plan: %s", subscription.Plan))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddSubscription(&subscription))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,14 +31,14 @@ import (
|
||||
// @Success 200 {array} object.Syncer The Response object
|
||||
// @router /get-syncers [get]
|
||||
func (c *ApiController) GetSyncers() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
syncers, err := object.GetMaskedSyncers(object.GetOrganizationSyncers(owner, organization))
|
||||
@@ -55,7 +56,7 @@ func (c *ApiController) GetSyncers() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
syncers, err := object.GetMaskedSyncers(object.GetPaginationSyncers(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -74,7 +75,7 @@ func (c *ApiController) GetSyncers() {
|
||||
// @Success 200 {object} object.Syncer The Response object
|
||||
// @router /get-syncer [get]
|
||||
func (c *ApiController) GetSyncer() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
syncer, err := object.GetMaskedSyncer(object.GetSyncer(id))
|
||||
if err != nil {
|
||||
@@ -94,7 +95,7 @@ func (c *ApiController) GetSyncer() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-syncer [post]
|
||||
func (c *ApiController) UpdateSyncer() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var syncer object.Syncer
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &syncer)
|
||||
@@ -103,7 +104,7 @@ func (c *ApiController) UpdateSyncer() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateSyncer(id, &syncer))
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateSyncer(id, &syncer, c.IsGlobalAdmin(), c.GetAcceptLanguage()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -153,12 +154,16 @@ func (c *ApiController) DeleteSyncer() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /run-syncer [get]
|
||||
func (c *ApiController) RunSyncer() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
syncer, err := object.GetSyncer(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if syncer == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The syncer: %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
|
||||
err = object.RunSyncer(syncer)
|
||||
if err != nil {
|
||||
@@ -177,7 +182,7 @@ func (c *ApiController) TestSyncerDb() {
|
||||
return
|
||||
}
|
||||
|
||||
err = object.TestSyncerDb(syncer)
|
||||
err = object.TestSyncer(syncer)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// GetSystemInfo
|
||||
@@ -46,10 +49,10 @@ func (c *ApiController) GetSystemInfo() {
|
||||
// @Success 200 {object} util.VersionInfo The Response object
|
||||
// @router /get-version-info [get]
|
||||
func (c *ApiController) GetVersionInfo() {
|
||||
errInfo := ""
|
||||
versionInfo, err := util.GetVersionInfo()
|
||||
if err != nil {
|
||||
errInfo = "Git error: " + err.Error()
|
||||
if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if versionInfo.Version != "" {
|
||||
@@ -57,14 +60,7 @@ func (c *ApiController) GetVersionInfo() {
|
||||
return
|
||||
}
|
||||
|
||||
versionInfo, err = util.GetVersionInfoFromFile()
|
||||
if err != nil {
|
||||
errInfo = errInfo + ", File error: " + err.Error()
|
||||
c.ResponseError(errInfo)
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(versionInfo)
|
||||
c.ResponseOk(util.GetBuiltInVersionInfo())
|
||||
}
|
||||
|
||||
// Health
|
||||
|
||||
271
controllers/ticket.go
Normal file
271
controllers/ticket.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetTickets
|
||||
// @Title GetTickets
|
||||
// @Tag Ticket API
|
||||
// @Description get tickets
|
||||
// @Param owner query string true "The owner of tickets"
|
||||
// @Success 200 {array} object.Ticket The Response object
|
||||
// @router /get-tickets [get]
|
||||
func (c *ApiController) GetTickets() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
var tickets []*object.Ticket
|
||||
var err error
|
||||
|
||||
if limit == "" || page == "" {
|
||||
if isAdmin {
|
||||
tickets, err = object.GetTickets(owner)
|
||||
} else {
|
||||
tickets, err = object.GetUserTickets(owner, user.GetId())
|
||||
}
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(tickets)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
var count int64
|
||||
|
||||
if isAdmin {
|
||||
count, err = object.GetTicketCount(owner, field, value)
|
||||
} else {
|
||||
// For non-admin users, only show their own tickets
|
||||
tickets, err = object.GetUserTickets(owner, user.GetId())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
count = int64(len(tickets))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
|
||||
if isAdmin {
|
||||
tickets, err = object.GetPaginationTickets(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(tickets, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetTicket
|
||||
// @Title GetTicket
|
||||
// @Tag Ticket API
|
||||
// @Description get ticket
|
||||
// @Param id query string true "The id ( owner/name ) of the ticket"
|
||||
// @Success 200 {object} object.Ticket The Response object
|
||||
// @router /get-ticket [get]
|
||||
func (c *ApiController) GetTicket() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
ticket, err := object.GetTicket(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission: user can only view their own tickets unless they are admin
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
if ticket != nil && !isAdmin && ticket.User != user.GetId() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(ticket)
|
||||
}
|
||||
|
||||
// UpdateTicket
|
||||
// @Title UpdateTicket
|
||||
// @Tag Ticket API
|
||||
// @Description update ticket
|
||||
// @Param id query string true "The id ( owner/name ) of the ticket"
|
||||
// @Param body body object.Ticket true "The details of the ticket"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-ticket [post]
|
||||
func (c *ApiController) UpdateTicket() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var ticket object.Ticket
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ticket)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
existingTicket, err := object.GetTicket(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if existingTicket == nil {
|
||||
c.ResponseError(c.T("ticket:Ticket not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Normal users can only close their own tickets
|
||||
if !isAdmin {
|
||||
if existingTicket.User != user.GetId() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
// Normal users can only change state to "Closed"
|
||||
if ticket.State != "Closed" && ticket.State != existingTicket.State {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
// Preserve original fields that users shouldn't modify
|
||||
ticket.Owner = existingTicket.Owner
|
||||
ticket.Name = existingTicket.Name
|
||||
ticket.User = existingTicket.User
|
||||
ticket.CreatedTime = existingTicket.CreatedTime
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateTicket(id, &ticket))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddTicket
|
||||
// @Title AddTicket
|
||||
// @Tag Ticket API
|
||||
// @Description add ticket
|
||||
// @Param body body object.Ticket true "The details of the ticket"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-ticket [post]
|
||||
func (c *ApiController) AddTicket() {
|
||||
var ticket object.Ticket
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ticket)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set the user field to the current user
|
||||
user := c.getCurrentUser()
|
||||
ticket.User = user.GetId()
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddTicket(&ticket))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteTicket
|
||||
// @Title DeleteTicket
|
||||
// @Tag Ticket API
|
||||
// @Description delete ticket
|
||||
// @Param body body object.Ticket true "The details of the ticket"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-ticket [post]
|
||||
func (c *ApiController) DeleteTicket() {
|
||||
var ticket object.Ticket
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ticket)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Only admins can delete tickets
|
||||
if !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteTicket(&ticket))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddTicketMessage
|
||||
// @Title AddTicketMessage
|
||||
// @Tag Ticket API
|
||||
// @Description add a message to a ticket
|
||||
// @Param id query string true "The id ( owner/name ) of the ticket"
|
||||
// @Param body body object.TicketMessage true "The message to add"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-ticket-message [post]
|
||||
func (c *ApiController) AddTicketMessage() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var message object.TicketMessage
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
ticket, err := object.GetTicket(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if ticket == nil {
|
||||
c.ResponseError(c.T("ticket:Ticket not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Users can only add messages to their own tickets, admins can add to any ticket
|
||||
if !isAdmin && ticket.User != user.GetId() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set the author and admin flag
|
||||
message.Author = user.GetId()
|
||||
message.IsAdmin = isAdmin
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddTicketMessage(id, &message))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -16,8 +16,10 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -26,20 +28,20 @@ import (
|
||||
// @Title GetTokens
|
||||
// @Tag Token API
|
||||
// @Description get tokens
|
||||
// @Param owner query string true "The owner of tokens"
|
||||
// @Param owner query string true "The organization name (e.g., built-in)"
|
||||
// @Param pageSize query string true "The size of each page"
|
||||
// @Param p query string true "The number of the page"
|
||||
// @Success 200 {array} object.Token The Response object
|
||||
// @router /get-tokens [get]
|
||||
func (c *ApiController) GetTokens() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
if limit == "" || page == "" {
|
||||
token, err := object.GetTokens(owner, organization)
|
||||
if err != nil {
|
||||
@@ -56,7 +58,7 @@ func (c *ApiController) GetTokens() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
tokens, err := object.GetPaginationTokens(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -71,11 +73,11 @@ func (c *ApiController) GetTokens() {
|
||||
// @Title GetToken
|
||||
// @Tag Token API
|
||||
// @Description get token
|
||||
// @Param id query string true "The id ( owner/name ) of token"
|
||||
// @Param id query string true "The token ID in format: organization/token-name (e.g., built-in/token-123456)"
|
||||
// @Success 200 {object} object.Token The Response object
|
||||
// @router /get-token [get]
|
||||
func (c *ApiController) GetToken() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
token, err := object.GetToken(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -89,12 +91,12 @@ func (c *ApiController) GetToken() {
|
||||
// @Title UpdateToken
|
||||
// @Tag Token API
|
||||
// @Description update token
|
||||
// @Param id query string true "The id ( owner/name ) of token"
|
||||
// @Param id query string true "The token ID in format: organization/token-name (e.g., built-in/token-123456)"
|
||||
// @Param body body object.Token true "Details of the token"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-token [post]
|
||||
func (c *ApiController) UpdateToken() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var token object.Token
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &token)
|
||||
@@ -103,7 +105,7 @@ func (c *ApiController) UpdateToken() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateToken(id, &token))
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateToken(id, &token, c.IsGlobalAdmin()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -158,24 +160,29 @@ func (c *ApiController) DeleteToken() {
|
||||
// @Success 401 {object} object.TokenError The Response object
|
||||
// @router /login/oauth/access_token [post]
|
||||
func (c *ApiController) GetOAuthToken() {
|
||||
clientId := c.Input().Get("client_id")
|
||||
clientSecret := c.Input().Get("client_secret")
|
||||
grantType := c.Input().Get("grant_type")
|
||||
code := c.Input().Get("code")
|
||||
verifier := c.Input().Get("code_verifier")
|
||||
scope := c.Input().Get("scope")
|
||||
nonce := c.Input().Get("nonce")
|
||||
username := c.Input().Get("username")
|
||||
password := c.Input().Get("password")
|
||||
tag := c.Input().Get("tag")
|
||||
avatar := c.Input().Get("avatar")
|
||||
refreshToken := c.Input().Get("refresh_token")
|
||||
clientId := c.Ctx.Input.Query("client_id")
|
||||
clientSecret := c.Ctx.Input.Query("client_secret")
|
||||
grantType := c.Ctx.Input.Query("grant_type")
|
||||
code := c.Ctx.Input.Query("code")
|
||||
verifier := c.Ctx.Input.Query("code_verifier")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
username := c.Ctx.Input.Query("username")
|
||||
password := c.Ctx.Input.Query("password")
|
||||
tag := c.Ctx.Input.Query("tag")
|
||||
avatar := c.Ctx.Input.Query("avatar")
|
||||
refreshToken := c.Ctx.Input.Query("refresh_token")
|
||||
deviceCode := c.Ctx.Input.Query("device_code")
|
||||
subjectToken := c.Ctx.Input.Query("subject_token")
|
||||
subjectTokenType := c.Ctx.Input.Query("subject_token_type")
|
||||
audience := c.Ctx.Input.Query("audience")
|
||||
resource := c.Ctx.Input.Query("resource")
|
||||
|
||||
if clientId == "" && clientSecret == "" {
|
||||
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
|
||||
}
|
||||
|
||||
if len(c.Ctx.Input.RequestBody) != 0 {
|
||||
if len(c.Ctx.Input.RequestBody) != 0 && grantType != "urn:ietf:params:oauth:grant-type:device_code" {
|
||||
// If clientId is empty, try to read data from RequestBody
|
||||
var tokenRequest TokenRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest)
|
||||
@@ -216,11 +223,63 @@ func (c *ApiController) GetOAuthToken() {
|
||||
if refreshToken == "" {
|
||||
refreshToken = tokenRequest.RefreshToken
|
||||
}
|
||||
if subjectToken == "" {
|
||||
subjectToken = tokenRequest.SubjectToken
|
||||
}
|
||||
if subjectTokenType == "" {
|
||||
subjectTokenType = tokenRequest.SubjectTokenType
|
||||
}
|
||||
if audience == "" {
|
||||
audience = tokenRequest.Audience
|
||||
}
|
||||
if resource == "" {
|
||||
resource = tokenRequest.Resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if deviceCode != "" {
|
||||
deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode)
|
||||
if !ok {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: "expired_token",
|
||||
ErrorDescription: "token is expired",
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
c.SetTokenErrorHttpStatus()
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
|
||||
if !deviceAuthCacheCast.UserSignIn {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: "authorization_pending",
|
||||
ErrorDescription: "authorization pending",
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
c.SetTokenErrorHttpStatus()
|
||||
return
|
||||
}
|
||||
|
||||
if deviceAuthCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: "expired_token",
|
||||
ErrorDescription: "token is expired",
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
c.SetTokenErrorHttpStatus()
|
||||
return
|
||||
}
|
||||
object.DeviceAuthMap.Delete(deviceCode)
|
||||
|
||||
username = deviceAuthCacheCast.UserName
|
||||
}
|
||||
|
||||
host := c.Ctx.Request.Host
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, audience, resource)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -245,11 +304,11 @@ func (c *ApiController) GetOAuthToken() {
|
||||
// @Success 401 {object} object.TokenError The Response object
|
||||
// @router /login/oauth/refresh_token [post]
|
||||
func (c *ApiController) RefreshToken() {
|
||||
grantType := c.Input().Get("grant_type")
|
||||
refreshToken := c.Input().Get("refresh_token")
|
||||
scope := c.Input().Get("scope")
|
||||
clientId := c.Input().Get("client_id")
|
||||
clientSecret := c.Input().Get("client_secret")
|
||||
grantType := c.Ctx.Input.Query("grant_type")
|
||||
refreshToken := c.Ctx.Input.Query("refresh_token")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
clientId := c.Ctx.Input.Query("client_id")
|
||||
clientSecret := c.Ctx.Input.Query("client_secret")
|
||||
host := c.Ctx.Request.Host
|
||||
|
||||
if clientId == "" {
|
||||
@@ -299,11 +358,11 @@ func (c *ApiController) ResponseTokenError(errorMsg string) {
|
||||
// @Success 401 {object} object.TokenError The Response object
|
||||
// @router /login/oauth/introspect [post]
|
||||
func (c *ApiController) IntrospectToken() {
|
||||
tokenValue := c.Input().Get("token")
|
||||
tokenValue := c.Ctx.Input.Query("token")
|
||||
clientId, clientSecret, ok := c.Ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
clientId = c.Input().Get("client_id")
|
||||
clientSecret = c.Input().Get("client_secret")
|
||||
clientId = c.Ctx.Input.Query("client_id")
|
||||
clientSecret = c.Ctx.Input.Query("client_secret")
|
||||
if clientId == "" || clientSecret == "" {
|
||||
c.ResponseTokenError(object.InvalidRequest)
|
||||
return
|
||||
@@ -321,35 +380,49 @@ func (c *ApiController) IntrospectToken() {
|
||||
return
|
||||
}
|
||||
|
||||
tokenTypeHint := c.Input().Get("token_type_hint")
|
||||
token, err := object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil {
|
||||
respondWithInactiveToken := func() {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil || jwtToken.Valid() != nil {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
tokenTypeHint := c.Ctx.Input.Query("token_type_hint")
|
||||
var token *object.Token
|
||||
if tokenTypeHint != "" {
|
||||
token, err = object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
|
||||
if token.ExpiresIn <= 0 {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = &object.IntrospectionResponse{
|
||||
var introspectionResponse object.IntrospectionResponse
|
||||
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
|
||||
introspectionResponse = object.IntrospectionResponse{
|
||||
Active: true,
|
||||
Scope: jwtToken.Scope,
|
||||
ClientId: clientId,
|
||||
Username: token.User,
|
||||
TokenType: token.TokenType,
|
||||
Username: jwtToken.Name,
|
||||
TokenType: jwtToken.TokenType,
|
||||
Exp: jwtToken.ExpiresAt.Unix(),
|
||||
Iat: jwtToken.IssuedAt.Unix(),
|
||||
Nbf: jwtToken.NotBefore.Unix(),
|
||||
@@ -358,33 +431,66 @@ func (c *ApiController) IntrospectToken() {
|
||||
Iss: jwtToken.Issuer,
|
||||
Jti: jwtToken.ID,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
} else {
|
||||
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
|
||||
introspectionResponse = object.IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientId: clientId,
|
||||
Exp: jwtToken.ExpiresAt.Unix(),
|
||||
Iat: jwtToken.IssuedAt.Unix(),
|
||||
Nbf: jwtToken.NotBefore.Unix(),
|
||||
Sub: jwtToken.Subject,
|
||||
Aud: jwtToken.Audience,
|
||||
Iss: jwtToken.Issuer,
|
||||
Jti: jwtToken.ID,
|
||||
}
|
||||
|
||||
if jwtToken.Scope != "" {
|
||||
introspectionResponse.Scope = jwtToken.Scope
|
||||
}
|
||||
if jwtToken.Name != "" {
|
||||
introspectionResponse.Username = jwtToken.Name
|
||||
}
|
||||
if jwtToken.TokenType != "" {
|
||||
introspectionResponse.TokenType = jwtToken.TokenType
|
||||
}
|
||||
}
|
||||
|
||||
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil || jwtToken.Valid() != nil {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
if tokenTypeHint == "" {
|
||||
token, err = object.GetTokenByTokenValue(tokenValue, introspectionResponse.TokenType)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = &object.IntrospectionResponse{
|
||||
Active: true,
|
||||
Scope: jwtToken.Scope,
|
||||
ClientId: clientId,
|
||||
Username: token.User,
|
||||
TokenType: token.TokenType,
|
||||
Exp: jwtToken.ExpiresAt.Unix(),
|
||||
Iat: jwtToken.IssuedAt.Unix(),
|
||||
Nbf: jwtToken.NotBefore.Unix(),
|
||||
Sub: jwtToken.Subject,
|
||||
Aud: jwtToken.Audience,
|
||||
Iss: jwtToken.Issuer,
|
||||
Jti: jwtToken.ID,
|
||||
if token != nil {
|
||||
application, err = object.GetApplication(fmt.Sprintf("%s/%s", token.Owner, token.Application))
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), token.Application))
|
||||
return
|
||||
}
|
||||
|
||||
introspectionResponse.TokenType = token.TokenType
|
||||
introspectionResponse.ClientId = application.ClientId
|
||||
}
|
||||
|
||||
c.Data["json"] = introspectionResponse
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,16 +30,35 @@ import (
|
||||
// @Success 200 {array} object.Transaction The Response object
|
||||
// @router /get-transactions [get]
|
||||
func (c *ApiController) GetTransactions() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
transactions, err := object.GetTransactions(owner)
|
||||
var transactions []*object.Transaction
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
transactions, err = object.GetUserTransactions(owner, value)
|
||||
} else {
|
||||
transactions, err = object.GetTransactions(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
transactions, err = object.GetUserTransactions(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,13 +67,26 @@ func (c *ApiController) GetTransactions() {
|
||||
c.ResponseOk(transactions)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
|
||||
// Apply user filter for non-admin users
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
|
||||
count, err := object.GetTransactionCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
transactions, err := object.GetPaginationTransactions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -65,28 +97,6 @@ func (c *ApiController) GetTransactions() {
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserTransactions
|
||||
// @Title GetUserTransaction
|
||||
// @Tag Transaction API
|
||||
// @Description get transactions for a user
|
||||
// @Param owner query string true "The owner of transactions"
|
||||
// @Param organization query string true "The organization of the user"
|
||||
// @Param user query string true "The username of the user"
|
||||
// @Success 200 {array} object.Transaction The Response object
|
||||
// @router /get-user-transactions [get]
|
||||
func (c *ApiController) GetUserTransactions() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
|
||||
transactions, err := object.GetUserTransactions(owner, user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(transactions)
|
||||
}
|
||||
|
||||
// GetTransaction
|
||||
// @Title GetTransaction
|
||||
// @Tag Transaction API
|
||||
@@ -95,7 +105,7 @@ func (c *ApiController) GetUserTransactions() {
|
||||
// @Success 200 {object} object.Transaction The Response object
|
||||
// @router /get-transaction [get]
|
||||
func (c *ApiController) GetTransaction() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
transaction, err := object.GetTransaction(id)
|
||||
if err != nil {
|
||||
@@ -103,6 +113,27 @@ func (c *ApiController) GetTransaction() {
|
||||
return
|
||||
}
|
||||
|
||||
if transaction == nil {
|
||||
c.ResponseOk(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if non-admin user is trying to access someone else's transaction
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Only allow users to view their own transactions
|
||||
if transaction.User != userName {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.ResponseOk(transaction)
|
||||
}
|
||||
|
||||
@@ -115,7 +146,7 @@ func (c *ApiController) GetTransaction() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-transaction [post]
|
||||
func (c *ApiController) UpdateTransaction() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var transaction object.Transaction
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &transaction)
|
||||
@@ -124,7 +155,7 @@ func (c *ApiController) UpdateTransaction() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateTransaction(id, &transaction))
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateTransaction(id, &transaction, c.GetAcceptLanguage()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -133,6 +164,7 @@ func (c *ApiController) UpdateTransaction() {
|
||||
// @Tag Transaction API
|
||||
// @Description add transaction
|
||||
// @Param body body object.Transaction true "The details of the transaction"
|
||||
// @Param dryRun query string false "Dry run mode: set to 'true' or '1' to validate without committing"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-transaction [post]
|
||||
func (c *ApiController) AddTransaction() {
|
||||
@@ -143,8 +175,22 @@ func (c *ApiController) AddTransaction() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddTransaction(&transaction))
|
||||
c.ServeJSON()
|
||||
dryRunParam := c.Ctx.Input.Query("dryRun")
|
||||
dryRun := dryRunParam != ""
|
||||
|
||||
affected, transactionId, err := object.AddTransaction(&transaction, c.GetAcceptLanguage(), dryRun)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !affected {
|
||||
c.Data["json"] = wrapActionResponse(false)
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(transactionId)
|
||||
}
|
||||
|
||||
// DeleteTransaction
|
||||
@@ -162,6 +208,6 @@ func (c *ApiController) DeleteTransaction() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteTransaction(&transaction))
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteTransaction(&transaction, c.GetAcceptLanguage()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -15,16 +15,20 @@
|
||||
package controllers
|
||||
|
||||
type TokenRequest struct {
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code string `json:"code"`
|
||||
Verifier string `json:"code_verifier"`
|
||||
Scope string `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code string `json:"code"`
|
||||
Verifier string `json:"code_verifier"`
|
||||
Scope string `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
SubjectToken string `json:"subject_token"`
|
||||
SubjectTokenType string `json:"subject_token_type"`
|
||||
Audience string `json:"audience"`
|
||||
Resource string `json:"resource"` // RFC 8707 Resource Indicator
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -32,12 +32,12 @@ import (
|
||||
// @Success 200 {array} object.User The Response object
|
||||
// @router /get-global-users [get]
|
||||
func (c *ApiController) GetGlobalUsers() {
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
users, err := object.GetMaskedUsers(object.GetGlobalUsers())
|
||||
@@ -55,7 +55,7 @@ func (c *ApiController) GetGlobalUsers() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
users, err := object.GetPaginationGlobalUsers(paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -80,14 +80,14 @@ func (c *ApiController) GetGlobalUsers() {
|
||||
// @Success 200 {array} object.User The Response object
|
||||
// @router /get-users [get]
|
||||
func (c *ApiController) GetUsers() {
|
||||
owner := c.Input().Get("owner")
|
||||
groupName := c.Input().Get("groupName")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
groupName := c.Ctx.Input.Query("groupName")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
if groupName != "" {
|
||||
@@ -115,7 +115,7 @@ func (c *ApiController) GetUsers() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
users, err := object.GetPaginationUsers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder, groupName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -144,11 +144,11 @@ func (c *ApiController) GetUsers() {
|
||||
// @Success 200 {object} object.User The Response object
|
||||
// @router /get-user [get]
|
||||
func (c *ApiController) GetUser() {
|
||||
id := c.Input().Get("id")
|
||||
email := c.Input().Get("email")
|
||||
phone := c.Input().Get("phone")
|
||||
userId := c.Input().Get("userId")
|
||||
owner := c.Input().Get("owner")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
email := c.Ctx.Input.Query("email")
|
||||
phone := c.Ctx.Input.Query("phone")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
var err error
|
||||
var userFromUserId *object.User
|
||||
if userId != "" && owner != "" {
|
||||
@@ -197,8 +197,8 @@ func (c *ApiController) GetUser() {
|
||||
return
|
||||
}
|
||||
|
||||
var organization *object.Organization
|
||||
if user != nil {
|
||||
var organization *object.Organization
|
||||
organization, err = object.GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -237,6 +237,14 @@ func (c *ApiController) GetUser() {
|
||||
return
|
||||
}
|
||||
|
||||
if organization != nil && user != nil {
|
||||
user, err = object.GetFilteredUser(user, c.IsAdmin(), c.IsAdminOrSelf(user), organization.AccountItems)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.ResponseOk(user)
|
||||
}
|
||||
|
||||
@@ -244,13 +252,17 @@ func (c *ApiController) GetUser() {
|
||||
// @Title UpdateUser
|
||||
// @Tag User API
|
||||
// @Description update user
|
||||
// @Param id query string true "The id ( owner/name ) of the user"
|
||||
// @Param id query string false "The id ( owner/name ) of the user"
|
||||
// @Param userId query string false "The userId (UUID) of the user"
|
||||
// @Param owner query string false "The owner of the user (required when using userId)"
|
||||
// @Param body body object.User true "The details of the user"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-user [post]
|
||||
func (c *ApiController) UpdateUser() {
|
||||
id := c.Input().Get("id")
|
||||
columnsStr := c.Input().Get("columns")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
columnsStr := c.Ctx.Input.Query("columns")
|
||||
|
||||
var user object.User
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
|
||||
@@ -259,17 +271,38 @@ func (c *ApiController) UpdateUser() {
|
||||
return
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
if id == "" && userId == "" {
|
||||
id = c.GetSessionUsername()
|
||||
if id == "" {
|
||||
c.ResponseError(c.T("general:Missing parameter"))
|
||||
return
|
||||
}
|
||||
}
|
||||
oldUser, err := object.GetUser(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
var userFromUserId *object.User
|
||||
if userId != "" && owner != "" {
|
||||
userFromUserId, err = object.GetUserByUserId(owner, userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if userFromUserId == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return
|
||||
}
|
||||
|
||||
id = util.GetId(userFromUserId.Owner, userFromUserId.Name)
|
||||
}
|
||||
|
||||
var oldUser *object.User
|
||||
if userId != "" {
|
||||
oldUser = userFromUserId
|
||||
} else {
|
||||
oldUser, err = object.GetUser(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if oldUser == nil {
|
||||
@@ -282,13 +315,6 @@ func (c *ApiController) UpdateUser() {
|
||||
return
|
||||
}
|
||||
|
||||
if c.Input().Get("allowEmpty") == "" {
|
||||
if user.DisplayName == "" {
|
||||
c.ResponseError(c.T("user:Display name cannot be empty"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if user.MfaEmailEnabled && user.Email == "" {
|
||||
c.ResponseError(c.T("user:MFA email is enabled but email is empty"))
|
||||
return
|
||||
@@ -310,7 +336,8 @@ func (c *ApiController) UpdateUser() {
|
||||
}
|
||||
|
||||
isAdmin := c.IsAdmin()
|
||||
if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, c.GetAcceptLanguage()); !pass {
|
||||
allowDisplayNameEmpty := c.Ctx.Input.Query("allowEmpty") != ""
|
||||
if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, allowDisplayNameEmpty, c.GetAcceptLanguage()); !pass {
|
||||
c.ResponseError(err)
|
||||
return
|
||||
}
|
||||
@@ -353,29 +380,30 @@ func (c *ApiController) AddUser() {
|
||||
return
|
||||
}
|
||||
|
||||
count, err := object.GetUserCount("", "", "", "")
|
||||
if err != nil {
|
||||
if err := checkQuotaForUser(); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkQuotaForUser(int(count)); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
msg := object.CheckUsername(user.Name, c.GetAcceptLanguage())
|
||||
emptyUser := object.User{}
|
||||
msg := object.CheckUpdateUser(&emptyUser, &user, c.GetAcceptLanguage())
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
}
|
||||
|
||||
if err = object.CheckIpWhitelist(user.IpWhitelist, c.GetAcceptLanguage()); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
// Set RegisterSource based on the current user if not already set
|
||||
if user.RegisterType == "" {
|
||||
user.RegisterType = "Add User"
|
||||
}
|
||||
if user.RegisterSource == "" {
|
||||
currentUser := c.getCurrentUser()
|
||||
if currentUser != nil {
|
||||
user.RegisterSource = currentUser.GetId()
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddUser(&user))
|
||||
c.Data["json"] = wrapActionResponse(object.AddUser(&user, c.GetAcceptLanguage()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -467,18 +495,58 @@ func (c *ApiController) SetPassword() {
|
||||
newPassword := c.Ctx.Request.Form.Get("newPassword")
|
||||
code := c.Ctx.Request.Form.Get("code")
|
||||
|
||||
//if userOwner == "built-in" && userName == "admin" {
|
||||
// if userOwner == "built-in" && userName == "admin" {
|
||||
// c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
// return
|
||||
//}
|
||||
// }
|
||||
|
||||
userId := util.GetId(userOwner, userName)
|
||||
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return
|
||||
}
|
||||
|
||||
// Get organization to check for password obfuscation settings
|
||||
organization, err := object.GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if organization == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:the organization: %s is not found"), user.Owner))
|
||||
return
|
||||
}
|
||||
|
||||
// Deobfuscate passwords if organization has password obfuscator configured
|
||||
// Note: Deobfuscation is optional - if it fails, we treat the password as plain text
|
||||
// This allows SDKs and raw HTTP API calls to work without obfuscation support
|
||||
if organization.PasswordObfuscatorType != "" && organization.PasswordObfuscatorType != "Plain" {
|
||||
if oldPassword != "" {
|
||||
deobfuscatedOldPassword, deobfuscateErr := util.GetUnobfuscatedPassword(organization.PasswordObfuscatorType, organization.PasswordObfuscatorKey, oldPassword)
|
||||
if deobfuscateErr == nil {
|
||||
oldPassword = deobfuscatedOldPassword
|
||||
}
|
||||
}
|
||||
|
||||
if newPassword != "" {
|
||||
deobfuscatedNewPassword, deobfuscateErr := util.GetUnobfuscatedPassword(organization.PasswordObfuscatorType, organization.PasswordObfuscatorKey, newPassword)
|
||||
if deobfuscateErr == nil {
|
||||
newPassword = deobfuscatedNewPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(newPassword, " ") {
|
||||
c.ResponseError(c.T("user:New password cannot contain blank space."))
|
||||
return
|
||||
}
|
||||
|
||||
userId := util.GetId(userOwner, userName)
|
||||
|
||||
requestUserId := c.GetSessionUsername()
|
||||
if requestUserId == "" && code == "" {
|
||||
c.ResponseError(c.T("general:Please login first"), "Please login first")
|
||||
@@ -494,7 +562,12 @@ func (c *ApiController) SetPassword() {
|
||||
c.ResponseError(c.T("general:Missing parameter"))
|
||||
return
|
||||
}
|
||||
if userId != c.GetSession("verifiedUserId") {
|
||||
c.ResponseError(c.T("general:Wrong userId"))
|
||||
return
|
||||
}
|
||||
c.SetSession("verifiedCode", "")
|
||||
c.SetSession("verifiedUserId", "")
|
||||
}
|
||||
|
||||
targetUser, err := object.GetUser(userId)
|
||||
@@ -517,26 +590,28 @@ func (c *ApiController) SetPassword() {
|
||||
}
|
||||
}
|
||||
} else if code == "" {
|
||||
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
if targetUser.Password != "" || user.Ldap != "" {
|
||||
if user.Ldap == "" {
|
||||
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
|
||||
} else {
|
||||
err = object.CheckLdapUserPassword(targetUser, oldPassword, c.GetAcceptLanguage())
|
||||
}
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg := object.CheckPasswordComplexity(targetUser, newPassword)
|
||||
msg := object.CheckPasswordComplexity(targetUser, newPassword, c.GetAcceptLanguage())
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
}
|
||||
|
||||
organization, err := object.GetOrganizationByUser(targetUser)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if organization == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("the organization: %s is not found"), targetUser.Owner))
|
||||
// Check if the new password is the same as the current password
|
||||
if !object.CheckPasswordNotSameAsCurrent(targetUser, newPassword, organization) {
|
||||
c.ResponseError(c.T("user:The new password must be different from your current password"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -560,8 +635,18 @@ func (c *ApiController) SetPassword() {
|
||||
targetUser.Password = newPassword
|
||||
targetUser.UpdateUserPassword(organization)
|
||||
targetUser.NeedUpdatePassword = false
|
||||
targetUser.LastChangePasswordTime = util.GetCurrentTime()
|
||||
|
||||
if user.Ldap == "" {
|
||||
_, err = object.UpdateUser(userId, targetUser, []string{"password", "password_salt", "need_update_password", "password_type", "last_change_password_time"}, false)
|
||||
} else {
|
||||
if isAdmin {
|
||||
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())
|
||||
} else {
|
||||
err = object.ResetLdapPassword(targetUser, oldPassword, newPassword, c.GetAcceptLanguage())
|
||||
}
|
||||
}
|
||||
|
||||
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type"}, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -583,7 +668,11 @@ func (c *ApiController) CheckUserPassword() {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage())
|
||||
/*
|
||||
* Verified password with user as subject, if field ldap not empty,
|
||||
* then `isPasswordWithLdapEnabled` is true
|
||||
*/
|
||||
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage(), false, false, user.Ldap != "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
} else {
|
||||
@@ -601,9 +690,9 @@ func (c *ApiController) CheckUserPassword() {
|
||||
// @Success 200 {array} object.User The Response object
|
||||
// @router /get-sorted-users [get]
|
||||
func (c *ApiController) GetSortedUsers() {
|
||||
owner := c.Input().Get("owner")
|
||||
sorter := c.Input().Get("sorter")
|
||||
limit := util.ParseInt(c.Input().Get("limit"))
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
sorter := c.Ctx.Input.Query("sorter")
|
||||
limit := util.ParseInt(c.Ctx.Input.Query("limit"))
|
||||
|
||||
users, err := object.GetMaskedUsers(object.GetSortedUsers(owner, sorter, limit))
|
||||
if err != nil {
|
||||
@@ -623,8 +712,8 @@ func (c *ApiController) GetSortedUsers() {
|
||||
// @Success 200 {int} int The count of filtered users for an organization
|
||||
// @router /get-user-count [get]
|
||||
func (c *ApiController) GetUserCount() {
|
||||
owner := c.Input().Get("owner")
|
||||
isOnline := c.Input().Get("isOnline")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
isOnline := c.Ctx.Input.Query("isOnline")
|
||||
|
||||
var count int64
|
||||
var err error
|
||||
@@ -680,7 +769,7 @@ func (c *ApiController) RemoveUserFromGroup() {
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := object.DeleteGroupForUser(util.GetId(owner, name), groupName)
|
||||
affected, err := object.DeleteGroupForUser(util.GetId(owner, name), util.GetId(owner, groupName))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -688,3 +777,205 @@ func (c *ApiController) RemoveUserFromGroup() {
|
||||
|
||||
c.ResponseOk(affected)
|
||||
}
|
||||
|
||||
// ImpersonateUser
|
||||
// @Title ImpersonateUser
|
||||
// @Tag User API
|
||||
// @Description set impersonation user for current admin session
|
||||
// @Param username formData string true "The username to impersonate (owner/name)"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /impersonation-user [post]
|
||||
func (c *ApiController) ImpersonateUser() {
|
||||
org, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
username := c.Ctx.Request.Form.Get("username")
|
||||
if username == "" {
|
||||
c.ResponseError(c.T("general:Missing parameter"))
|
||||
return
|
||||
}
|
||||
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !(owner == org || org == "") {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
targetUser, err := object.GetUser(username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if targetUser == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), username))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.SetSession("impersonateUser", username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Ctx.SetCookie("impersonateUser", username, 0, "/")
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// ExitImpersonateUser
|
||||
// @Title ExitImpersonateUser
|
||||
// @Tag User API
|
||||
// @Description clear impersonation info for current session
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /exit-impersonation-user [post]
|
||||
func (c *ApiController) ExitImpersonateUser() {
|
||||
_, ok := c.Ctx.Input.GetData("impersonating").(bool)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
err := c.SetSession("impersonateUser", "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Ctx.SetCookie("impersonateUser", "", -1, "/")
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// VerifyIdentification
|
||||
// @Title VerifyIdentification
|
||||
// @Tag User API
|
||||
// @Description verify user's real identity using ID Verification provider
|
||||
// @Param owner query string false "The owner of the user (optional, defaults to logged-in user)"
|
||||
// @Param name query string false "The name of the user (optional, defaults to logged-in user)"
|
||||
// @Param provider query string false "The name of the ID Verification provider (optional, auto-selected if not provided)"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /verify-identification [post]
|
||||
func (c *ApiController) VerifyIdentification() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
name := c.Ctx.Input.Query("name")
|
||||
providerName := c.Ctx.Input.Query("provider")
|
||||
|
||||
// If user not specified, use logged-in user
|
||||
if owner == "" || name == "" {
|
||||
loggedInUser := c.GetSessionUsername()
|
||||
if loggedInUser == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
var err error
|
||||
owner, name, err = util.GetOwnerAndNameFromIdWithError(loggedInUser)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// If user is specified, check if current user has permission to verify other users
|
||||
// Only admins can verify other users
|
||||
loggedInUser := c.GetSessionUsername()
|
||||
if loggedInUser != util.GetId(owner, name) && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := object.GetUser(util.GetId(owner, name))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(owner, name)))
|
||||
return
|
||||
}
|
||||
|
||||
if user.IdCard == "" || user.IdCardType == "" || user.RealName == "" {
|
||||
c.ResponseError(c.T("user:ID card information and real name are required"))
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsVerified {
|
||||
c.ResponseError(c.T("user:User is already verified"))
|
||||
return
|
||||
}
|
||||
|
||||
var provider *object.Provider
|
||||
// If provider not specified, find suitable IDV provider from user's application
|
||||
if providerName == "" {
|
||||
application, err := object.GetApplicationByUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(c.T("user:No application found for user"))
|
||||
return
|
||||
}
|
||||
|
||||
// Find IDV provider from application
|
||||
idvProvider, err := object.GetIdvProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if idvProvider == nil {
|
||||
c.ResponseError(c.T("provider:No ID Verification provider configured"))
|
||||
return
|
||||
}
|
||||
provider = idvProvider
|
||||
} else {
|
||||
provider, err = object.GetProvider(providerName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), providerName))
|
||||
return
|
||||
}
|
||||
|
||||
if provider.Category != "ID Verification" {
|
||||
c.ResponseError(c.T("provider:Provider is not an ID Verification provider"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
idvProvider := object.GetIdvProviderFromProvider(provider)
|
||||
if idvProvider == nil {
|
||||
c.ResponseError(c.T("provider:Failed to initialize ID Verification provider"))
|
||||
return
|
||||
}
|
||||
|
||||
verified, err := idvProvider.VerifyIdentity(user.IdCardType, user.IdCard, user.RealName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !verified {
|
||||
c.ResponseError(c.T("user:Identity verification failed"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set IsVerified to true upon successful verification
|
||||
user.IsVerified = true
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"is_verified"}, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(user.RealName)
|
||||
}
|
||||
|
||||
@@ -40,8 +40,23 @@ func saveFile(path string, file *multipart.File) (err error) {
|
||||
}
|
||||
|
||||
func (c *ApiController) UploadUsers() {
|
||||
if !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
userObj := c.getCurrentUser()
|
||||
if userObj == nil {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.GetSessionUsername()
|
||||
owner, user := util.GetOwnerAndNameFromId(userId)
|
||||
owner, user, err := util.GetOwnerAndNameFromIdWithError(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Ctx.Request.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -58,7 +73,7 @@ func (c *ApiController) UploadUsers() {
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := object.UploadUsers(owner, path)
|
||||
affected, err := object.UploadUsers(owner, path, userObj, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -67,6 +82,6 @@ func (c *ApiController) UploadUsers() {
|
||||
if affected {
|
||||
c.ResponseOk()
|
||||
} else {
|
||||
c.ResponseError(c.T("user_upload:Failed to import users"))
|
||||
c.ResponseError(c.T("general:Failed to import users"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +54,6 @@ func (c *ApiController) ResponseError(error string, data ...interface{}) {
|
||||
return
|
||||
}
|
||||
|
||||
enableErrorMask := conf.GetConfigBool("enableErrorMask")
|
||||
if enableErrorMask {
|
||||
if strings.HasPrefix(error, "The user: ") && strings.HasSuffix(error, " doesn't exist") || strings.HasPrefix(error, "用户: ") && strings.HasSuffix(error, "不存在") {
|
||||
error = c.T("check:password or code is incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
resp := &Response{Status: "error", Msg: error}
|
||||
c.ResponseJsonData(resp, data...)
|
||||
}
|
||||
@@ -113,7 +106,7 @@ func (c *ApiController) RequireSignedInUser() (*object.User, bool) {
|
||||
}
|
||||
|
||||
if object.IsAppUser(userId) {
|
||||
tmpUserId := c.Input().Get("userId")
|
||||
tmpUserId := c.Ctx.Input.Query("userId")
|
||||
if tmpUserId != "" {
|
||||
userId = tmpUserId
|
||||
}
|
||||
@@ -179,7 +172,7 @@ func (c *ApiController) IsOrgAdmin() (bool, bool) {
|
||||
// IsMaskedEnabled ...
|
||||
func (c *ApiController) IsMaskedEnabled() (bool, bool) {
|
||||
isMaskEnabled := true
|
||||
withSecret := c.Input().Get("withSecret")
|
||||
withSecret := c.Ctx.Input.Query("withSecret")
|
||||
if withSecret == "1" {
|
||||
isMaskEnabled = false
|
||||
|
||||
@@ -209,14 +202,14 @@ func refineFullFilePath(fullFilePath string) (string, string) {
|
||||
}
|
||||
|
||||
func (c *ApiController) GetProviderFromContext(category string) (*object.Provider, error) {
|
||||
providerName := c.Input().Get("provider")
|
||||
providerName := c.Ctx.Input.Query("provider")
|
||||
if providerName == "" {
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
if field == "provider" && value != "" {
|
||||
providerName = value
|
||||
} else {
|
||||
fullFilePath := c.Input().Get("fullFilePath")
|
||||
fullFilePath := c.Ctx.Input.Query("fullFilePath")
|
||||
providerName, _ = refineFullFilePath(fullFilePath)
|
||||
}
|
||||
}
|
||||
@@ -294,12 +287,18 @@ func checkQuotaForProvider(count int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkQuotaForUser(count int) error {
|
||||
func checkQuotaForUser() error {
|
||||
quota := conf.GetConfigQuota().User
|
||||
if quota == -1 {
|
||||
return nil
|
||||
}
|
||||
if count >= quota {
|
||||
|
||||
count, err := object.GetUserCount("", "", "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if int(count) >= quota {
|
||||
return fmt.Errorf("user quota is exceeded")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/captcha"
|
||||
"github.com/casdoor/casdoor/form"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -44,16 +44,27 @@ const (
|
||||
// @Success 200 {array} object.Verification The Response object
|
||||
// @router /get-payments [get]
|
||||
func (c *ApiController) GetVerifications() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
// For global admin with organizationName parameter, use it to filter
|
||||
// For org admin, use their organization
|
||||
if c.IsGlobalAdmin() && owner != "" {
|
||||
organization = owner
|
||||
}
|
||||
|
||||
if limit == "" || page == "" {
|
||||
payments, err := object.GetVerifications(owner)
|
||||
payments, err := object.GetVerifications(organization)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -62,14 +73,14 @@ func (c *ApiController) GetVerifications() {
|
||||
c.ResponseOk(payments)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetVerificationCount(owner, field, value)
|
||||
count, err := object.GetVerificationCount(organization, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
payments, err := object.GetPaginationVerifications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
payments, err := object.GetPaginationVerifications(organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -89,8 +100,8 @@ func (c *ApiController) GetVerifications() {
|
||||
// @Success 200 {array} object.Verification The Response object
|
||||
// @router /get-user-payments [get]
|
||||
func (c *ApiController) GetUserVerifications() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
user := c.Ctx.Input.Query("user")
|
||||
|
||||
payments, err := object.GetUserVerifications(owner, user)
|
||||
if err != nil {
|
||||
@@ -109,7 +120,7 @@ func (c *ApiController) GetUserVerifications() {
|
||||
// @Success 200 {object} object.Verification The Response object
|
||||
// @router /get-payment [get]
|
||||
func (c *ApiController) GetVerification() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
payment, err := object.GetVerification(id)
|
||||
if err != nil {
|
||||
@@ -140,42 +151,33 @@ func (c *ApiController) SendVerificationCode() {
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if provider != nil {
|
||||
if vform.CaptchaType != provider.Type {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
|
||||
if provider.Type != "Default" {
|
||||
vform.ClientSecret = provider.ClientSecret
|
||||
}
|
||||
|
||||
if vform.CaptchaType != "none" {
|
||||
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
|
||||
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
|
||||
return
|
||||
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else if !isHuman {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
application, err := object.GetApplication(vform.ApplicationId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), vform.ApplicationId))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if "Forgot password?" signin item is visible when using forget verification
|
||||
if vform.Method == ForgetVerification {
|
||||
isForgotPasswordEnabled := false
|
||||
for _, item := range application.SigninItems {
|
||||
if item.Name == "Forgot password?" {
|
||||
isForgotPasswordEnabled = item.Visible
|
||||
break
|
||||
}
|
||||
}
|
||||
// Block access if the signin item is not found or is explicitly hidden
|
||||
if !isForgotPasswordEnabled {
|
||||
c.ResponseError(c.T("verification:The forgot password feature is disabled"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
organization, err := object.GetOrganization(util.GetId(application.Owner, application.Organization))
|
||||
if err != nil {
|
||||
c.ResponseError(c.T(err.Error()))
|
||||
@@ -187,6 +189,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
}
|
||||
|
||||
var user *object.User
|
||||
// Try to resolve user for CAPTCHA rule checking
|
||||
// checkUser != "", means method is ForgetVerification
|
||||
if vform.CheckUser != "" {
|
||||
owner := application.Organization
|
||||
@@ -204,18 +207,86 @@ func (c *ApiController) SendVerificationCode() {
|
||||
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// mfaUserSession != "", means method is MfaAuthVerification
|
||||
if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
|
||||
} else if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
|
||||
// mfaUserSession != "", means method is MfaAuthVerification
|
||||
user, err = object.GetUser(mfaUserSession)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else if vform.Method == ResetVerification {
|
||||
// For reset verification, get the current logged-in user
|
||||
user = c.getCurrentUser()
|
||||
} else if vform.Method == LoginVerification {
|
||||
// For login verification, try to find user by email/phone for CAPTCHA check
|
||||
// This is a preliminary lookup; the actual validation happens later in the switch statement
|
||||
if vform.Type == object.VerifyTypeEmail && util.IsEmailValid(vform.Dest) {
|
||||
user, err = object.GetUserByEmail(organization.Name, vform.Dest)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else if vform.Type == object.VerifyTypePhone {
|
||||
// Prefer resolving the user directly by phone, consistent with the later login switch,
|
||||
// so that Dynamic CAPTCHA is not skipped due to missing/invalid country code.
|
||||
user, err = object.GetUserByPhone(organization.Name, vform.Dest)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine username for CAPTCHA check
|
||||
username := ""
|
||||
if user != nil {
|
||||
username = user.Name
|
||||
} else if vform.CheckUser != "" {
|
||||
username = vform.CheckUser
|
||||
}
|
||||
|
||||
// Check if CAPTCHA should be enabled based on the rule (Dynamic/Always/Internet-Only)
|
||||
enableCaptcha, err := object.CheckToEnableCaptcha(application, organization.Name, username, clientIp)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Only verify CAPTCHA if it should be enabled
|
||||
if enableCaptcha {
|
||||
captchaProvider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if captchaProvider != nil {
|
||||
if vform.CaptchaType != captchaProvider.Type {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
|
||||
if captchaProvider.Type != "Default" {
|
||||
vform.ClientSecret = captchaProvider.ClientSecret
|
||||
}
|
||||
|
||||
if vform.CaptchaType != "none" {
|
||||
if captchaService := captcha.GetCaptchaProvider(vform.CaptchaType); captchaService == nil {
|
||||
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
|
||||
return
|
||||
} else if isHuman, err := captchaService.VerifyCaptcha(vform.CaptchaToken, captchaProvider.ClientId, vform.ClientSecret, captchaProvider.ClientId2); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else if !isHuman {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendResp := errors.New("invalid dest type")
|
||||
var provider *object.Provider
|
||||
|
||||
switch vform.Type {
|
||||
case object.VerifyTypeEmail:
|
||||
@@ -242,12 +313,10 @@ func (c *ApiController) SendVerificationCode() {
|
||||
} else if vform.Method == ResetVerification {
|
||||
user = c.getCurrentUser()
|
||||
} else if vform.Method == MfaAuthVerification {
|
||||
mfaProps := user.GetPreferredMfaProps(false)
|
||||
mfaProps := user.GetMfaProps(object.EmailType, false)
|
||||
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
||||
vform.Dest = mfaProps.Secret
|
||||
}
|
||||
} else if vform.Method == MfaSetupVerification {
|
||||
c.SetSession(MfaDestSession, vform.Dest)
|
||||
}
|
||||
|
||||
provider, err = application.GetEmailProvider(vform.Method)
|
||||
@@ -260,7 +329,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
return
|
||||
}
|
||||
|
||||
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, clientIp, vform.Dest)
|
||||
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, clientIp, vform.Dest, vform.Method, c.Ctx.Request.Host, application.Name, application)
|
||||
case object.VerifyTypePhone:
|
||||
if vform.Method == LoginVerification || vform.Method == ForgetVerification {
|
||||
if user != nil && util.GetMaskedPhone(user.Phone) == vform.Dest {
|
||||
@@ -282,18 +351,14 @@ func (c *ApiController) SendVerificationCode() {
|
||||
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
|
||||
}
|
||||
}
|
||||
|
||||
if vform.Method == MfaSetupVerification {
|
||||
c.SetSession(MfaCountryCodeSession, vform.CountryCode)
|
||||
c.SetSession(MfaDestSession, vform.Dest)
|
||||
}
|
||||
} else if vform.Method == MfaAuthVerification {
|
||||
mfaProps := user.GetPreferredMfaProps(false)
|
||||
mfaProps := user.GetMfaProps(object.SmsType, false)
|
||||
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
||||
vform.Dest = mfaProps.Secret
|
||||
}
|
||||
|
||||
vform.CountryCode = mfaProps.CountryCode
|
||||
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
|
||||
}
|
||||
|
||||
provider, err = application.GetSmsProvider(vform.Method, vform.CountryCode)
|
||||
@@ -310,7 +375,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), vform.CountryCode))
|
||||
return
|
||||
} else {
|
||||
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, clientIp, phone)
|
||||
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, clientIp, phone, application)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +420,7 @@ func (c *ApiController) VerifyCaptcha() {
|
||||
return
|
||||
}
|
||||
|
||||
isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret)
|
||||
isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, captchaProvider.ClientId, vform.ClientSecret, captchaProvider.ClientId2)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -441,8 +506,15 @@ func (c *ApiController) ResetEmailOrPhone() {
|
||||
|
||||
switch destType {
|
||||
case object.VerifyTypeEmail:
|
||||
id := user.GetId()
|
||||
user.Email = dest
|
||||
_, err = object.SetUserField(user, "email", user.Email)
|
||||
user.EmailVerified = true
|
||||
columns := []string{"email", "email_verified"}
|
||||
if organization.UseEmailAsUsername {
|
||||
user.Name = user.Email
|
||||
columns = append(columns, "name")
|
||||
}
|
||||
_, err = object.UpdateUser(id, user, columns, false)
|
||||
case object.VerifyTypePhone:
|
||||
user.Phone = dest
|
||||
_, err = object.SetUserField(user, "phone", user.Phone)
|
||||
@@ -454,6 +526,9 @@ func (c *ApiController) ResetEmailOrPhone() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if organization.UseEmailAsUsername {
|
||||
c.SetSessionUsername(user.GetId())
|
||||
}
|
||||
|
||||
err = object.DisableVerificationCode(checkDest)
|
||||
if err != nil {
|
||||
@@ -516,22 +591,31 @@ func (c *ApiController) VerifyCode() {
|
||||
}
|
||||
}
|
||||
|
||||
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Code)
|
||||
if err != nil {
|
||||
c.ResponseError(c.T(err.Error()))
|
||||
return
|
||||
}
|
||||
if result.Code != object.VerificationSuccess {
|
||||
c.ResponseError(result.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
err = object.DisableVerificationCode(checkDest)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
if !passed {
|
||||
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if result.Code != object.VerificationSuccess {
|
||||
c.ResponseError(result.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
err = object.DisableVerificationCode(checkDest)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.SetSession("verifiedCode", authForm.Code)
|
||||
c.SetSession("verifiedUserId", user.GetId())
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
36
controllers/verification_util.go
Normal file
36
controllers/verification_util.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func (c *ApiController) checkOrgMasterVerificationCode(user *object.User, code string) (bool, error) {
|
||||
organization, err := object.GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if organization == nil {
|
||||
return false, fmt.Errorf("The organization: %s does not exist", user.Owner)
|
||||
}
|
||||
|
||||
if organization.MasterVerificationCode != "" && organization.MasterVerificationCode == code {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
@@ -47,6 +48,13 @@ func (c *ApiController) WebAuthnSignupBegin() {
|
||||
|
||||
registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) {
|
||||
credCreationOpts.CredentialExcludeList = user.CredentialExcludeList()
|
||||
credCreationOpts.AuthenticatorSelection.ResidentKey = "preferred"
|
||||
credCreationOpts.Attestation = "none"
|
||||
|
||||
ext := map[string]interface{}{
|
||||
"credProps": true,
|
||||
}
|
||||
credCreationOpts.Extensions = ext
|
||||
}
|
||||
options, sessionData, err := webauthnObj.BeginRegistration(
|
||||
user,
|
||||
@@ -118,24 +126,34 @@ func (c *ApiController) WebAuthnSigninBegin() {
|
||||
return
|
||||
}
|
||||
|
||||
userOwner := c.Input().Get("owner")
|
||||
userName := c.Input().Get("name")
|
||||
user, err := object.GetUserByFields(userOwner, userName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
userOwner := c.Ctx.Input.Query("owner")
|
||||
userName := c.Ctx.Input.Query("name")
|
||||
|
||||
var options *protocol.CredentialAssertion
|
||||
var sessionData *webauthn.SessionData
|
||||
|
||||
if userName == "" {
|
||||
options, sessionData, err = webauthnObj.BeginDiscoverableLogin()
|
||||
} else {
|
||||
var user *object.User
|
||||
user, err = object.GetUserByFields(userOwner, userName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(userOwner, userName)))
|
||||
return
|
||||
}
|
||||
if len(user.WebauthnCredentials) == 0 {
|
||||
c.ResponseError(c.T("webauthn:Found no credentials for this user"))
|
||||
return
|
||||
}
|
||||
|
||||
options, sessionData, err = webauthnObj.BeginLogin(user)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(userOwner, userName)))
|
||||
return
|
||||
}
|
||||
if len(user.WebauthnCredentials) == 0 {
|
||||
c.ResponseError(c.T("webauthn:Found no credentials for this user"))
|
||||
return
|
||||
}
|
||||
|
||||
options, sessionData, err := webauthnObj.BeginLogin(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -153,8 +171,8 @@ func (c *ApiController) WebAuthnSigninBegin() {
|
||||
// @Success 200 {object} controllers.Response "The Response object"
|
||||
// @router /webauthn/signin/finish [post]
|
||||
func (c *ApiController) WebAuthnSigninFinish() {
|
||||
responseType := c.Input().Get("responseType")
|
||||
clientId := c.Input().Get("clientId")
|
||||
responseType := c.Ctx.Input.Query("responseType")
|
||||
clientId := c.Ctx.Input.Query("clientId")
|
||||
webauthnObj, err := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -168,20 +186,35 @@ func (c *ApiController) WebAuthnSigninFinish() {
|
||||
return
|
||||
}
|
||||
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
||||
userId := string(sessionData.UserID)
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
var user *object.User
|
||||
if sessionData.UserID != nil {
|
||||
userId := string(sessionData.UserID)
|
||||
user, err = object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
|
||||
} else {
|
||||
handler := func(rawID, userHandle []byte) (webauthn.User, error) {
|
||||
user, err = object.GetUserByWebauthID(base64.StdEncoding.EncodeToString(rawID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
_, err = webauthnObj.FinishDiscoverableLogin(handler, sessionData, c.Ctx.Request)
|
||||
}
|
||||
|
||||
_, err = webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.SetSessionUsername(userId)
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||
c.SetSessionUsername(user.GetId())
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", user.GetId())
|
||||
|
||||
var application *object.Application
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -31,14 +31,14 @@ import (
|
||||
// @router /get-webhooks [get]
|
||||
// @Security test_apiKey
|
||||
func (c *ApiController) GetWebhooks() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
webhooks, err := object.GetWebhooks(owner, organization)
|
||||
@@ -56,7 +56,7 @@ func (c *ApiController) GetWebhooks() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
|
||||
webhooks, err := object.GetPaginationWebhooks(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
@@ -76,7 +76,7 @@ func (c *ApiController) GetWebhooks() {
|
||||
// @Success 200 {object} object.Webhook The Response object
|
||||
// @router /get-webhook [get]
|
||||
func (c *ApiController) GetWebhook() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
webhook, err := object.GetWebhook(id)
|
||||
if err != nil {
|
||||
@@ -96,7 +96,7 @@ func (c *ApiController) GetWebhook() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-webhook [post]
|
||||
func (c *ApiController) UpdateWebhook() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var webhook object.Webhook
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &webhook)
|
||||
@@ -105,7 +105,7 @@ func (c *ApiController) UpdateWebhook() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateWebhook(id, &webhook))
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateWebhook(id, &webhook, c.IsGlobalAdmin(), c.GetAcceptLanguage()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
||||
45
controllers/wellknown_oauth_prm.go
Normal file
45
controllers/wellknown_oauth_prm.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
// GetOauthProtectedResourceMetadata
|
||||
// @Title GetOauthProtectedResourceMetadata
|
||||
// @Tag OAuth 2.0 API
|
||||
// @Description Get OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
||||
// @Success 200 {object} object.OauthProtectedResourceMetadata
|
||||
// @router /.well-known/oauth-protected-resource [get]
|
||||
func (c *RootController) GetOauthProtectedResourceMetadata() {
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOauthProtectedResourceMetadata(host)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetOauthProtectedResourceMetadataByApplication
|
||||
// @Title GetOauthProtectedResourceMetadataByApplication
|
||||
// @Tag OAuth 2.0 API
|
||||
// @Description Get OAuth 2.0 Protected Resource Metadata for specific application (RFC 9728)
|
||||
// @Param application path string true "application name"
|
||||
// @Success 200 {object} object.OauthProtectedResourceMetadata
|
||||
// @router /.well-known/:application/oauth-protected-resource [get]
|
||||
func (c *RootController) GetOauthProtectedResourceMetadataByApplication() {
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOauthProtectedResourceMetadataByApplication(host, application)
|
||||
c.ServeJSON()
|
||||
}
|
||||
165
controllers/wellknown_oidc_discovery.go
Normal file
165
controllers/wellknown_oidc_discovery.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
// GetOidcDiscovery
|
||||
// @Title GetOidcDiscovery
|
||||
// @Tag OIDC API
|
||||
// @Description Get Oidc Discovery
|
||||
// @Success 200 {object} object.OidcDiscovery
|
||||
// @router /.well-known/openid-configuration [get]
|
||||
func (c *RootController) GetOidcDiscovery() {
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOidcDiscovery(host, "")
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetOidcDiscoveryByApplication
|
||||
// @Title GetOidcDiscoveryByApplication
|
||||
// @Tag OIDC API
|
||||
// @Description Get Oidc Discovery for specific application
|
||||
// @Param application path string true "application name"
|
||||
// @Success 200 {object} object.OidcDiscovery
|
||||
// @router /.well-known/:application/openid-configuration [get]
|
||||
func (c *RootController) GetOidcDiscoveryByApplication() {
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOidcDiscovery(host, application)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetJwks
|
||||
// @Title GetJwks
|
||||
// @Tag OIDC API
|
||||
// @Success 200 {object} jose.JSONWebKey
|
||||
// @router /.well-known/jwks [get]
|
||||
func (c *RootController) GetJwks() {
|
||||
jwks, err := object.GetJsonWebKeySet("")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = jwks
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetJwksByApplication
|
||||
// @Title GetJwksByApplication
|
||||
// @Tag OIDC API
|
||||
// @Param application path string true "application name"
|
||||
// @Success 200 {object} jose.JSONWebKey
|
||||
// @router /.well-known/:application/jwks [get]
|
||||
func (c *RootController) GetJwksByApplication() {
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
jwks, err := object.GetJsonWebKeySet(application)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = jwks
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetWebFinger
|
||||
// @Title GetWebFinger
|
||||
// @Tag OIDC API
|
||||
// @Param resource query string true "resource"
|
||||
// @Success 200 {object} object.WebFinger
|
||||
// @router /.well-known/webfinger [get]
|
||||
func (c *RootController) GetWebFinger() {
|
||||
resource := c.Ctx.Input.Query("resource")
|
||||
rels := []string{}
|
||||
host := c.Ctx.Request.Host
|
||||
|
||||
inputs, _ := c.Input()
|
||||
for key, value := range inputs {
|
||||
if strings.HasPrefix(key, "rel") {
|
||||
rels = append(rels, value...)
|
||||
}
|
||||
}
|
||||
|
||||
webfinger, err := object.GetWebFinger(resource, rels, host, "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = webfinger
|
||||
c.Ctx.Output.ContentType("application/jrd+json")
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetWebFingerByApplication
|
||||
// @Title GetWebFingerByApplication
|
||||
// @Tag OIDC API
|
||||
// @Param application path string true "application name"
|
||||
// @Param resource query string true "resource"
|
||||
// @Success 200 {object} object.WebFinger
|
||||
// @router /.well-known/:application/webfinger [get]
|
||||
func (c *RootController) GetWebFingerByApplication() {
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
resource := c.Ctx.Input.Query("resource")
|
||||
rels := []string{}
|
||||
host := c.Ctx.Request.Host
|
||||
|
||||
inputs, _ := c.Input()
|
||||
for key, value := range inputs {
|
||||
if strings.HasPrefix(key, "rel") {
|
||||
rels = append(rels, value...)
|
||||
}
|
||||
}
|
||||
|
||||
webfinger, err := object.GetWebFinger(resource, rels, host, application)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = webfinger
|
||||
c.Ctx.Output.ContentType("application/jrd+json")
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetOAuthServerMetadata
|
||||
// @Title GetOAuthServerMetadata
|
||||
// @Tag OAuth API
|
||||
// @Description Get OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||
// @Success 200 {object} object.OidcDiscovery
|
||||
// @router /.well-known/oauth-authorization-server [get]
|
||||
func (c *RootController) GetOAuthServerMetadata() {
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOidcDiscovery(host, "")
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetOAuthServerMetadataByApplication
|
||||
// @Title GetOAuthServerMetadataByApplication
|
||||
// @Tag OAuth API
|
||||
// @Description Get OAuth 2.0 Authorization Server Metadata for specific application (RFC 8414)
|
||||
// @Param application path string true "application name"
|
||||
// @Success 200 {object} object.OidcDiscovery
|
||||
// @router /.well-known/:application/oauth-authorization-server [get]
|
||||
func (c *RootController) GetOAuthServerMetadataByApplication() {
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOidcDiscovery(host, application)
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func NewArgon2idCredManager() *Argon2idCredManager {
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *Argon2idCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
func (cm *Argon2idCredManager) GetHashedPassword(password string, salt string) string {
|
||||
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -31,7 +31,7 @@ func (cm *Argon2idCredManager) GetHashedPassword(password string, userSalt strin
|
||||
return hash
|
||||
}
|
||||
|
||||
func (cm *Argon2idCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
func (cm *Argon2idCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
|
||||
match, _ := argon2id.ComparePasswordAndHash(plainPwd, hashedPwd)
|
||||
return match
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ func NewBcryptCredManager() *BcryptCredManager {
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *BcryptCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
func (cm *BcryptCredManager) GetHashedPassword(password string, salt string) string {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -17,7 +17,7 @@ func (cm *BcryptCredManager) GetHashedPassword(password string, userSalt string,
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (cm *BcryptCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
func (cm *BcryptCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(plainPwd))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
package cred
|
||||
|
||||
type CredManager interface {
|
||||
GetHashedPassword(password string, userSalt string, organizationSalt string) string
|
||||
IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool
|
||||
GetHashedPassword(password string, salt string) string
|
||||
IsPasswordCorrect(password string, passwordHash string, salt string) bool
|
||||
}
|
||||
|
||||
func GetCredManager(passwordType string) CredManager {
|
||||
@@ -34,6 +34,8 @@ func GetCredManager(passwordType string) CredManager {
|
||||
return NewPbkdf2SaltCredManager()
|
||||
} else if passwordType == "argon2id" {
|
||||
return NewArgon2idCredManager()
|
||||
} else if passwordType == "pbkdf2-django" {
|
||||
return NewPbkdf2DjangoCredManager()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -37,14 +37,21 @@ func NewMd5UserSaltCredManager() *Md5UserSaltCredManager {
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
res := getMd5HexDigest(password)
|
||||
if userSalt != "" {
|
||||
res = getMd5HexDigest(res + userSalt)
|
||||
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, salt string) string {
|
||||
if salt == "" {
|
||||
return getMd5HexDigest(password)
|
||||
}
|
||||
return res
|
||||
|
||||
return getMd5HexDigest(getMd5HexDigest(password) + salt)
|
||||
}
|
||||
|
||||
func (cm *Md5UserSaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
|
||||
func (cm *Md5UserSaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
|
||||
// For backward-compatibility
|
||||
if salt == "" {
|
||||
if hashedPwd == cm.GetHashedPassword(getMd5HexDigest(plainPwd), salt) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@ func NewPbkdf2SaltCredManager() *Pbkdf2SaltCredManager {
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *Pbkdf2SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
func (cm *Pbkdf2SaltCredManager) GetHashedPassword(password string, salt string) string {
|
||||
// https://www.keycloak.org/docs/latest/server_admin/index.html#password-database-compromised
|
||||
decodedSalt, _ := base64.StdEncoding.DecodeString(userSalt)
|
||||
decodedSalt, _ := base64.StdEncoding.DecodeString(salt)
|
||||
res := pbkdf2.Key([]byte(password), decodedSalt, 27500, 64, sha256.New)
|
||||
return base64.StdEncoding.EncodeToString(res)
|
||||
}
|
||||
|
||||
func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
|
||||
func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
|
||||
}
|
||||
|
||||
67
cred/pbkdf2_django.go
Normal file
67
cred/pbkdf2_django.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cred
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// password type: pbkdf2-django
|
||||
|
||||
type Pbkdf2DjangoCredManager struct{}
|
||||
|
||||
func NewPbkdf2DjangoCredManager() *Pbkdf2DjangoCredManager {
|
||||
cm := &Pbkdf2DjangoCredManager{}
|
||||
return cm
|
||||
}
|
||||
|
||||
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, salt string) string {
|
||||
iterations := 260000
|
||||
|
||||
saltBytes := []byte(salt)
|
||||
passwordBytes := []byte(password)
|
||||
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iterations, sha256.Size, sha256.New)
|
||||
hashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||
return "pbkdf2_sha256$" + strconv.Itoa(iterations) + "$" + salt + "$" + hashBase64
|
||||
}
|
||||
|
||||
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, _salt string) bool {
|
||||
parts := strings.Split(passwordHash, "$")
|
||||
if len(parts) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
algorithm, iterations, salt, hash := parts[0], parts[1], parts[2], parts[3]
|
||||
if algorithm != "pbkdf2_sha256" {
|
||||
return false
|
||||
}
|
||||
|
||||
iter, err := strconv.Atoi(iterations)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
saltBytes := []byte(salt)
|
||||
passwordBytes := []byte(password)
|
||||
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iter, sha256.Size, sha256.New)
|
||||
computedHashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||
|
||||
return computedHashBase64 == hash
|
||||
}
|
||||
@@ -21,10 +21,10 @@ func NewPlainCredManager() *PlainCredManager {
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *PlainCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
func (cm *PlainCredManager) GetHashedPassword(password string, salt string) string {
|
||||
return password
|
||||
}
|
||||
|
||||
func (cm *PlainCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
func (cm *PlainCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
|
||||
return hashedPwd == plainPwd
|
||||
}
|
||||
|
||||
@@ -37,14 +37,21 @@ func NewSha256SaltCredManager() *Sha256SaltCredManager {
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
res := getSha256HexDigest(password)
|
||||
if organizationSalt != "" {
|
||||
res = getSha256HexDigest(res + organizationSalt)
|
||||
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, salt string) string {
|
||||
if salt == "" {
|
||||
return getSha256HexDigest(password)
|
||||
}
|
||||
return res
|
||||
|
||||
return getSha256HexDigest(getSha256HexDigest(password) + salt)
|
||||
}
|
||||
|
||||
func (cm *Sha256SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
|
||||
func (cm *Sha256SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
|
||||
// For backward-compatibility
|
||||
if salt == "" {
|
||||
if hashedPwd == cm.GetHashedPassword(getSha256HexDigest(plainPwd), salt) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ func TestGetSaltedPassword(t *testing.T) {
|
||||
password := "123456"
|
||||
salt := "123"
|
||||
cm := NewSha256SaltCredManager()
|
||||
fmt.Printf("%s -> %s\n", password, cm.GetHashedPassword(password, "", salt))
|
||||
fmt.Printf("%s -> %s\n", password, cm.GetHashedPassword(password, salt))
|
||||
}
|
||||
|
||||
func TestGetPassword(t *testing.T) {
|
||||
password := "123456"
|
||||
cm := NewSha256SaltCredManager()
|
||||
// https://passwordsgenerator.net/sha256-hash-generator/
|
||||
fmt.Printf("%s -> %s\n", "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", cm.GetHashedPassword(password, "", ""))
|
||||
fmt.Printf("%s -> %s\n", "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", cm.GetHashedPassword(password, ""))
|
||||
}
|
||||
|
||||
@@ -37,14 +37,21 @@ func NewSha512SaltCredManager() *Sha512SaltCredManager {
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
res := getSha512HexDigest(password)
|
||||
if organizationSalt != "" {
|
||||
res = getSha512HexDigest(res + organizationSalt)
|
||||
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, salt string) string {
|
||||
if salt == "" {
|
||||
return getSha512HexDigest(password)
|
||||
}
|
||||
return res
|
||||
|
||||
return getSha512HexDigest(getSha512HexDigest(password) + salt)
|
||||
}
|
||||
|
||||
func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
|
||||
func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
|
||||
// For backward-compatibility
|
||||
if salt == "" {
|
||||
if hashedPwd == cm.GetHashedPassword(getSha512HexDigest(plainPwd), salt) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package deployment
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/bin/bash
|
||||
if [ "${MYSQL_ROOT_PASSWORD}" = "" ] ;then MYSQL_ROOT_PASSWORD=123456 ;fi
|
||||
|
||||
service mariadb start
|
||||
if [ -z "${driverName:-}" ]; then
|
||||
export driverName=sqlite
|
||||
fi
|
||||
if [ -z "${dataSourceName:-}" ]; then
|
||||
export dataSourceName="file:casdoor.db?cache=shared"
|
||||
fi
|
||||
|
||||
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD}
|
||||
|
||||
exec /server --createDatabase=true
|
||||
exec /server
|
||||
|
||||
@@ -96,15 +96,17 @@ func NewAzureACSEmailProvider(accessKey string, endpoint string) *AzureACSEmailP
|
||||
}
|
||||
}
|
||||
|
||||
func newEmail(fromAddress string, toAddress string, subject string, content string) *Email {
|
||||
func newEmail(fromAddress string, toAddress []string, subject string, content string) *Email {
|
||||
var to []EmailAddress
|
||||
for _, addr := range toAddress {
|
||||
to = append(to, EmailAddress{
|
||||
DisplayName: addr,
|
||||
Address: addr,
|
||||
})
|
||||
}
|
||||
return &Email{
|
||||
Recipients: Recipients{
|
||||
To: []EmailAddress{
|
||||
{
|
||||
DisplayName: toAddress,
|
||||
Address: toAddress,
|
||||
},
|
||||
},
|
||||
To: to,
|
||||
},
|
||||
SenderAddress: fromAddress,
|
||||
Content: Content{
|
||||
@@ -116,7 +118,7 @@ func newEmail(fromAddress string, toAddress string, subject string, content stri
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||
func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress []string, subject string, content string) error {
|
||||
email := newEmail(fromAddress, toAddress, subject, content)
|
||||
|
||||
postBody, err := json.Marshal(email)
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -24,33 +26,89 @@ import (
|
||||
)
|
||||
|
||||
type HttpEmailProvider struct {
|
||||
endpoint string
|
||||
method string
|
||||
endpoint string
|
||||
method string
|
||||
httpHeaders map[string]string
|
||||
bodyMapping map[string]string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
|
||||
func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) *HttpEmailProvider {
|
||||
if contentType == "" {
|
||||
contentType = "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
client := &HttpEmailProvider{
|
||||
endpoint: endpoint,
|
||||
method: method,
|
||||
endpoint: endpoint,
|
||||
method: method,
|
||||
httpHeaders: httpHeaders,
|
||||
bodyMapping: bodyMapping,
|
||||
contentType: contentType,
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress []string, subject string, content string) error {
|
||||
var req *http.Request
|
||||
var err error
|
||||
if c.method == "POST" {
|
||||
formValues := url.Values{}
|
||||
formValues.Set("fromName", fromName)
|
||||
formValues.Set("toAddress", toAddress)
|
||||
formValues.Set("subject", subject)
|
||||
formValues.Set("content", content)
|
||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||
|
||||
fromAddressField := "fromAddress"
|
||||
fromNameField := "fromName"
|
||||
toAddressField := "toAddress"
|
||||
toAddressesField := "toAddresses"
|
||||
subjectField := "subject"
|
||||
contentField := "content"
|
||||
for k, v := range c.bodyMapping {
|
||||
switch k {
|
||||
case "fromAddress":
|
||||
fromAddressField = v
|
||||
case "fromName":
|
||||
fromNameField = v
|
||||
case "toAddress":
|
||||
toAddressField = v
|
||||
case "toAddresses":
|
||||
toAddressesField = v
|
||||
case "subject":
|
||||
subjectField = v
|
||||
case "content":
|
||||
contentField = v
|
||||
}
|
||||
}
|
||||
|
||||
if c.method == "POST" || c.method == "PUT" || c.method == "DELETE" {
|
||||
bodyMap := make(map[string]string)
|
||||
bodyMap[fromAddressField] = fromAddress
|
||||
bodyMap[fromNameField] = fromName
|
||||
bodyMap[subjectField] = subject
|
||||
bodyMap[contentField] = content
|
||||
|
||||
var fromValueBytes []byte
|
||||
if c.contentType == "application/json" {
|
||||
fromValueBytes, err = json.Marshal(bodyMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest(c.method, c.endpoint, bytes.NewBuffer(fromValueBytes))
|
||||
} else {
|
||||
formValues := url.Values{}
|
||||
for k, v := range bodyMap {
|
||||
formValues.Add(k, v)
|
||||
}
|
||||
if len(toAddress) == 1 {
|
||||
formValues.Add(toAddressField, toAddress[0])
|
||||
} else {
|
||||
for _, addr := range toAddress {
|
||||
formValues.Add(toAddressesField, addr)
|
||||
}
|
||||
}
|
||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", c.contentType)
|
||||
} else if c.method == "GET" {
|
||||
req, err = http.NewRequest(c.method, c.endpoint, nil)
|
||||
if err != nil {
|
||||
@@ -58,15 +116,26 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("fromName", fromName)
|
||||
q.Add("toAddress", toAddress)
|
||||
q.Add("subject", subject)
|
||||
q.Add("content", content)
|
||||
q.Add(fromAddressField, fromAddress)
|
||||
q.Add(fromNameField, fromName)
|
||||
if len(toAddress) == 1 {
|
||||
q.Add(toAddressField, toAddress[0])
|
||||
} else {
|
||||
for _, addr := range toAddress {
|
||||
q.Add(toAddressesField, addr)
|
||||
}
|
||||
}
|
||||
q.Add(subjectField, subject)
|
||||
q.Add(contentField, content)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
} else {
|
||||
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
|
||||
}
|
||||
|
||||
for k, v := range c.httpHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
httpClient := proxy.DefaultHttpClient
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,17 +15,17 @@
|
||||
package email
|
||||
|
||||
type EmailProvider interface {
|
||||
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
|
||||
Send(fromAddress string, fromName string, toAddress []string, subject string, content string) error
|
||||
}
|
||||
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string) EmailProvider {
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, sslMode string, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string, enableProxy bool) EmailProvider {
|
||||
if typ == "Azure ACS" {
|
||||
return NewAzureACSEmailProvider(clientSecret, host)
|
||||
} else if typ == "Custom HTTP Email" {
|
||||
return NewHttpEmailProvider(endpoint, method)
|
||||
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
|
||||
} else if typ == "SendGrid" {
|
||||
return NewSendgridEmailProvider(clientSecret)
|
||||
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||
} else {
|
||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
|
||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, sslMode, enableProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,16 @@ package email
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"net/http"
|
||||
|
||||
"github.com/sendgrid/sendgrid-go"
|
||||
"github.com/sendgrid/sendgrid-go/helpers/mail"
|
||||
)
|
||||
|
||||
type SendgridEmailProvider struct {
|
||||
ApiKey string
|
||||
ApiKey string
|
||||
Host string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
type SendgridResponseBody struct {
|
||||
@@ -35,23 +37,36 @@ type SendgridResponseBody struct {
|
||||
} `json:"errors"`
|
||||
}
|
||||
|
||||
func NewSendgridEmailProvider(apiKey string) *SendgridEmailProvider {
|
||||
return &SendgridEmailProvider{ApiKey: apiKey}
|
||||
func NewSendgridEmailProvider(apiKey string, host string, endpoint string) *SendgridEmailProvider {
|
||||
return &SendgridEmailProvider{ApiKey: apiKey, Host: host, Endpoint: endpoint}
|
||||
}
|
||||
|
||||
func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress string, subject string, content string) error {
|
||||
func (s *SendgridEmailProvider) Send(fromAddress string, fromName string, toAddresses []string, subject string, content string) error {
|
||||
from := mail.NewEmail(fromName, fromAddress)
|
||||
to := mail.NewEmail("", toAddress)
|
||||
message := mail.NewSingleEmail(from, subject, to, "", content)
|
||||
client := sendgrid.NewSendClient(s.ApiKey)
|
||||
response, err := client.Send(message)
|
||||
message := mail.NewV3Mail()
|
||||
message.SetFrom(from)
|
||||
message.AddContent(mail.NewContent("text/html", content))
|
||||
|
||||
personalization := mail.NewPersonalization()
|
||||
|
||||
for _, toAddress := range toAddresses {
|
||||
to := mail.NewEmail(toAddress, toAddress)
|
||||
personalization.AddTos(to)
|
||||
}
|
||||
|
||||
personalization.Subject = subject
|
||||
|
||||
message.AddPersonalizations(personalization)
|
||||
|
||||
client := s.initSendgridClient()
|
||||
resp, err := client.Send(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.StatusCode >= 300 {
|
||||
if resp.StatusCode >= 300 {
|
||||
var responseBody SendgridResponseBody
|
||||
err = json.Unmarshal([]byte(response.Body), &responseBody)
|
||||
err = json.Unmarshal([]byte(resp.Body), &responseBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,8 +76,23 @@ func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress str
|
||||
messages = append(messages, sendgridError.Message)
|
||||
}
|
||||
|
||||
return fmt.Errorf("SendGrid status code: %d, error message: %s", response.StatusCode, strings.Join(messages, " | "))
|
||||
return fmt.Errorf("status code: %d, error message: %s", resp.StatusCode, messages)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SendgridEmailProvider) initSendgridClient() *sendgrid.Client {
|
||||
if s.Host == "" || s.Endpoint == "" {
|
||||
return sendgrid.NewSendClient(s.ApiKey)
|
||||
}
|
||||
|
||||
request := sendgrid.GetRequest(s.ApiKey, s.Endpoint, s.Host)
|
||||
request.Method = "POST"
|
||||
|
||||
return &sendgrid.Client{Request: request}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package email
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/gomail/v2"
|
||||
)
|
||||
|
||||
@@ -24,23 +25,40 @@ type SmtpEmailProvider struct {
|
||||
Dialer *gomail.Dialer
|
||||
}
|
||||
|
||||
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, disableSsl bool) *SmtpEmailProvider {
|
||||
dialer := &gomail.Dialer{}
|
||||
dialer = gomail.NewDialer(host, port, userName, password)
|
||||
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, sslMode string, enableProxy bool) *SmtpEmailProvider {
|
||||
dialer := gomail.NewDialer(host, port, userName, password)
|
||||
if typ == "SUBMAIL" {
|
||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
dialer.SSL = !disableSsl
|
||||
// Handle SSL mode: "Auto" (or empty) means don't override gomail's default behavior
|
||||
// "Enable" means force SSL on, "Disable" means force SSL off
|
||||
if sslMode == "Enable" {
|
||||
dialer.SSL = true
|
||||
} else if sslMode == "Disable" {
|
||||
dialer.SSL = false
|
||||
}
|
||||
// If sslMode is "Auto" or empty, don't set dialer.SSL - let gomail decide based on port
|
||||
|
||||
if enableProxy {
|
||||
socks5Proxy := conf.GetConfigString("socks5Proxy")
|
||||
if socks5Proxy != "" {
|
||||
dialer.SetSocks5Proxy(socks5Proxy)
|
||||
}
|
||||
}
|
||||
|
||||
return &SmtpEmailProvider{Dialer: dialer}
|
||||
}
|
||||
|
||||
func (s *SmtpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||
func (s *SmtpEmailProvider) Send(fromAddress string, fromName string, toAddresses []string, subject string, content string) error {
|
||||
message := gomail.NewMessage()
|
||||
|
||||
message.SetAddressHeader("From", fromAddress, fromName)
|
||||
message.SetHeader("To", toAddress)
|
||||
var addresses []string
|
||||
for _, address := range toAddresses {
|
||||
addresses = append(addresses, message.FormatAddress(address, ""))
|
||||
}
|
||||
message.SetHeader("To", addresses...)
|
||||
message.SetHeader("Subject", subject)
|
||||
message.SetBody("text/html", content)
|
||||
|
||||
|
||||
81
faceId/aliyun.go
Normal file
81
faceId/aliyun.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package faceId
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
facebody20191230 "github.com/alibabacloud-go/facebody-20191230/v5/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
type AliyunFaceIdProvider struct {
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
|
||||
Endpoint string
|
||||
QualityScoreThreshold float32
|
||||
}
|
||||
|
||||
func NewAliyunFaceIdProvider(accessKey string, accessSecret string, endPoint string) *AliyunFaceIdProvider {
|
||||
return &AliyunFaceIdProvider{
|
||||
AccessKey: accessKey,
|
||||
AccessSecret: accessSecret,
|
||||
Endpoint: endPoint,
|
||||
QualityScoreThreshold: 0.65,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *AliyunFaceIdProvider) Check(base64ImageA string, base64ImageB string) (bool, error) {
|
||||
config := openapi.Config{
|
||||
AccessKeyId: tea.String(provider.AccessKey),
|
||||
AccessKeySecret: tea.String(provider.AccessSecret),
|
||||
}
|
||||
config.Endpoint = tea.String(provider.Endpoint)
|
||||
client, err := facebody20191230.NewClient(&config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
compareFaceRequest := &facebody20191230.CompareFaceRequest{
|
||||
QualityScoreThreshold: tea.Float32(provider.QualityScoreThreshold),
|
||||
ImageDataA: tea.String(strings.Replace(base64ImageA, "data:image/png;base64,", "", -1)),
|
||||
ImageDataB: tea.String(strings.Replace(base64ImageB, "data:image/png;base64,", "", -1)),
|
||||
}
|
||||
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
defer func() {
|
||||
if r := tea.Recover(recover()); r != nil {
|
||||
err = r
|
||||
}
|
||||
}()
|
||||
result, err := client.CompareFaceWithOptions(compareFaceRequest, runtime)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if *result.Body.Data.Thresholds[0] < *result.Body.Data.Confidence {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
23
faceId/provider.go
Normal file
23
faceId/provider.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package faceId
|
||||
|
||||
type FaceIdProvider interface {
|
||||
Check(base64ImageA string, base64ImageB string) (bool, error)
|
||||
}
|
||||
|
||||
func GetFaceIdProvider(typ string, clientId string, clientSecret string, endPoint string) FaceIdProvider {
|
||||
return NewAliyunFaceIdProvider(clientId, clientSecret, endPoint)
|
||||
}
|
||||
28
form/auth.go
28
form/auth.go
@@ -34,16 +34,19 @@ type AuthForm struct {
|
||||
Phone string `json:"phone"`
|
||||
Affiliation string `json:"affiliation"`
|
||||
IdCard string `json:"idCard"`
|
||||
Language string `json:"language"`
|
||||
Region string `json:"region"`
|
||||
InvitationCode string `json:"invitationCode"`
|
||||
|
||||
Application string `json:"application"`
|
||||
ClientId string `json:"clientId"`
|
||||
Provider string `json:"provider"`
|
||||
Code string `json:"code"`
|
||||
State string `json:"state"`
|
||||
RedirectUri string `json:"redirectUri"`
|
||||
Method string `json:"method"`
|
||||
Application string `json:"application"`
|
||||
ClientId string `json:"clientId"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderBack string `json:"providerBack"`
|
||||
Code string `json:"code"`
|
||||
State string `json:"state"`
|
||||
RedirectUri string `json:"redirectUri"`
|
||||
Method string `json:"method"`
|
||||
CodeVerifier string `json:"codeVerifier"`
|
||||
|
||||
EmailCode string `json:"emailCode"`
|
||||
PhoneCode string `json:"phoneCode"`
|
||||
@@ -59,14 +62,17 @@ type AuthForm struct {
|
||||
CaptchaToken string `json:"captchaToken"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
|
||||
MfaType string `json:"mfaType"`
|
||||
Passcode string `json:"passcode"`
|
||||
RecoveryCode string `json:"recoveryCode"`
|
||||
MfaType string `json:"mfaType"`
|
||||
Passcode string `json:"passcode"`
|
||||
RecoveryCode string `json:"recoveryCode"`
|
||||
EnableMfaRemember bool `json:"enableMfaRemember"`
|
||||
|
||||
Plan string `json:"plan"`
|
||||
Pricing string `json:"pricing"`
|
||||
|
||||
FaceId []float64 `json:"faceId"`
|
||||
FaceId []float64 `json:"faceId"`
|
||||
FaceIdImage []string `json:"faceIdImage"`
|
||||
UserCode string `json:"userCode"`
|
||||
}
|
||||
|
||||
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {
|
||||
|
||||
@@ -47,7 +47,7 @@ func (form *VerificationForm) CheckParameter(checkType int, lang string) string
|
||||
return i18n.Translate(lang, "general:Missing parameter") + ": dest."
|
||||
}
|
||||
if form.CaptchaType == "" {
|
||||
return i18n.Translate(lang, "general:Missing parameter") + ": checkType."
|
||||
return i18n.Translate(lang, "general:Missing parameter") + ": captchaType."
|
||||
}
|
||||
if !strings.Contains(form.ApplicationId, "/") {
|
||||
return i18n.Translate(lang, "verification:Wrong parameter") + ": applicationId."
|
||||
|
||||
278
go.mod
278
go.mod
@@ -1,72 +1,286 @@
|
||||
module github.com/casdoor/casdoor
|
||||
|
||||
go 1.16
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
github.com/NdoleStudio/lemonsqueezy-go v1.2.4
|
||||
github.com/PaddleHQ/paddle-go-sdk v1.0.0
|
||||
github.com/adyen/adyen-go-api-library/v11 v11.0.0
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/alibabacloud-go/cloudauth-20190307/v3 v3.9.2
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
|
||||
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0
|
||||
github.com/alibabacloud-go/tea v1.3.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible
|
||||
github.com/aliyun/credentials-go v1.3.10
|
||||
github.com/aws/aws-sdk-go v1.45.5
|
||||
github.com/beego/beego v1.12.12
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||
github.com/beego/beego/v2 v2.3.8
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/casbin/casbin/v2 v2.77.2
|
||||
github.com/casdoor/go-sms-sender v0.24.0
|
||||
github.com/casdoor/gomail/v2 v2.0.1
|
||||
github.com/casdoor/go-sms-sender v0.25.0
|
||||
github.com/casdoor/gomail/v2 v2.2.0
|
||||
github.com/casdoor/ldapserver v1.2.0
|
||||
github.com/casdoor/notify v0.45.0
|
||||
github.com/casdoor/notify2 v1.6.0
|
||||
github.com/casdoor/oss v1.8.0
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/denisenkom/go-mssqldb v0.9.0
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5
|
||||
github.com/go-git/go-git/v5 v5.11.0
|
||||
github.com/go-git/go-git/v5 v5.16.3
|
||||
github.com/go-jose/go-jose/v4 v4.1.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
github.com/go-mysql-org/go-mysql v1.7.0
|
||||
github.com/go-pay/gopay v1.5.72
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-pay/gopay v1.5.115
|
||||
github.com/go-pay/util v0.0.4
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/go-webauthn/webauthn v0.6.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lestrrat-go/jwx v1.2.29
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
|
||||
github.com/markbates/goth v1.79.0
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/microsoft/go-mssqldb v1.9.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nyaruka/phonenumbers v1.1.5
|
||||
github.com/nyaruka/phonenumbers v1.2.2
|
||||
github.com/polarsource/polar-go v0.12.0
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.11.1
|
||||
github.com/prometheus/client_model v0.4.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/prometheus/client_model v0.6.0
|
||||
github.com/qiangmzsx/string-adapter/v2 v2.1.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russellhaering/gosaml2 v0.9.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/sendgrid/sendgrid-go v3.16.0+incompatible
|
||||
github.com/shirou/gopsutil/v4 v4.25.9
|
||||
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stripe/stripe-go/v74 v74.29.0
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
github.com/thanhpk/randstr v1.0.4
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/xorm-io/builder v0.3.13
|
||||
github.com/xorm-io/core v0.7.4
|
||||
github.com/xorm-io/xorm v1.1.6
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
google.golang.org/api v0.150.0
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
|
||||
maunium.net/go/mautrix v0.16.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/oauth2 v0.27.0
|
||||
golang.org/x/text v0.27.0
|
||||
google.golang.org/api v0.215.0
|
||||
layeh.com/radius v0.0.0-20231213012653-1006025d24f8
|
||||
maunium.net/go/mautrix v0.22.1
|
||||
modernc.org/sqlite v1.18.2
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.18.0 // indirect
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/iam v1.2.2 // indirect
|
||||
cloud.google.com/go/monitoring v1.21.2 // indirect
|
||||
cloud.google.com/go/storage v1.47.0 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20240116134246-a8cbe886bab0 // indirect
|
||||
github.com/SherClockHolmes/webpush-go v1.4.0 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-number v1.0.4 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
|
||||
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
|
||||
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.3.6 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/bwmarrin/discordgo v0.28.1 // indirect
|
||||
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.3 // indirect
|
||||
github.com/dghubble/sling v1.4.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/di-wu/parser v0.2.2 // indirect
|
||||
github.com/di-wu/xsd-datetime v1.0.0 // indirect
|
||||
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.13.1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/ggicci/httpin v0.19.0 // indirect
|
||||
github.com/ggicci/owl v0.8.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-lark/lark v1.15.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-pay/crypto v0.0.1 // indirect
|
||||
github.com/go-pay/errgroup v0.0.3 // indirect
|
||||
github.com/go-pay/smap v0.0.2 // indirect
|
||||
github.com/go-pay/xlog v0.0.3 // indirect
|
||||
github.com/go-pay/xtime v0.0.2 // indirect
|
||||
github.com/go-webauthn/x v0.1.9 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/gregdel/pushover v1.3.1 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/markbates/going v1.0.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mileusna/viber v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
|
||||
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
|
||||
github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.5.5 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rs/zerolog v1.33.0 // indirect
|
||||
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
|
||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/slack-go/slack v0.15.0 // indirect
|
||||
github.com/spyzhov/ajson v0.8.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.744 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/twilio/twilio-go v1.13.0 // indirect
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
|
||||
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mau.fi/util v0.8.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.19.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/grpc v1.68.0 // indirect
|
||||
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.37.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||
modernc.org/libc v1.18.0 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.3.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
||||
|
||||
140
i18n/deduplicate_test.go
Normal file
140
i18n/deduplicate_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// DuplicateInfo represents information about a duplicate key
|
||||
type DuplicateInfo struct {
|
||||
Key string
|
||||
OldPrefix string
|
||||
NewPrefix string
|
||||
OldPrefixKey string // e.g., "general:Submitter"
|
||||
NewPrefixKey string // e.g., "permission:Submitter"
|
||||
}
|
||||
|
||||
// findDuplicateKeysInJSON finds duplicate keys across the entire JSON file
|
||||
// Returns a list of duplicate information showing old and new prefix:key pairs
|
||||
// The order is determined by the order keys appear in the JSON file (git history)
|
||||
func findDuplicateKeysInJSON(filePath string) ([]DuplicateInfo, error) {
|
||||
// Read the JSON file
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// Track the first occurrence of each key (prefix where it was first seen)
|
||||
keyFirstPrefix := make(map[string]string)
|
||||
var duplicates []DuplicateInfo
|
||||
|
||||
// To preserve order, we need to parse the JSON with order preservation
|
||||
// We'll use a decoder to read through the top-level object
|
||||
decoder := json.NewDecoder(bytes.NewReader(fileContent))
|
||||
|
||||
// Read the opening brace of the top-level object
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read token: %w", err)
|
||||
}
|
||||
if delim, ok := token.(json.Delim); !ok || delim != '{' {
|
||||
return nil, fmt.Errorf("expected object start, got %v", token)
|
||||
}
|
||||
|
||||
// Read all namespaces in order
|
||||
for decoder.More() {
|
||||
// Read the namespace (prefix) name
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read namespace: %w", err)
|
||||
}
|
||||
|
||||
prefix, ok := token.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected string namespace, got %v", token)
|
||||
}
|
||||
|
||||
// Read the namespace object as raw message
|
||||
var namespaceData map[string]string
|
||||
if err := decoder.Decode(&namespaceData); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode namespace %s: %w", prefix, err)
|
||||
}
|
||||
|
||||
// Now check each key in this namespace
|
||||
for key := range namespaceData {
|
||||
// Check if this key was already seen in a different prefix
|
||||
if firstPrefix, exists := keyFirstPrefix[key]; exists {
|
||||
// This is a duplicate - the key exists in another prefix
|
||||
duplicates = append(duplicates, DuplicateInfo{
|
||||
Key: key,
|
||||
OldPrefix: firstPrefix,
|
||||
NewPrefix: prefix,
|
||||
OldPrefixKey: fmt.Sprintf("%s:%s", firstPrefix, key),
|
||||
NewPrefixKey: fmt.Sprintf("%s:%s", prefix, key),
|
||||
})
|
||||
} else {
|
||||
// First time seeing this key, record the prefix
|
||||
keyFirstPrefix[key] = prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates, nil
|
||||
}
|
||||
|
||||
// TestDeduplicateFrontendI18n checks for duplicate i18n keys in the frontend en.json file
|
||||
func TestDeduplicateFrontendI18n(t *testing.T) {
|
||||
filePath := "../web/src/locales/en/data.json"
|
||||
|
||||
// Find duplicate keys
|
||||
duplicates, err := findDuplicateKeysInJSON(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check for duplicates in frontend i18n file: %v", err)
|
||||
}
|
||||
|
||||
// Print all duplicates and fail the test if any are found
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("Found duplicate i18n keys in frontend file (%s):", filePath)
|
||||
for _, dup := range duplicates {
|
||||
t.Errorf(" i18next.t(\"%s\") duplicates with i18next.t(\"%s\")", dup.NewPrefixKey, dup.OldPrefixKey)
|
||||
}
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeduplicateBackendI18n checks for duplicate i18n keys in the backend en.json file
|
||||
func TestDeduplicateBackendI18n(t *testing.T) {
|
||||
filePath := "../i18n/locales/en/data.json"
|
||||
|
||||
// Find duplicate keys
|
||||
duplicates, err := findDuplicateKeysInJSON(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check for duplicates in backend i18n file: %v", err)
|
||||
}
|
||||
|
||||
// Print all duplicates and fail the test if any are found
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("Found duplicate i18n keys in backend file (%s):", filePath)
|
||||
for _, dup := range duplicates {
|
||||
t.Errorf(" i18n.Translate(\"%s\") duplicates with i18n.Translate(\"%s\")", dup.NewPrefixKey, dup.OldPrefixKey)
|
||||
}
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user