@kengcom202/kcore-ui 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { FormBuilder, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, Valid
5
5
  import { HttpClient, HttpErrorResponse } from "@angular/common/http";
6
6
  import { ActivatedRoute, Router, RouterLink } from "@angular/router";
7
7
  import { catchError, throwError } from "rxjs";
8
+ import { toSignal } from "@angular/core/rxjs-interop";
8
9
  var AnchoredPopup = class {
9
10
  anchor;
10
11
  component;
@@ -1395,7 +1396,8 @@ const KC_AUTH_CONFIG = new InjectionToken("KC_AUTH_CONFIG");
1395
1396
  const KC_AUTH_PATHS = {
1396
1397
  login: "/login",
1397
1398
  register: "/register",
1398
- ssoCallback: "/sso-callback"
1399
+ ssoCallback: "/sso-callback",
1400
+ thaidCallback: "/thaid-callback"
1399
1401
  };
1400
1402
  function resolveKcAuthConfig(o) {
1401
1403
  return {
@@ -1404,8 +1406,10 @@ function resolveKcAuthConfig(o) {
1404
1406
  apiBase: (o.apiBase ?? "/api").replace(/\/+$/, ""),
1405
1407
  homeUrl: o.homeUrl || "/",
1406
1408
  sso: o.sso ? { label: o.sso.label || "SSO" } : null,
1407
- passwordLogin: o.passwordLogin !== false || !o.sso,
1409
+ thaid: o.thaid ? { label: o.thaid.label || "ThaiD" } : null,
1410
+ passwordLogin: o.passwordLogin !== false || !o.sso && !o.thaid,
1408
1411
  register: o.register ?? "none",
1412
+ profileOrgChange: o.profileOrgChange === true,
1409
1413
  orgLabel: o.orgLabel || "หน่วยงาน",
1410
1414
  adminRoles: o.adminRoles ?? ["admin"],
1411
1415
  passwordMinLength: o.passwordMinLength ?? 4,
@@ -1418,6 +1422,12 @@ function provideKcAuth(options) {
1418
1422
  useValue: resolveKcAuthConfig(options)
1419
1423
  }]);
1420
1424
  }
1425
+ const KC_LOGIN_LOCK_SECONDS = 300;
1426
+ function kcLoginRetryAfter(err) {
1427
+ if (!(err instanceof HttpErrorResponse) || err.status !== 429) return 0;
1428
+ const s = Number(err.error?.retryAfter);
1429
+ return Number.isFinite(s) && s > 0 ? Math.ceil(s) : 300;
1430
+ }
1421
1431
  function kcErrorMessage(err, fallback) {
1422
1432
  if (err instanceof HttpErrorResponse) {
1423
1433
  const m = err.error?.message;
@@ -1429,6 +1439,7 @@ function kcErrorMessage(err, fallback) {
1429
1439
  const SESSION_KEY = "kc_session";
1430
1440
  const KC_SSO_STATE_KEY = "kc_sso_state";
1431
1441
  const KC_SSO_TICKET_KEY = "kc_sso_ticket";
1442
+ const KC_THAID_STATE_KEY = "kc_thaid_state";
1432
1443
  const KC_RETURN_URL_KEY = "kc_return_url";
1433
1444
  function kcTokenExpired(token, now = Date.now()) {
1434
1445
  const part = token.split(".")[1];
@@ -1492,6 +1503,7 @@ var KcAuthService = class KcAuthService {
1492
1503
  for (const k of [
1493
1504
  KC_SSO_STATE_KEY,
1494
1505
  KC_SSO_TICKET_KEY,
1506
+ KC_THAID_STATE_KEY,
1495
1507
  KC_RETURN_URL_KEY
1496
1508
  ]) this.storage("sessionStorage")?.removeItem(k);
1497
1509
  }
@@ -1719,6 +1731,9 @@ const AUTH_CSS = `
1719
1731
  .kc-auth-field-error { color: var(--kc-auth-danger, #c62828); font-size: 0.86em; }
1720
1732
  .kc-auth-hint { margin: 2px 0 0; color: var(--kc-auth-muted, #806f80); font-size: 0.86em; }
1721
1733
  .kc-auth-check { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
1734
+ .kc-auth-check:has(input:disabled) { cursor: not-allowed; opacity: 0.7; }
1735
+ .kc-auth-methods { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 22px; }
1736
+ .kc-auth-methods > .kc-auth-btn { margin-left: auto; min-width: 110px; }
1722
1737
 
1723
1738
  /* ---- ปุ่ม ---- */
1724
1739
  .kc-auth-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 6px; }
@@ -1760,7 +1775,7 @@ const AUTH_CSS = `
1760
1775
  .kc-auth-btn-row { display: flex; gap: 10px; margin-top: 6px; }
1761
1776
  .kc-auth-btn-row > .kc-auth-btn { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
1762
1777
  /* ปิดรหัสผ่าน: ปุ่ม SSO ปุ่มเดียวตรงกลาง */
1763
- .kc-auth-sso-only { display: flex; justify-content: center; padding: 8px 0 4px; }
1778
+ .kc-auth-sso-only { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 8px 0 4px; }
1764
1779
  .kc-auth-sso-only > .kc-auth-btn { min-width: 75%; height: 46px; padding: 0 24px; font-size: 1.05em; }
1765
1780
  .kc-auth-btn.block { display: flex; width: 100%; }
1766
1781
  .kc-auth-btn.sm { height: 30px; padding: 0 10px; font-size: 0.93em; }
@@ -1981,6 +1996,23 @@ i0.ɵɵngDeclareClassMetadata({
1981
1996
  }],
1982
1997
  ctorParameters: () => []
1983
1998
  });
1999
+ const KC_LOGIN_LOCK_KEY = "kc_login_lock";
2000
+ const lockKey = (username) => username.trim().toLowerCase();
2001
+ function readLocks(storage) {
2002
+ try {
2003
+ const raw = JSON.parse(storage?.getItem("kc_login_lock") ?? "{}");
2004
+ const now = Date.now();
2005
+ return Object.fromEntries(Object.entries(raw && typeof raw === "object" ? raw : {}).filter((e) => typeof e[1] === "number" && e[1] > now));
2006
+ } catch {
2007
+ return {};
2008
+ }
2009
+ }
2010
+ function writeLocks(storage, locks) {
2011
+ try {
2012
+ if (Object.keys(locks).length) storage?.setItem(KC_LOGIN_LOCK_KEY, JSON.stringify(locks));
2013
+ else storage?.removeItem(KC_LOGIN_LOCK_KEY);
2014
+ } catch {}
2015
+ }
1984
2016
  var KcLoginComponent = class KcLoginComponent {
1985
2017
  config = inject(KC_AUTH_CONFIG);
1986
2018
  paths = KC_AUTH_PATHS;
@@ -1988,19 +2020,57 @@ var KcLoginComponent = class KcLoginComponent {
1988
2020
  http = inject(HttpClient);
1989
2021
  router = inject(Router);
1990
2022
  route = inject(ActivatedRoute);
2023
+ doc = inject(DOCUMENT);
1991
2024
  busy = signal("", ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
2025
+ methods = signal({
2026
+ password: true,
2027
+ sso: true,
2028
+ thaid: true
2029
+ }, ...ngDevMode ? [{ debugName: "methods" }] : /* istanbul ignore next */ []);
2030
+ external = computed(() => [...this.config.sso && this.methods().sso ? [{
2031
+ key: "sso",
2032
+ label: this.config.sso.label,
2033
+ stateKey: KC_SSO_STATE_KEY
2034
+ }] : [], ...this.config.thaid && this.methods().thaid ? [{
2035
+ key: "thaid",
2036
+ label: this.config.thaid.label,
2037
+ stateKey: KC_THAID_STATE_KEY
2038
+ }] : []], ...ngDevMode ? [{ debugName: "external" }] : /* istanbul ignore next */ []);
2039
+ showPassword = computed(() => this.config.passwordLogin && this.methods().password || !this.external().length, ...ngDevMode ? [{ debugName: "showPassword" }] : /* istanbul ignore next */ []);
1992
2040
  message = signal(null, ...ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []);
1993
2041
  form = inject(FormBuilder).nonNullable.group({
1994
2042
  username: ["", Validators.required],
1995
2043
  password: ["", Validators.required]
1996
2044
  });
2045
+ locks = signal(readLocks(this.storage()), ...ngDevMode ? [{ debugName: "locks" }] : /* istanbul ignore next */ []);
2046
+ now = signal(Date.now(), ...ngDevMode ? [{ debugName: "now" }] : /* istanbul ignore next */ []);
2047
+ username = toSignal(this.form.controls.username.valueChanges, { initialValue: "" });
2048
+ lockLeft = computed(() => {
2049
+ const until = this.locks()[lockKey(this.username())] ?? 0;
2050
+ return Math.max(0, Math.ceil((until - this.now()) / 1e3));
2051
+ }, ...ngDevMode ? [{ debugName: "lockLeft" }] : /* istanbul ignore next */ []);
2052
+ lockText = computed(() => {
2053
+ const s = this.lockLeft();
2054
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
2055
+ }, ...ngDevMode ? [{ debugName: "lockText" }] : /* istanbul ignore next */ []);
2056
+ timer;
1997
2057
  constructor() {
1998
2058
  ensureAuthStyles();
2059
+ inject(DestroyRef).onDestroy(() => this.stopTimer());
2060
+ if (Object.keys(this.locks()).length) this.startTimer();
1999
2061
  }
2000
2062
  get returnUrl() {
2001
2063
  return this.auth.safeReturnUrl(this.route.snapshot.queryParamMap.get("returnUrl"));
2002
2064
  }
2003
2065
  ngOnInit() {
2066
+ this.http.get(`${this.config.apiBase}/user/login-methods`).subscribe({
2067
+ next: (m) => this.methods.set({
2068
+ password: m?.password !== false,
2069
+ sso: m?.sso !== false,
2070
+ thaid: m?.thaid !== false
2071
+ }),
2072
+ error: () => {}
2073
+ });
2004
2074
  if (this.route.snapshot.queryParamMap.get("expired") === "1") this.message.set({
2005
2075
  text: "หมดเวลาการใช้งาน กรุณาเข้าสู่ระบบใหม่",
2006
2076
  error: false
@@ -2008,7 +2078,7 @@ var KcLoginComponent = class KcLoginComponent {
2008
2078
  else if (this.auth.isLoggedIn()) this.router.navigateByUrl(this.returnUrl);
2009
2079
  }
2010
2080
  submit() {
2011
- if (this.busy()) return;
2081
+ if (this.busy() || this.lockLeft()) return;
2012
2082
  const v = this.form.getRawValue();
2013
2083
  if (!v.username.trim() || !v.password) {
2014
2084
  this.message.set({
@@ -2024,33 +2094,66 @@ var KcLoginComponent = class KcLoginComponent {
2024
2094
  password: v.password
2025
2095
  }).subscribe({
2026
2096
  next: (res) => {
2097
+ this.setLock(v.username, 0);
2027
2098
  this.auth.start(res);
2028
2099
  this.router.navigateByUrl(this.returnUrl);
2029
2100
  },
2030
2101
  error: (err) => {
2031
2102
  this.busy.set("");
2032
- this.message.set({
2103
+ const wait = kcLoginRetryAfter(err);
2104
+ if (wait) this.setLock(v.username, Date.now() + wait * 1e3);
2105
+ else this.message.set({
2033
2106
  text: kcErrorMessage(err, "เข้าสู่ระบบไม่สำเร็จ"),
2034
2107
  error: true
2035
2108
  });
2036
2109
  }
2037
2110
  });
2038
2111
  }
2039
- startSso() {
2040
- const sso = this.config.sso;
2041
- if (!sso || this.busy()) return;
2042
- this.busy.set("sso");
2112
+ setLock(username, until) {
2113
+ const next = { ...this.locks() };
2114
+ if (until) next[lockKey(username)] = until;
2115
+ else delete next[lockKey(username)];
2116
+ this.locks.set(next);
2117
+ this.now.set(Date.now());
2118
+ writeLocks(this.storage(), next);
2119
+ if (until) this.startTimer();
2120
+ }
2121
+ startTimer() {
2122
+ this.timer ??= setInterval(() => {
2123
+ this.now.set(Date.now());
2124
+ const live = Object.fromEntries(Object.entries(this.locks()).filter(([, until]) => until > this.now()));
2125
+ if (Object.keys(live).length === Object.keys(this.locks()).length) return;
2126
+ this.locks.set(live);
2127
+ writeLocks(this.storage(), live);
2128
+ if (!Object.keys(live).length) this.stopTimer();
2129
+ }, 1e3);
2130
+ }
2131
+ stopTimer() {
2132
+ clearInterval(this.timer);
2133
+ this.timer = void 0;
2134
+ }
2135
+ storage() {
2136
+ try {
2137
+ return this.doc.defaultView?.localStorage ?? null;
2138
+ } catch {
2139
+ return null;
2140
+ }
2141
+ }
2142
+ startExternal(key) {
2143
+ const p = this.external().find((x) => x.key === key);
2144
+ if (!p || this.busy()) return;
2145
+ this.busy.set(key);
2043
2146
  this.message.set(null);
2044
- this.http.get(`${this.config.apiBase}/user/sso/authorize-url`).subscribe({
2147
+ this.http.get(`${this.config.apiBase}/user/${key}/authorize-url`).subscribe({
2045
2148
  next: ({ url, state }) => {
2046
- this.auth.setTemp(KC_SSO_STATE_KEY, state);
2149
+ this.auth.setTemp(p.stateKey, state);
2047
2150
  this.auth.setTemp(KC_RETURN_URL_KEY, this.returnUrl);
2048
2151
  this.auth.redirect(url);
2049
2152
  },
2050
2153
  error: (err) => {
2051
2154
  this.busy.set("");
2052
2155
  this.message.set({
2053
- text: err.status === 404 ? `ระบบยังไม่ได้ตั้งค่าการเข้าสู่ระบบด้วย ${sso.label}` : kcErrorMessage(err, `ไม่สามารถเชื่อมต่อ ${sso.label} ได้ กรุณาลองใหม่อีกครั้ง`),
2156
+ text: err.status === 404 ? `ระบบยังไม่ได้ตั้งค่าการเข้าสู่ระบบด้วย ${p.label}` : kcErrorMessage(err, `ไม่สามารถเชื่อมต่อ ${p.label} ได้ กรุณาลองใหม่อีกครั้ง`),
2054
2157
  error: true
2055
2158
  });
2056
2159
  }
@@ -2080,11 +2183,15 @@ var KcLoginComponent = class KcLoginComponent {
2080
2183
  <h1 class="kc-auth-title">{{ config.appName }}</h1>
2081
2184
  <p class="kc-auth-subtitle">ลงชื่อเข้าสู่ระบบ</p>
2082
2185
 
2083
- @if (message(); as m) {
2186
+ @if (lockLeft()) {
2187
+ <div class="kc-auth-alert error" role="alert">
2188
+ เข้าสู่ระบบไม่สำเร็จเกินจำนวนครั้งที่กำหนด กรุณารอ {{ lockText() }} นาที แล้วลองใหม่อีกครั้ง
2189
+ </div>
2190
+ } @else if (message(); as m) {
2084
2191
  <div class="kc-auth-alert" [class.error]="m.error" [class.info]="!m.error" role="alert">{{ m.text }}</div>
2085
2192
  }
2086
2193
 
2087
- @if (config.passwordLogin) {
2194
+ @if (showPassword()) {
2088
2195
  <form [formGroup]="form" (ngSubmit)="submit()" novalidate>
2089
2196
  <label class="kc-auth-field">
2090
2197
  <span>ชื่อผู้ใช้</span>
@@ -2094,32 +2201,37 @@ var KcLoginComponent = class KcLoginComponent {
2094
2201
  <span>รหัสผ่าน</span>
2095
2202
  <kc-password><input class="kc-auth-input" type="password" formControlName="password" autocomplete="current-password" /></kc-password>
2096
2203
  </label>
2097
- <!-- ปุ่มเข้าสู่ระบบกับ SSO อยู่บรรทัดเดียวกัน แบ่งครึ่ง — ปุ่ม SSO ใช้แค่ชื่อเพื่อให้พอดีครึ่งการ์ด -->
2204
+ <!-- ปุ่มเข้าสู่ระบบกับ SSO / ThaiD อยู่บรรทัดเดียวกัน แบ่งเท่ากัน — ปุ่มภายนอกใช้แค่ชื่อเพื่อให้พอดี -->
2098
2205
  <div class="kc-auth-btn-row">
2099
- <button type="submit" class="kc-auth-btn primary" [disabled]="!!busy()">
2100
- {{ busy() === 'password' ? 'กำลังเข้าสู่ระบบ...' : 'เข้าสู่ระบบ' }}
2206
+ <button type="submit" class="kc-auth-btn primary" [disabled]="!!busy() || !!lockLeft()">
2207
+ {{ busy() === 'password' ? 'กำลังเข้าสู่ระบบ...' : lockLeft() ? 'รอ ' + lockText() : 'เข้าสู่ระบบ' }}
2101
2208
  </button>
2102
- @if (config.sso; as sso) {
2103
- <button type="button" class="kc-auth-btn outline" [disabled]="!!busy()" (click)="startSso()"
2104
- [attr.title]="'เข้าสู่ระบบด้วย ' + sso.label">
2105
- {{ busy() === 'sso' ? 'กำลังเชื่อมต่อ...' : sso.label }}
2209
+ @for (p of external(); track p.key) {
2210
+ <button type="button" class="kc-auth-btn outline" [disabled]="!!busy()" (click)="startExternal(p.key)"
2211
+ [attr.title]="'เข้าสู่ระบบด้วย ' + p.label">
2212
+ {{ busy() === p.key ? 'กำลังเชื่อมต่อ...' : p.label }}
2106
2213
  </button>
2107
2214
  }
2108
2215
  </div>
2109
2216
  </form>
2110
- } @else if (config.sso; as sso) {
2111
- <!-- ปิดรหัสผ่าน: มีแต่ปุ่ม SSO ตรงกลาง -->
2217
+ } @else {
2218
+ <!-- ปิดรหัสผ่าน: มีแต่ปุ่ม SSO / ThaiD ตรงกลาง -->
2112
2219
  <div class="kc-auth-sso-only">
2113
- <button type="button" class="kc-auth-btn primary" [disabled]="!!busy()" (click)="startSso()">
2114
- {{ busy() === 'sso' ? 'กำลังเชื่อมต่อ...' : 'เข้าสู่ระบบด้วย ' + sso.label }}
2220
+ @for (p of external(); track p.key) {
2221
+ <button type="button" class="kc-auth-btn" [class.primary]="$first" [class.outline]="!$first" [disabled]="!!busy()"
2222
+ (click)="startExternal(p.key)">
2223
+ {{ busy() === p.key ? 'กำลังเชื่อมต่อ...' : 'เข้าสู่ระบบด้วย ' + p.label }}
2115
2224
  </button>
2225
+ }
2116
2226
  </div>
2117
2227
  }
2118
2228
 
2119
2229
  @if (config.register === 'form') {
2120
2230
  <p class="kc-auth-footer">ยังไม่มีบัญชี? <a class="kc-auth-link" [routerLink]="paths.register">ลงทะเบียน</a></p>
2121
- } @else if (config.register === 'sso' && config.sso) {
2231
+ } @else if (config.register === 'sso' && config.sso && methods().sso) {
2122
2232
  <p class="kc-auth-footer">ผู้ใช้ใหม่ กดปุ่ม {{ config.sso.label }} เพื่อลงทะเบียน</p>
2233
+ } @else if (config.register === 'thaid' && config.thaid && methods().thaid) {
2234
+ <p class="kc-auth-footer">ผู้ใช้ใหม่ กดปุ่ม {{ config.thaid.label }} เพื่อลงทะเบียน</p>
2123
2235
  }
2124
2236
  <ng-content />
2125
2237
  </div>
@@ -2221,11 +2333,15 @@ i0.ɵɵngDeclareClassMetadata({
2221
2333
  <h1 class="kc-auth-title">{{ config.appName }}</h1>
2222
2334
  <p class="kc-auth-subtitle">ลงชื่อเข้าสู่ระบบ</p>
2223
2335
 
2224
- @if (message(); as m) {
2336
+ @if (lockLeft()) {
2337
+ <div class="kc-auth-alert error" role="alert">
2338
+ เข้าสู่ระบบไม่สำเร็จเกินจำนวนครั้งที่กำหนด กรุณารอ {{ lockText() }} นาที แล้วลองใหม่อีกครั้ง
2339
+ </div>
2340
+ } @else if (message(); as m) {
2225
2341
  <div class="kc-auth-alert" [class.error]="m.error" [class.info]="!m.error" role="alert">{{ m.text }}</div>
2226
2342
  }
2227
2343
 
2228
- @if (config.passwordLogin) {
2344
+ @if (showPassword()) {
2229
2345
  <form [formGroup]="form" (ngSubmit)="submit()" novalidate>
2230
2346
  <label class="kc-auth-field">
2231
2347
  <span>ชื่อผู้ใช้</span>
@@ -2235,32 +2351,37 @@ i0.ɵɵngDeclareClassMetadata({
2235
2351
  <span>รหัสผ่าน</span>
2236
2352
  <kc-password><input class="kc-auth-input" type="password" formControlName="password" autocomplete="current-password" /></kc-password>
2237
2353
  </label>
2238
- <!-- ปุ่มเข้าสู่ระบบกับ SSO อยู่บรรทัดเดียวกัน แบ่งครึ่ง — ปุ่ม SSO ใช้แค่ชื่อเพื่อให้พอดีครึ่งการ์ด -->
2354
+ <!-- ปุ่มเข้าสู่ระบบกับ SSO / ThaiD อยู่บรรทัดเดียวกัน แบ่งเท่ากัน — ปุ่มภายนอกใช้แค่ชื่อเพื่อให้พอดี -->
2239
2355
  <div class="kc-auth-btn-row">
2240
- <button type="submit" class="kc-auth-btn primary" [disabled]="!!busy()">
2241
- {{ busy() === 'password' ? 'กำลังเข้าสู่ระบบ...' : 'เข้าสู่ระบบ' }}
2356
+ <button type="submit" class="kc-auth-btn primary" [disabled]="!!busy() || !!lockLeft()">
2357
+ {{ busy() === 'password' ? 'กำลังเข้าสู่ระบบ...' : lockLeft() ? 'รอ ' + lockText() : 'เข้าสู่ระบบ' }}
2242
2358
  </button>
2243
- @if (config.sso; as sso) {
2244
- <button type="button" class="kc-auth-btn outline" [disabled]="!!busy()" (click)="startSso()"
2245
- [attr.title]="'เข้าสู่ระบบด้วย ' + sso.label">
2246
- {{ busy() === 'sso' ? 'กำลังเชื่อมต่อ...' : sso.label }}
2359
+ @for (p of external(); track p.key) {
2360
+ <button type="button" class="kc-auth-btn outline" [disabled]="!!busy()" (click)="startExternal(p.key)"
2361
+ [attr.title]="'เข้าสู่ระบบด้วย ' + p.label">
2362
+ {{ busy() === p.key ? 'กำลังเชื่อมต่อ...' : p.label }}
2247
2363
  </button>
2248
2364
  }
2249
2365
  </div>
2250
2366
  </form>
2251
- } @else if (config.sso; as sso) {
2252
- <!-- ปิดรหัสผ่าน: มีแต่ปุ่ม SSO ตรงกลาง -->
2367
+ } @else {
2368
+ <!-- ปิดรหัสผ่าน: มีแต่ปุ่ม SSO / ThaiD ตรงกลาง -->
2253
2369
  <div class="kc-auth-sso-only">
2254
- <button type="button" class="kc-auth-btn primary" [disabled]="!!busy()" (click)="startSso()">
2255
- {{ busy() === 'sso' ? 'กำลังเชื่อมต่อ...' : 'เข้าสู่ระบบด้วย ' + sso.label }}
2370
+ @for (p of external(); track p.key) {
2371
+ <button type="button" class="kc-auth-btn" [class.primary]="$first" [class.outline]="!$first" [disabled]="!!busy()"
2372
+ (click)="startExternal(p.key)">
2373
+ {{ busy() === p.key ? 'กำลังเชื่อมต่อ...' : 'เข้าสู่ระบบด้วย ' + p.label }}
2256
2374
  </button>
2375
+ }
2257
2376
  </div>
2258
2377
  }
2259
2378
 
2260
2379
  @if (config.register === 'form') {
2261
2380
  <p class="kc-auth-footer">ยังไม่มีบัญชี? <a class="kc-auth-link" [routerLink]="paths.register">ลงทะเบียน</a></p>
2262
- } @else if (config.register === 'sso' && config.sso) {
2381
+ } @else if (config.register === 'sso' && config.sso && methods().sso) {
2263
2382
  <p class="kc-auth-footer">ผู้ใช้ใหม่ กดปุ่ม {{ config.sso.label }} เพื่อลงทะเบียน</p>
2383
+ } @else if (config.register === 'thaid' && config.thaid && methods().thaid) {
2384
+ <p class="kc-auth-footer">ผู้ใช้ใหม่ กดปุ่ม {{ config.thaid.label }} เพื่อลงทะเบียน</p>
2264
2385
  }
2265
2386
  <ng-content />
2266
2387
  </div>
@@ -2293,7 +2414,7 @@ function kcContactProblem(email, mobile) {
2293
2414
  var KcRegisterComponent = class KcRegisterComponent {
2294
2415
  config = inject(KC_AUTH_CONFIG);
2295
2416
  paths = KC_AUTH_PATHS;
2296
- ssoLabel = this.config.sso?.label ?? "SSO";
2417
+ ssoLabel = this.config.register === "thaid" ? this.config.thaid?.label ?? "ThaiD" : this.config.sso?.label ?? "SSO";
2297
2418
  auth = inject(KcAuthService);
2298
2419
  http = inject(HttpClient);
2299
2420
  router = inject(Router);
@@ -2326,7 +2447,7 @@ var KcRegisterComponent = class KcRegisterComponent {
2326
2447
  this.error.set("ระบบนี้ไม่เปิดให้ลงทะเบียนเอง กรุณาติดต่อผู้ดูแลระบบ");
2327
2448
  return;
2328
2449
  }
2329
- if (mode === "sso") {
2450
+ if (mode === "sso" || mode === "thaid") {
2330
2451
  this.ticket = this.auth.getTemp(KC_SSO_TICKET_KEY);
2331
2452
  if (!this.ticket) {
2332
2453
  this.error.set(`ยังไม่ได้ยืนยันตัวตน กรุณากลับไปกด "เข้าสู่ระบบด้วย ${this.ssoLabel}" ที่หน้าเข้าสู่ระบบก่อน`);
@@ -2678,22 +2799,35 @@ i0.ɵɵngDeclareClassMetadata({
2678
2799
  }],
2679
2800
  ctorParameters: () => []
2680
2801
  });
2681
- var KcSsoCallbackComponent = class KcSsoCallbackComponent {
2802
+ const CALLBACK_TEMPLATE = `
2803
+ <div class="kc-auth-screen">
2804
+ <div class="kc-auth-card center">
2805
+ <h1 class="kc-auth-title">{{ label }}</h1>
2806
+ @if (failed()) {
2807
+ <div class="kc-auth-alert error" role="alert" style="margin-top: 16px">{{ message() }}</div>
2808
+ <a class="kc-auth-btn" [routerLink]="paths.login">กลับไปหน้าเข้าสู่ระบบ</a>
2809
+ } @else {
2810
+ <p class="kc-auth-subtitle" style="margin-bottom: 0">{{ message() }}</p>
2811
+ }
2812
+ </div>
2813
+ </div>
2814
+ `;
2815
+ var KcExternalCallbackBase = class KcExternalCallbackBase {
2682
2816
  config = inject(KC_AUTH_CONFIG);
2683
2817
  auth = inject(KcAuthService);
2684
2818
  http = inject(HttpClient);
2685
- route = inject(ActivatedRoute);
2686
2819
  router = inject(Router);
2820
+ route = inject(ActivatedRoute);
2687
2821
  paths = KC_AUTH_PATHS;
2688
- label = this.config.sso?.label ?? "SSO";
2689
- message = signal(`กำลังเข้าสู่ระบบผ่าน ${this.label}...`, ...ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []);
2822
+ message = signal("", ...ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []);
2690
2823
  failed = signal(false, ...ngDevMode ? [{ debugName: "failed" }] : /* istanbul ignore next */ []);
2691
2824
  constructor() {
2692
2825
  ensureAuthStyles();
2693
2826
  }
2694
2827
  ngOnInit() {
2828
+ this.message.set(`กำลังเข้าสู่ระบบผ่าน ${this.label}...`);
2695
2829
  const params = this.route.snapshot.queryParamMap;
2696
- const expectedState = this.auth.takeTemp(KC_SSO_STATE_KEY);
2830
+ const expectedState = this.auth.takeTemp(this.stateKey);
2697
2831
  const code = params.get("code");
2698
2832
  if (params.get("error")) {
2699
2833
  this.fail(`${this.label} ปฏิเสธการเข้าสู่ระบบ กรุณาลองใหม่อีกครั้ง`);
@@ -2701,33 +2835,83 @@ var KcSsoCallbackComponent = class KcSsoCallbackComponent {
2701
2835
  }
2702
2836
  if (!code) {
2703
2837
  this.message.set(`กำลังเชื่อมต่อกับ ${this.label}...`);
2704
- this.http.get(`${this.config.apiBase}/user/sso/authorize-url`).subscribe({
2705
- next: ({ url, state }) => {
2706
- this.auth.setTemp(KC_SSO_STATE_KEY, state);
2707
- this.auth.redirect(url);
2708
- },
2709
- error: () => this.fail(`ไม่สามารถเชื่อมต่อ ${this.label} ได้ กรุณาเริ่มเข้าสู่ระบบใหม่`)
2710
- });
2838
+ this.startFlow(this.endpoint, this.stateKey, this.label);
2711
2839
  return;
2712
2840
  }
2713
2841
  if (!expectedState || expectedState !== params.get("state")) {
2714
2842
  this.fail("คำขอเข้าสู่ระบบไม่ถูกต้อง (state ไม่ตรงกัน) กรุณาเริ่มเข้าสู่ระบบใหม่");
2715
2843
  return;
2716
2844
  }
2845
+ this.exchange(code);
2846
+ }
2847
+ startFlow(endpoint, stateKey, label) {
2848
+ this.http.get(`${this.config.apiBase}/user/${endpoint}/authorize-url`).subscribe({
2849
+ next: ({ url, state }) => {
2850
+ this.auth.setTemp(stateKey, state);
2851
+ this.auth.redirect(url);
2852
+ },
2853
+ error: () => this.fail(`ไม่สามารถเชื่อมต่อ ${label} ได้ กรุณาเริ่มเข้าสู่ระบบใหม่`)
2854
+ });
2855
+ }
2856
+ toRegister(ticket) {
2857
+ this.auth.setTemp(KC_SSO_TICKET_KEY, ticket);
2858
+ this.message.set("ยังไม่มีบัญชีในระบบ กำลังไปหน้าลงทะเบียน...");
2859
+ this.router.navigateByUrl(KC_AUTH_PATHS.register, { replaceUrl: true });
2860
+ }
2861
+ signIn(session) {
2862
+ this.auth.start(session);
2863
+ this.router.navigateByUrl(this.auth.safeReturnUrl(this.auth.takeTemp(KC_RETURN_URL_KEY)), { replaceUrl: true });
2864
+ }
2865
+ fail(message) {
2866
+ this.message.set(message);
2867
+ this.failed.set(true);
2868
+ }
2869
+ static ɵfac = i0.ɵɵngDeclareFactory({
2870
+ minVersion: "12.0.0",
2871
+ version: "22.2.1",
2872
+ ngImport: i0,
2873
+ type: KcExternalCallbackBase,
2874
+ deps: [],
2875
+ target: i0.ɵɵFactoryTarget.Directive
2876
+ });
2877
+ static ɵdir = i0.ɵɵngDeclareDirective({
2878
+ minVersion: "14.0.0",
2879
+ version: "22.2.1",
2880
+ type: KcExternalCallbackBase,
2881
+ isStandalone: true,
2882
+ ngImport: i0
2883
+ });
2884
+ };
2885
+ i0.ɵɵngDeclareClassMetadata({
2886
+ minVersion: "12.0.0",
2887
+ version: "22.2.1",
2888
+ ngImport: i0,
2889
+ type: KcExternalCallbackBase,
2890
+ decorators: [{ type: Directive }],
2891
+ ctorParameters: () => []
2892
+ });
2893
+ var KcSsoCallbackComponent = class KcSsoCallbackComponent extends KcExternalCallbackBase {
2894
+ label = this.config.sso?.label ?? "SSO";
2895
+ endpoint = "sso";
2896
+ stateKey = KC_SSO_STATE_KEY;
2897
+ exchange(code) {
2717
2898
  this.http.post(`${this.config.apiBase}/user/sso/callback`, { code }).subscribe({
2718
2899
  next: (res) => {
2719
2900
  if (res.mode === "signed-in" && res.token && res.user) {
2720
- this.auth.start({
2901
+ this.signIn({
2721
2902
  token: res.token,
2722
2903
  user: res.user
2723
2904
  });
2724
- this.router.navigateByUrl(this.auth.safeReturnUrl(this.auth.takeTemp(KC_RETURN_URL_KEY)), { replaceUrl: true });
2725
2905
  return;
2726
2906
  }
2727
2907
  if (res.mode === "register" && res.ticket) {
2728
- this.auth.setTemp(KC_SSO_TICKET_KEY, res.ticket);
2729
- this.message.set("ยังไม่มีบัญชีในระบบ กำลังไปหน้าลงทะเบียน...");
2730
- this.router.navigateByUrl(KC_AUTH_PATHS.register, { replaceUrl: true });
2908
+ this.toRegister(res.ticket);
2909
+ return;
2910
+ }
2911
+ if (res.mode === "thaid" && this.config.register === "thaid" && this.config.thaid) {
2912
+ const thaid = this.config.thaid.label;
2913
+ this.message.set(`ไม่พบบัญชีผู้ใช้ในระบบ กำลังไปยืนยันตัวตนด้วย ${thaid} เพื่อลงทะเบียน...`);
2914
+ this.startFlow("thaid", KC_THAID_STATE_KEY, thaid);
2731
2915
  return;
2732
2916
  }
2733
2917
  this.fail("ไม่สามารถเข้าสู่ระบบได้ กรุณาลองใหม่อีกครั้ง");
@@ -2735,16 +2919,12 @@ var KcSsoCallbackComponent = class KcSsoCallbackComponent {
2735
2919
  error: (err) => this.fail(kcErrorMessage(err, `ไม่สามารถเข้าสู่ระบบผ่าน ${this.label} ได้`))
2736
2920
  });
2737
2921
  }
2738
- fail(message) {
2739
- this.message.set(message);
2740
- this.failed.set(true);
2741
- }
2742
2922
  static ɵfac = i0.ɵɵngDeclareFactory({
2743
2923
  minVersion: "12.0.0",
2744
2924
  version: "22.2.1",
2745
2925
  ngImport: i0,
2746
2926
  type: KcSsoCallbackComponent,
2747
- deps: [],
2927
+ deps: null,
2748
2928
  target: i0.ɵɵFactoryTarget.Component
2749
2929
  });
2750
2930
  static ɵcmp = i0.ɵɵngDeclareComponent({
@@ -2753,20 +2933,9 @@ var KcSsoCallbackComponent = class KcSsoCallbackComponent {
2753
2933
  type: KcSsoCallbackComponent,
2754
2934
  isStandalone: true,
2755
2935
  selector: "kc-sso-callback",
2936
+ usesInheritance: true,
2756
2937
  ngImport: i0,
2757
- template: `
2758
- <div class="kc-auth-screen">
2759
- <div class="kc-auth-card center">
2760
- <h1 class="kc-auth-title">{{ label }}</h1>
2761
- @if (failed()) {
2762
- <div class="kc-auth-alert error" role="alert" style="margin-top: 16px">{{ message() }}</div>
2763
- <a class="kc-auth-btn" [routerLink]="paths.login">กลับไปหน้าเข้าสู่ระบบ</a>
2764
- } @else {
2765
- <p class="kc-auth-subtitle" style="margin-bottom: 0">{{ message() }}</p>
2766
- }
2767
- </div>
2768
- </div>
2769
- `,
2938
+ template: "\n <div class=\"kc-auth-screen\">\n <div class=\"kc-auth-card center\">\n <h1 class=\"kc-auth-title\">{{ label }}</h1>\n @if (failed()) {\n <div class=\"kc-auth-alert error\" role=\"alert\" style=\"margin-top: 16px\">{{ message() }}</div>\n <a class=\"kc-auth-btn\" [routerLink]=\"paths.login\">กลับไปหน้าเข้าสู่ระบบ</a>\n } @else {\n <p class=\"kc-auth-subtitle\" style=\"margin-bottom: 0\">{{ message() }}</p>\n }\n </div>\n </div>\n",
2770
2939
  isInline: true,
2771
2940
  dependencies: [{
2772
2941
  kind: "directive",
@@ -2800,22 +2969,83 @@ i0.ɵɵngDeclareClassMetadata({
2800
2969
  selector: "kc-sso-callback",
2801
2970
  standalone: true,
2802
2971
  imports: [RouterLink],
2803
- template: `
2804
- <div class="kc-auth-screen">
2805
- <div class="kc-auth-card center">
2806
- <h1 class="kc-auth-title">{{ label }}</h1>
2807
- @if (failed()) {
2808
- <div class="kc-auth-alert error" role="alert" style="margin-top: 16px">{{ message() }}</div>
2809
- <a class="kc-auth-btn" [routerLink]="paths.login">กลับไปหน้าเข้าสู่ระบบ</a>
2810
- } @else {
2811
- <p class="kc-auth-subtitle" style="margin-bottom: 0">{{ message() }}</p>
2812
- }
2813
- </div>
2814
- </div>
2815
- `
2972
+ template: CALLBACK_TEMPLATE
2816
2973
  }]
2817
- }],
2818
- ctorParameters: () => []
2974
+ }]
2975
+ });
2976
+ var KcThaidCallbackComponent = class KcThaidCallbackComponent extends KcExternalCallbackBase {
2977
+ label = this.config.thaid?.label ?? "ThaiD";
2978
+ endpoint = "thaid";
2979
+ stateKey = KC_THAID_STATE_KEY;
2980
+ exchange(code) {
2981
+ this.http.post(`${this.config.apiBase}/user/thaid/callback`, { code }).subscribe({
2982
+ next: (res) => {
2983
+ if (res && "token" in res && res.token && res.user) {
2984
+ this.signIn(res);
2985
+ return;
2986
+ }
2987
+ if (res && "mode" in res && res.mode === "register" && res.ticket && this.config.register === "thaid") {
2988
+ this.toRegister(res.ticket);
2989
+ return;
2990
+ }
2991
+ this.fail("ไม่สามารถเข้าสู่ระบบได้ กรุณาลองใหม่อีกครั้ง");
2992
+ },
2993
+ error: (err) => this.fail(kcErrorMessage(err, `ไม่สามารถเข้าสู่ระบบผ่าน ${this.label} ได้`))
2994
+ });
2995
+ }
2996
+ static ɵfac = i0.ɵɵngDeclareFactory({
2997
+ minVersion: "12.0.0",
2998
+ version: "22.2.1",
2999
+ ngImport: i0,
3000
+ type: KcThaidCallbackComponent,
3001
+ deps: null,
3002
+ target: i0.ɵɵFactoryTarget.Component
3003
+ });
3004
+ static ɵcmp = i0.ɵɵngDeclareComponent({
3005
+ minVersion: "17.0.0",
3006
+ version: "22.2.1",
3007
+ type: KcThaidCallbackComponent,
3008
+ isStandalone: true,
3009
+ selector: "kc-thaid-callback",
3010
+ usesInheritance: true,
3011
+ ngImport: i0,
3012
+ template: "\n <div class=\"kc-auth-screen\">\n <div class=\"kc-auth-card center\">\n <h1 class=\"kc-auth-title\">{{ label }}</h1>\n @if (failed()) {\n <div class=\"kc-auth-alert error\" role=\"alert\" style=\"margin-top: 16px\">{{ message() }}</div>\n <a class=\"kc-auth-btn\" [routerLink]=\"paths.login\">กลับไปหน้าเข้าสู่ระบบ</a>\n } @else {\n <p class=\"kc-auth-subtitle\" style=\"margin-bottom: 0\">{{ message() }}</p>\n }\n </div>\n </div>\n",
3013
+ isInline: true,
3014
+ dependencies: [{
3015
+ kind: "directive",
3016
+ type: RouterLink,
3017
+ selector: "[routerLink]",
3018
+ inputs: [
3019
+ "target",
3020
+ "queryParams",
3021
+ "fragment",
3022
+ "queryParamsHandling",
3023
+ "state",
3024
+ "info",
3025
+ "relativeTo",
3026
+ "preserveFragment",
3027
+ "skipLocationChange",
3028
+ "replaceUrl",
3029
+ "browserUrl",
3030
+ "routerLink"
3031
+ ]
3032
+ }]
3033
+ });
3034
+ };
3035
+ i0.ɵɵngDeclareClassMetadata({
3036
+ minVersion: "12.0.0",
3037
+ version: "22.2.1",
3038
+ ngImport: i0,
3039
+ type: KcThaidCallbackComponent,
3040
+ decorators: [{
3041
+ type: Component,
3042
+ args: [{
3043
+ selector: "kc-thaid-callback",
3044
+ standalone: true,
3045
+ imports: [RouterLink],
3046
+ template: CALLBACK_TEMPLATE
3047
+ }]
3048
+ }]
2819
3049
  });
2820
3050
  function kcAuthRoutes() {
2821
3051
  return [
@@ -2833,6 +3063,11 @@ function kcAuthRoutes() {
2833
3063
  path: "sso-callback",
2834
3064
  component: KcSsoCallbackComponent,
2835
3065
  title: "เข้าสู่ระบบ"
3066
+ },
3067
+ {
3068
+ path: "thaid-callback",
3069
+ component: KcThaidCallbackComponent,
3070
+ title: "เข้าสู่ระบบ"
2836
3071
  }
2837
3072
  ];
2838
3073
  }
@@ -2847,10 +3082,12 @@ var KcProfileComponent = class KcProfileComponent {
2847
3082
  success = signal("", ...ngDevMode ? [{ debugName: "success" }] : /* istanbul ignore next */ []);
2848
3083
  fullName = signal("", ...ngDevMode ? [{ debugName: "fullName" }] : /* istanbul ignore next */ []);
2849
3084
  orgName = signal("", ...ngDevMode ? [{ debugName: "orgName" }] : /* istanbul ignore next */ []);
3085
+ orgs = signal([], ...ngDevMode ? [{ debugName: "orgs" }] : /* istanbul ignore next */ []);
2850
3086
  form = inject(FormBuilder).nonNullable.group({
2851
3087
  email: ["", [Validators.required, Validators.email]],
2852
3088
  mobile: ["", [Validators.required, Validators.pattern(/^0\d{9}$/)]],
2853
3089
  username: ["", [Validators.required, Validators.pattern(/^\S+$/)]],
3090
+ org: [""],
2854
3091
  password: [""],
2855
3092
  confirmPassword: [""]
2856
3093
  });
@@ -2862,10 +3099,12 @@ var KcProfileComponent = class KcProfileComponent {
2862
3099
  next: (me) => {
2863
3100
  this.fullName.set(me.fullName);
2864
3101
  this.orgName.set(me.orgName || me.org);
3102
+ if (this.config.profileOrgChange && me.orgs?.length) this.orgs.set(me.orgs);
2865
3103
  this.form.patchValue({
2866
3104
  email: me.email ?? "",
2867
3105
  mobile: me.mobile ?? "",
2868
- username: me.username ?? ""
3106
+ username: me.username ?? "",
3107
+ org: me.org ?? ""
2869
3108
  });
2870
3109
  this.loading.set(false);
2871
3110
  },
@@ -2885,7 +3124,7 @@ var KcProfileComponent = class KcProfileComponent {
2885
3124
  this.success.set("");
2886
3125
  this.form.markAllAsTouched();
2887
3126
  const v = this.form.getRawValue();
2888
- const problem = kcContactProblem(v.email, v.mobile) || kcUsernameProblem(v.username) || kcPasswordProblem(v.password, v.confirmPassword, this.config.passwordMinLength, false);
3127
+ const problem = kcContactProblem(v.email, v.mobile) || kcUsernameProblem(v.username) || kcPasswordProblem(v.password, v.confirmPassword, this.config.passwordMinLength, false) || (this.orgs().length && !v.org ? `กรุณาเลือก${this.config.orgLabel}ที่สังกัด` : "");
2889
3128
  this.error.set(problem);
2890
3129
  if (problem) return;
2891
3130
  const body = {
@@ -2894,6 +3133,7 @@ var KcProfileComponent = class KcProfileComponent {
2894
3133
  mobile: v.mobile.trim(),
2895
3134
  password: v.password
2896
3135
  };
3136
+ if (this.orgs().length) body.org = v.org;
2897
3137
  this.saving.set(true);
2898
3138
  this.http.put(`${this.config.apiBase}/user/me`, body).subscribe({
2899
3139
  next: (res) => {
@@ -2942,11 +3182,19 @@ var KcProfileComponent = class KcProfileComponent {
2942
3182
  <input class="kc-auth-input" type="text" [value]="fullName()" disabled />
2943
3183
  </label>
2944
3184
 
3185
+ @if (orgs().length) {
3186
+ <div class="kc-auth-field">
3187
+ <label class="kc-auth-label" for="kc-profile-org">{{ config.orgLabel }}ที่สังกัด <span class="req">*</span></label>
3188
+ <app-search-select inputId="kc-profile-org" [options]="orgs()" formControlName="org"
3189
+ [placeholder]="'-- เลือก' + config.orgLabel + 'ที่สังกัด --'" />
3190
+ </div>
3191
+ } @else {
2945
3192
  <label class="kc-auth-field">
2946
3193
  <span>{{ config.orgLabel }}ที่สังกัด</span>
2947
3194
  <input class="kc-auth-input" type="text" [value]="orgName()" disabled />
2948
3195
  <small class="kc-auth-hint">หากต้องการย้าย{{ config.orgLabel }} กรุณาติดต่อผู้ดูแลระบบ</small>
2949
3196
  </label>
3197
+ }
2950
3198
 
2951
3199
  <label class="kc-auth-field">
2952
3200
  <span>อีเมล <span class="req">*</span></span>
@@ -3051,6 +3299,17 @@ var KcProfileComponent = class KcProfileComponent {
3051
3299
  ],
3052
3300
  outputs: ["ngModelChange"]
3053
3301
  },
3302
+ {
3303
+ kind: "component",
3304
+ type: SearchSelectComponent,
3305
+ selector: "app-search-select",
3306
+ inputs: [
3307
+ "options",
3308
+ "placeholder",
3309
+ "searchHint",
3310
+ "inputId"
3311
+ ]
3312
+ },
3054
3313
  {
3055
3314
  kind: "component",
3056
3315
  type: KcPasswordToggleComponent,
@@ -3069,7 +3328,11 @@ i0.ɵɵngDeclareClassMetadata({
3069
3328
  args: [{
3070
3329
  selector: "kc-profile",
3071
3330
  standalone: true,
3072
- imports: [ReactiveFormsModule, KcPasswordToggleComponent],
3331
+ imports: [
3332
+ ReactiveFormsModule,
3333
+ SearchSelectComponent,
3334
+ KcPasswordToggleComponent
3335
+ ],
3073
3336
  template: `
3074
3337
  <div class="kc-auth-page single">
3075
3338
  <div class="kc-auth-panel">
@@ -3086,11 +3349,19 @@ i0.ɵɵngDeclareClassMetadata({
3086
3349
  <input class="kc-auth-input" type="text" [value]="fullName()" disabled />
3087
3350
  </label>
3088
3351
 
3352
+ @if (orgs().length) {
3353
+ <div class="kc-auth-field">
3354
+ <label class="kc-auth-label" for="kc-profile-org">{{ config.orgLabel }}ที่สังกัด <span class="req">*</span></label>
3355
+ <app-search-select inputId="kc-profile-org" [options]="orgs()" formControlName="org"
3356
+ [placeholder]="'-- เลือก' + config.orgLabel + 'ที่สังกัด --'" />
3357
+ </div>
3358
+ } @else {
3089
3359
  <label class="kc-auth-field">
3090
3360
  <span>{{ config.orgLabel }}ที่สังกัด</span>
3091
3361
  <input class="kc-auth-input" type="text" [value]="orgName()" disabled />
3092
3362
  <small class="kc-auth-hint">หากต้องการย้าย{{ config.orgLabel }} กรุณาติดต่อผู้ดูแลระบบ</small>
3093
3363
  </label>
3364
+ }
3094
3365
 
3095
3366
  <label class="kc-auth-field">
3096
3367
  <span>อีเมล <span class="req">*</span></span>
@@ -3163,6 +3434,33 @@ var KcConfigUsersComponent = class KcConfigUsersComponent {
3163
3434
  search = signal("", ...ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []);
3164
3435
  editing = signal(null, ...ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []);
3165
3436
  myId = computed(() => this.auth.user()?.id ?? "", ...ngDevMode ? [{ debugName: "myId" }] : /* istanbul ignore next */ []);
3437
+ methodsSaved = signal(null, ...ngDevMode ? [{ debugName: "methodsSaved" }] : /* istanbul ignore next */ []);
3438
+ methodsDraft = signal(null, ...ngDevMode ? [{ debugName: "methodsDraft" }] : /* istanbul ignore next */ []);
3439
+ savingMethods = signal(false, ...ngDevMode ? [{ debugName: "savingMethods" }] : /* istanbul ignore next */ []);
3440
+ methodsError = signal("", ...ngDevMode ? [{ debugName: "methodsError" }] : /* istanbul ignore next */ []);
3441
+ methodOptions = computed(() => [
3442
+ ...this.config.passwordLogin ? [{
3443
+ key: "password",
3444
+ label: "ชื่อผู้ใช้ / รหัสผ่าน"
3445
+ }] : [],
3446
+ ...this.config.sso ? [{
3447
+ key: "sso",
3448
+ label: this.config.sso.label
3449
+ }] : [],
3450
+ ...this.config.thaid ? [{
3451
+ key: "thaid",
3452
+ label: this.config.thaid.label
3453
+ }] : []
3454
+ ], ...ngDevMode ? [{ debugName: "methodOptions" }] : /* istanbul ignore next */ []);
3455
+ enabledCount = computed(() => {
3456
+ const m = this.methodsDraft();
3457
+ return m ? this.methodOptions().filter((o) => m[o.key]).length : 0;
3458
+ }, ...ngDevMode ? [{ debugName: "enabledCount" }] : /* istanbul ignore next */ []);
3459
+ methodsDirty = computed(() => {
3460
+ const a = this.methodsDraft();
3461
+ const b = this.methodsSaved();
3462
+ return !!a && !!b && (a.password !== b.password || a.sso !== b.sso || a.thaid !== b.thaid);
3463
+ }, ...ngDevMode ? [{ debugName: "methodsDirty" }] : /* istanbul ignore next */ []);
3166
3464
  orgOptions = computed(() => [{
3167
3465
  value: "",
3168
3466
  label: "-- ไม่ระบุ --"
@@ -3188,6 +3486,51 @@ var KcConfigUsersComponent = class KcConfigUsersComponent {
3188
3486
  }
3189
3487
  ngOnInit() {
3190
3488
  this.load();
3489
+ this.http.get(`${this.config.apiBase}/user/login-methods`).subscribe({
3490
+ next: (m) => {
3491
+ const v = {
3492
+ password: m?.password !== false,
3493
+ sso: m?.sso !== false,
3494
+ thaid: m?.thaid !== false
3495
+ };
3496
+ this.methodsSaved.set(v);
3497
+ this.methodsDraft.set({ ...v });
3498
+ },
3499
+ error: () => {}
3500
+ });
3501
+ }
3502
+ toggleMethod(key, on) {
3503
+ const m = this.methodsDraft();
3504
+ if (!m) return;
3505
+ const next = {
3506
+ ...m,
3507
+ [key]: on
3508
+ };
3509
+ if (!this.methodOptions().some((o) => next[o.key])) return;
3510
+ this.methodsError.set("");
3511
+ this.methodsDraft.set(next);
3512
+ }
3513
+ saveMethods() {
3514
+ const m = this.methodsDraft();
3515
+ if (!m || this.savingMethods()) return;
3516
+ if (!this.methodOptions().some((o) => m[o.key])) {
3517
+ this.methodsError.set("ต้องเปิดการเข้าสู่ระบบอย่างน้อย 1 วิธี");
3518
+ return;
3519
+ }
3520
+ this.savingMethods.set(true);
3521
+ this.http.put(`${this.config.apiBase}/config/login-methods`, m).subscribe({
3522
+ next: (res) => {
3523
+ this.savingMethods.set(false);
3524
+ this.methodsSaved.set({ ...m });
3525
+ this.methodsError.set("");
3526
+ this.error.set("");
3527
+ this.success.set(res?.message || "บันทึกวิธีเข้าสู่ระบบเรียบร้อยแล้ว");
3528
+ },
3529
+ error: (err) => {
3530
+ this.savingMethods.set(false);
3531
+ this.methodsError.set(kcErrorMessage(err, "บันทึกวิธีเข้าสู่ระบบไม่สำเร็จ"));
3532
+ }
3533
+ });
3191
3534
  }
3192
3535
  load() {
3193
3536
  this.loading.set(true);
@@ -3286,6 +3629,30 @@ var KcConfigUsersComponent = class KcConfigUsersComponent {
3286
3629
  <div class="kc-auth-alert success" role="status">{{ success() }}</div>
3287
3630
  }
3288
3631
 
3632
+ @if (methodOptions().length > 1 && methodsDraft(); as m) {
3633
+ <div class="kc-auth-panel">
3634
+ <h2 class="kc-auth-panel-title">วิธีเข้าสู่ระบบ</h2>
3635
+ <div class="kc-auth-methods">
3636
+ @for (o of methodOptions(); track o.key) {
3637
+ <label class="kc-auth-check">
3638
+ <input type="checkbox" [checked]="m[o.key]" [disabled]="savingMethods() || (m[o.key] && enabledCount() === 1)"
3639
+ (change)="toggleMethod(o.key, $any($event.target).checked)" />
3640
+ {{ o.label }}
3641
+ </label>
3642
+ }
3643
+ <button type="button" class="kc-auth-btn primary" [disabled]="savingMethods() || !methodsDirty()" (click)="saveMethods()">
3644
+ {{ savingMethods() ? 'กำลังบันทึก...' : 'บันทึก' }}
3645
+ </button>
3646
+ </div>
3647
+ @if (methodsError()) {
3648
+ <div class="kc-auth-alert error" role="alert" style="margin: 10px 0 0">{{ methodsError() }}</div>
3649
+ }
3650
+ <p class="kc-auth-hint" style="margin: 10px 0 0">
3651
+ ต้องเปิดอย่างน้อย 1 วิธี — ก่อนปิดวิธีอื่น ให้ลองเข้าสู่ระบบด้วยวิธีที่จะเหลือให้ได้ก่อน มีผลกับการเข้าสู่ระบบครั้งถัดไป (ผู้ที่เข้าอยู่แล้วใช้งานต่อได้)
3652
+ </p>
3653
+ </div>
3654
+ }
3655
+
3289
3656
  <div class="kc-auth-panel">
3290
3657
  <input class="kc-auth-input" type="search" [placeholder]="'ค้นหาชื่อผู้ใช้ ชื่อ-สกุล ' + config.orgLabel + ' ระดับผู้ใช้ หรืออีเมล'"
3291
3658
  [ngModel]="search()" (ngModelChange)="search.set($event)" aria-label="ค้นหาผู้ใช้" />
@@ -3499,6 +3866,30 @@ i0.ɵɵngDeclareClassMetadata({
3499
3866
  <div class="kc-auth-alert success" role="status">{{ success() }}</div>
3500
3867
  }
3501
3868
 
3869
+ @if (methodOptions().length > 1 && methodsDraft(); as m) {
3870
+ <div class="kc-auth-panel">
3871
+ <h2 class="kc-auth-panel-title">วิธีเข้าสู่ระบบ</h2>
3872
+ <div class="kc-auth-methods">
3873
+ @for (o of methodOptions(); track o.key) {
3874
+ <label class="kc-auth-check">
3875
+ <input type="checkbox" [checked]="m[o.key]" [disabled]="savingMethods() || (m[o.key] && enabledCount() === 1)"
3876
+ (change)="toggleMethod(o.key, $any($event.target).checked)" />
3877
+ {{ o.label }}
3878
+ </label>
3879
+ }
3880
+ <button type="button" class="kc-auth-btn primary" [disabled]="savingMethods() || !methodsDirty()" (click)="saveMethods()">
3881
+ {{ savingMethods() ? 'กำลังบันทึก...' : 'บันทึก' }}
3882
+ </button>
3883
+ </div>
3884
+ @if (methodsError()) {
3885
+ <div class="kc-auth-alert error" role="alert" style="margin: 10px 0 0">{{ methodsError() }}</div>
3886
+ }
3887
+ <p class="kc-auth-hint" style="margin: 10px 0 0">
3888
+ ต้องเปิดอย่างน้อย 1 วิธี — ก่อนปิดวิธีอื่น ให้ลองเข้าสู่ระบบด้วยวิธีที่จะเหลือให้ได้ก่อน มีผลกับการเข้าสู่ระบบครั้งถัดไป (ผู้ที่เข้าอยู่แล้วใช้งานต่อได้)
3889
+ </p>
3890
+ </div>
3891
+ }
3892
+
3502
3893
  <div class="kc-auth-panel">
3503
3894
  <input class="kc-auth-input" type="search" [placeholder]="'ค้นหาชื่อผู้ใช้ ชื่อ-สกุล ' + config.orgLabel + ' ระดับผู้ใช้ หรืออีเมล'"
3504
3895
  [ngModel]="search()" (ngModelChange)="search.set($event)" aria-label="ค้นหาผู้ใช้" />
@@ -4115,6 +4506,6 @@ i0.ɵɵngDeclareClassMetadata({
4115
4506
  }],
4116
4507
  ctorParameters: () => []
4117
4508
  });
4118
- export { AnchoredPopup, ClockPickerDirective, ClockPickerPopupComponent, DatePickerDirective, DatePickerPopupComponent, KC_AUTH_CONFIG, KC_AUTH_PATHS, KC_RETURN_URL_KEY, KC_SSO_STATE_KEY, KC_SSO_TICKET_KEY, KcAuthService, KcConfigKclluComponent, KcConfigUsersComponent, KcLoginComponent, KcPasswordToggleComponent, KcProfileComponent, KcRegisterComponent, KcSsoCallbackComponent, PICKER_CSS, SearchSelectComponent, ThaiDatePickerComponent, ensurePickerStyles, fixThaiText, formatDate, kcAdminGuard, kcAuthGuard, kcAuthInterceptor, kcAuthRoutes, kcErrorMessage, kcTokenExpired, parseDate, provideKcAuth, provideThaiPasteFix, resolveKcAuthConfig, toBuddhistText, toChristianText };
4509
+ export { AnchoredPopup, ClockPickerDirective, ClockPickerPopupComponent, DatePickerDirective, DatePickerPopupComponent, KC_AUTH_CONFIG, KC_AUTH_PATHS, KC_LOGIN_LOCK_KEY, KC_LOGIN_LOCK_SECONDS, KC_RETURN_URL_KEY, KC_SSO_STATE_KEY, KC_SSO_TICKET_KEY, KC_THAID_STATE_KEY, KcAuthService, KcConfigKclluComponent, KcConfigUsersComponent, KcLoginComponent, KcPasswordToggleComponent, KcProfileComponent, KcRegisterComponent, KcSsoCallbackComponent, KcThaidCallbackComponent, PICKER_CSS, SearchSelectComponent, ThaiDatePickerComponent, ensurePickerStyles, fixThaiText, formatDate, kcAdminGuard, kcAuthGuard, kcAuthInterceptor, kcAuthRoutes, kcErrorMessage, kcLoginRetryAfter, kcTokenExpired, parseDate, provideKcAuth, provideThaiPasteFix, resolveKcAuthConfig, toBuddhistText, toChristianText };
4119
4510
 
4120
4511
  //# sourceMappingURL=kengcom202-kcore-ui.mjs.map