login.component.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. /*
  2. * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit, OnDestroy, AfterViewInit, Optional } from '@angular/core';
  17. import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
  18. import { Router, ActivatedRoute } from '@angular/router';
  19. import { throwIfAlreadyLoaded } from '@core';
  20. import { ReuseTabService } from '@delon/abc/reuse-tab';
  21. import { SettingsService, _HttpClient } from '@delon/theme';
  22. import { environment } from '@env/environment';
  23. import { NzSafeAny } from 'ng-zorro-antd/core/types';
  24. import { NzMessageService } from 'ng-zorro-antd/message';
  25. import { NzTabChangeEvent } from 'ng-zorro-antd/tabs';
  26. import { finalize } from 'rxjs/operators';
  27. import { AuthnService } from '../../../service/authn.service';
  28. import { ImageCaptchaService } from '../../../service/image-captcha.service';
  29. import { SocialsProviderService } from '../../../service/socials-provider.service';
  30. import {QrCodeService} from "../../../service/QrCode.service";
  31. import { CONSTS } from '../../../shared/consts';
  32. import { stringify } from 'querystring';
  33. @Component({
  34. selector: 'passport-login',
  35. templateUrl: './login.component.html',
  36. styleUrls: ['./login.component.less'],
  37. changeDetection: ChangeDetectionStrategy.OnPush
  38. })
  39. export class UserLoginComponent implements OnInit, OnDestroy {
  40. socials: {
  41. providers: NzSafeAny[];
  42. qrScan: string;
  43. } = {
  44. providers: [],
  45. qrScan: ''
  46. };
  47. form: FormGroup;
  48. error = '';
  49. switchTab = true;
  50. loginType = 'normal';
  51. loading = false;
  52. passwordVisible = false;
  53. qrexpire = false;
  54. imageCaptcha = '';
  55. captchaType = '';
  56. state = '';
  57. count = 0;
  58. interval$: any;
  59. //二维码内容
  60. ticket = '';
  61. constructor(
  62. fb: FormBuilder,
  63. private router: Router,
  64. private settingsService: SettingsService,
  65. private authnService: AuthnService,
  66. private socialsProviderService: SocialsProviderService,
  67. private imageCaptchaService: ImageCaptchaService,
  68. private qrCodeService: QrCodeService,
  69. @Optional()
  70. @Inject(ReuseTabService)
  71. private reuseTabService: ReuseTabService,
  72. private route: ActivatedRoute,
  73. private msg: NzMessageService,
  74. private cdr: ChangeDetectorRef
  75. ) {
  76. this.form = fb.group({
  77. userName: [null, [Validators.required]],
  78. password: [null, [Validators.required]],
  79. captcha: [null, [Validators.required]],
  80. mobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]],
  81. otpCaptcha: [null, [Validators.required]],
  82. remember: [false]
  83. });
  84. }
  85. ngOnInit(): void {
  86. //set redirect_uri , is BASE64URL
  87. if (this.route.snapshot.queryParams[CONSTS.REDIRECT_URI]) {
  88. this.authnService.setRedirectUri(this.route.snapshot.queryParams[CONSTS.REDIRECT_URI]);
  89. }
  90. //congress login
  91. if (this.route.snapshot.queryParams[CONSTS.CONGRESS]) {
  92. this.congressLogin(this.route.snapshot.queryParams[CONSTS.CONGRESS]);
  93. }
  94. //init socials,state
  95. this.authnService.clear();
  96. this.authnService
  97. .get({ remember_me: localStorage.getItem(CONSTS.REMEMBER) })
  98. .pipe(
  99. finalize(() => {
  100. this.loading = false;
  101. this.cdr.detectChanges();
  102. })
  103. )
  104. .subscribe(res => {
  105. this.loading = true;
  106. if (res.code !== 0) {
  107. this.error = res.msg;
  108. } else {
  109. // 清空路由复用信息
  110. //console.log(res.data);
  111. //REMEMBER ME
  112. if (res.data.token) {
  113. // 清空路由复用信息
  114. this.reuseTabService.clear();
  115. // 设置用户Token信息
  116. this.authnService.auth(res.data);
  117. this.authnService.navigate({});
  118. } else {
  119. this.socials = res.data.socials;
  120. this.state = res.data.state;
  121. this.captchaType = res.data.captcha;
  122. if (this.captchaType === 'NONE') {
  123. //清除校验规则
  124. this.form.get('captcha')?.clearValidators();
  125. } else {
  126. //init image captcha
  127. this.imageCaptchaService.captcha({ state: this.state, captcha: this.captchaType }).subscribe(res => {
  128. this.imageCaptcha = res.data.image;
  129. this.cdr.detectChanges();
  130. });
  131. }
  132. }
  133. }
  134. });
  135. this.cdr.detectChanges();
  136. }
  137. congressLogin(congress: string) {
  138. this.authnService
  139. .congress({
  140. congress: congress
  141. })
  142. .pipe(
  143. finalize(() => {
  144. this.loading = false;
  145. this.cdr.detectChanges();
  146. })
  147. )
  148. .subscribe(res => {
  149. this.loading = true;
  150. if (res.code !== 0) {
  151. this.error = res.msg;
  152. } else {
  153. // 清空路由复用信息
  154. this.reuseTabService.clear();
  155. // 设置用户Token信息
  156. this.authnService.auth(res.data);
  157. this.authnService.navigate({});
  158. }
  159. });
  160. }
  161. // #region fields
  162. get userName(): AbstractControl {
  163. return this.form.get('userName')!;
  164. }
  165. get password(): AbstractControl {
  166. return this.form.get('password')!;
  167. }
  168. get mobile(): AbstractControl {
  169. return this.form.get('mobile')!;
  170. }
  171. get captcha(): AbstractControl {
  172. return this.form.get('captcha')!;
  173. }
  174. get otpCaptcha(): AbstractControl {
  175. return this.form.get('otpCaptcha')!;
  176. }
  177. get remember(): AbstractControl {
  178. return this.form.get('remember')!;
  179. }
  180. // #endregion
  181. // #region get captcha
  182. getImageCaptcha(): void {
  183. this.imageCaptchaService.captcha({ state: this.state, captcha: this.captchaType }).subscribe(res => {
  184. this.imageCaptcha = res.data.image;
  185. this.cdr.detectChanges();
  186. });
  187. }
  188. //send sms
  189. sendOtpCode(): void {
  190. if (this.mobile.invalid) {
  191. this.mobile.markAsDirty({ onlySelf: true });
  192. this.mobile.updateValueAndValidity({ onlySelf: true });
  193. return;
  194. }
  195. this.authnService.produceOtp({ mobile: this.mobile.value }).subscribe(res => {
  196. if (res.code !== 0) {
  197. this.msg.success(`发送失败`);
  198. }
  199. });
  200. this.count = 59;
  201. this.interval$ = setInterval(() => {
  202. this.count -= 1;
  203. if (this.count <= 0) {
  204. clearInterval(this.interval$);
  205. }
  206. this.cdr.detectChanges();
  207. }, 1000);
  208. }
  209. // #endregion
  210. submit(): void {
  211. this.error = '';
  212. if (this.loginType === 'normal') {
  213. this.userName.markAsDirty();
  214. this.userName.updateValueAndValidity();
  215. this.password.markAsDirty();
  216. this.password.updateValueAndValidity();
  217. this.captcha.markAsDirty();
  218. this.captcha.updateValueAndValidity();
  219. if (this.userName.invalid || this.password.invalid || this.captcha.invalid) {
  220. return;
  221. }
  222. } else {
  223. this.mobile.markAsDirty();
  224. this.mobile.updateValueAndValidity();
  225. this.otpCaptcha.markAsDirty();
  226. this.otpCaptcha.updateValueAndValidity();
  227. if (this.mobile.invalid || this.otpCaptcha.invalid) {
  228. return;
  229. }
  230. }
  231. localStorage.setItem(CONSTS.REMEMBER, this.form.get(CONSTS.REMEMBER)?.value);
  232. this.loading = true;
  233. this.cdr.detectChanges();
  234. this.authnService
  235. .login({
  236. authType: this.loginType,
  237. state: this.state,
  238. username: this.userName.value,
  239. password: this.password.value,
  240. captcha: this.captcha.value,
  241. mobile: this.mobile.value,
  242. otpCaptcha: this.otpCaptcha.value,
  243. remeberMe: this.remember.value
  244. })
  245. .pipe(
  246. finalize(() => {
  247. this.loading = false;
  248. this.cdr.detectChanges();
  249. })
  250. )
  251. .subscribe(res => {
  252. this.loading = true;
  253. if (res.code !== 0) {
  254. this.error = res.message;
  255. //this.msg.error(this.error);
  256. if (this.loginType === 'normal') {
  257. this.getImageCaptcha();
  258. }
  259. } else {
  260. // 清空路由复用信息
  261. this.reuseTabService.clear();
  262. // 设置用户Token信息
  263. this.authnService.auth(res.data);
  264. this.authnService.navigate({});
  265. }
  266. this.cdr.detectChanges();
  267. });
  268. }
  269. // #region social
  270. socialauth(provider: string): void {
  271. this.authnService.clearUser();
  272. this.socialsProviderService.authorize(provider).subscribe(res => {
  273. //console.log(res.data);
  274. window.location.href = res.data;
  275. });
  276. }
  277. /**
  278. * 获取二维码
  279. */
  280. getLoginQrCode() {
  281. this.qrexpire = false;
  282. this.qrCodeService.getLoginQrCode().subscribe(res => {
  283. if (res.code === 0 && res.data.rqCode) { // 使用返回的 rqCode
  284. const qrImageElement = document.getElementById('div_qrcodelogin');
  285. this.ticket = res.data.ticket;
  286. if (qrImageElement) {
  287. qrImageElement.innerHTML = `<img src="${res.data.rqCode}" alt="QR Code" style="width: 200px; height: 200px;">`;
  288. }
  289. /* // 设置5分钟后 qrexpire 为 false
  290. setTimeout(() => {
  291. this.qrexpire = true;
  292. this.cdr.detectChanges(); // 更新视图
  293. }, 5 * 60 * 1000); // 5 分钟*/
  294. this.loginByQrCode();
  295. }
  296. });
  297. }
  298. /**
  299. * 二维码轮询登录
  300. */
  301. loginByQrCode() {
  302. const interval = setInterval(() => {
  303. this.qrCodeService.loginByQrCode({
  304. authType: 'scancode',
  305. code: this.ticket,
  306. }).subscribe(res => {
  307. if (res.code === 0) {
  308. this.qrexpire = true;
  309. // 清空路由复用信息
  310. this.reuseTabService.clear();
  311. // 设置用户Token信息
  312. this.authnService.auth(res.data);
  313. this.authnService.navigate({});
  314. } else if (res.code === 20004) {
  315. this.qrexpire = true;
  316. }
  317. // Handle response here
  318. // If you need to stop the interval after a certain condition is met,
  319. // you can clear the interval like this:
  320. if (this.qrexpire) {
  321. clearInterval(interval);
  322. }
  323. this.cdr.detectChanges(); // 更新视图
  324. });
  325. }, 5 * 1000); // 5 seconds
  326. }
  327. getQrCode(): void {
  328. this.qrexpire = false;
  329. if (this.interval$) {
  330. clearInterval(this.interval$);
  331. }
  332. this.authnService.clearUser();
  333. this.socialsProviderService.scanqrcode(this.socials.qrScan).subscribe(res => {
  334. if (res.code === 0) {
  335. if (this.socials.qrScan === 'workweixin') {
  336. this.qrScanWorkweixin(res.data);
  337. } else if (this.socials.qrScan === 'dingtalk') {
  338. this.qrScanDingtalk(res.data);
  339. } else if (this.socials.qrScan === 'maxkey') {
  340. this.qrScanMaxkey(res.data);
  341. } else if (this.socials.qrScan === 'feishu') {
  342. this.qrScanFeishu(res.data);
  343. }
  344. }
  345. });
  346. }
  347. // #endregion
  348. ngOnDestroy(): void {
  349. if (this.interval$) {
  350. clearInterval(this.interval$);
  351. }
  352. }
  353. // #region QR Scan for workweixin, dingtalk ,feishu
  354. qrScanWorkweixin(data: any) {
  355. //see doc https://developer.work.weixin.qq.com/document/path/91025
  356. // @ts-ignore
  357. let wwLogin = new WwLogin({
  358. id: 'div_qrcodelogin',
  359. appid: data.clientId,
  360. agentid: data.agentId,
  361. redirect_uri: encodeURIComponent(data.redirectUri),
  362. state: data.state,
  363. href: 'data:text/css;base64,LmltcG93ZXJCb3ggLnFyY29kZSB7d2lkdGg6IDI1MHB4O30NCi5pbXBvd2VyQm94IC50aXRsZSB7ZGlzcGxheTogbm9uZTt9DQouaW1wb3dlckJveCAuaW5mbyB7d2lkdGg6IDI1MHB4O30NCi5zdGF0dXNfaWNvbiB7ZGlzcGxheTpub25lfQ0KLmltcG93ZXJCb3ggLnN0YXR1cyB7dGV4dC1hbGlnbjogY2VudGVyO30='
  364. });
  365. }
  366. qrScanFeishu(data: any) {
  367. //see doc https://open.feishu.cn/document/common-capabilities/sso/web-application-sso/qr-sdk-documentation
  368. //remove old div
  369. var qrcodeDiv = document.querySelector('#div_qrcodelogin');
  370. qrcodeDiv?.childNodes.forEach(function (value, index, array) {
  371. qrcodeDiv?.removeChild(value);
  372. });
  373. // @ts-ignore
  374. fsredirectUri = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${data.clientId}&redirect_uri=${encodeURIComponent(
  375. data.redirectUri
  376. )}&response_type=code&state=${data.state}`;
  377. // @ts-ignore
  378. var redirectUri = fsredirectUri;
  379. // @ts-ignore
  380. QRLoginObj = QRLogin({
  381. id: 'div_qrcodelogin',
  382. goto: redirectUri,
  383. width: '300',
  384. height: '300',
  385. style: 'border: 0;'
  386. });
  387. }
  388. qrScanDingtalk(data: any) {
  389. //see doc https://open.dingtalk.com/document/isvapp-server/scan-qr-code-to-log-on-to-third-party-websites
  390. var url = encodeURIComponent(data.redirectUri);
  391. var gotodingtalk = encodeURIComponent(
  392. `https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=${data.clientId}&response_type=code&scope=snsapi_login&state=${data.state}&redirect_uri=${url}`
  393. );
  394. // @ts-ignore
  395. ddredirect_uri = `https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=${data.clientId}&response_type=code&scope=snsapi_login&state=${data.state}&redirect_uri=${data.redirectUri}`;
  396. // @ts-ignore
  397. var obj = DDLogin({
  398. id: 'div_qrcodelogin', //这里需要你在自己的页面定义一个HTML标签并设置id,例如<div id="login_container"></div>或<span id="login_container"></span>
  399. goto: gotodingtalk, //请参考注释里的方式
  400. style: 'border:none;background-color:#FFFFFF;',
  401. width: '360',
  402. height: '400'
  403. });
  404. }
  405. // #region QR Scan end
  406. qrScanMaxkey(data: any) {
  407. // @ts-ignore
  408. document.getElementById('div_qrcodelogin').innerHTML = '';
  409. // @ts-ignore
  410. var qrcode = new QRCode('div_qrcodelogin', {
  411. width: 200,
  412. height: 200,
  413. colorDark: '#000000',
  414. colorLight: '#ffffff'
  415. }).makeCode(data.state);
  416. //3分钟监听二维码
  417. this.count = 90;
  418. this.interval$ = setInterval(() => {
  419. this.count -= 1;
  420. if (this.loginType != 'qrscan') {
  421. clearInterval(this.interval$);
  422. }
  423. if (this.count <= 0) {
  424. clearInterval(this.interval$);
  425. }
  426. //轮询发送监听请求
  427. this.socialsProviderService.qrcallback(this.socials.qrScan, data.state).subscribe(res => {
  428. if (res.code === 0) {
  429. // 清空路由复用信息
  430. this.reuseTabService.clear();
  431. // 设置用户Token信息
  432. this.authnService.auth(res.data);
  433. this.authnService.navigate({});
  434. clearInterval(this.interval$);
  435. } else if (res.code === 102) {
  436. // 二维码过期
  437. clearInterval(this.interval$);
  438. this.qrexpire = true;
  439. this.cdr.detectChanges();
  440. } else if (res.code === 103) {
  441. // 暂无用户扫码
  442. }
  443. });
  444. this.cdr.detectChanges();
  445. }, 2000);
  446. }
  447. }