Users.svelte 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <script lang="ts">
  2. import { getBackendConfig, getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
  3. import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
  4. import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
  5. import {
  6. getLdapConfig,
  7. updateLdapConfig,
  8. getLdapServer,
  9. updateLdapServer
  10. } from '$lib/apis/auths';
  11. import { onMount, getContext } from 'svelte';
  12. import { models, config } from '$lib/stores';
  13. import Switch from '$lib/components/common/Switch.svelte';
  14. import { setDefaultModels } from '$lib/apis/configs';
  15. import Tooltip from '$lib/components/common/Tooltip.svelte';
  16. import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
  17. const i18n = getContext('i18n');
  18. export let saveHandler: Function;
  19. let defaultModelId = '';
  20. let whitelistEnabled = false;
  21. let whitelistModels = [''];
  22. let permissions = {
  23. chat: {
  24. deletion: true,
  25. edit: true,
  26. temporary: true
  27. }
  28. };
  29. let chatDeletion = true;
  30. let chatEdit = true;
  31. let chatTemporary = true;
  32. // LDAP
  33. let ENABLE_LDAP = false;
  34. let LDAP_SERVER = {
  35. label: '',
  36. host: '',
  37. port: '',
  38. attribute_for_username: 'uid',
  39. app_dn: '',
  40. app_dn_password: '',
  41. search_base: '',
  42. search_filters: '',
  43. use_tls: false,
  44. certificate_path: '',
  45. ciphers: ''
  46. };
  47. const updateLdapServerHandler = async () => {
  48. if (!ENABLE_LDAP) return;
  49. const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
  50. toast.error(error);
  51. return null;
  52. });
  53. if (res) {
  54. toast.success($i18n.t('LDAP server updated'));
  55. }
  56. };
  57. onMount(async () => {
  58. permissions = await getUserPermissions(localStorage.token);
  59. chatDeletion = permissions?.chat?.deletion ?? true;
  60. chatEdit = permissions?.chat?.editing ?? true;
  61. chatTemporary = permissions?.chat?.temporary ?? true;
  62. const res = await getModelFilterConfig(localStorage.token);
  63. if (res) {
  64. whitelistEnabled = res.enabled;
  65. whitelistModels = res.models.length > 0 ? res.models : [''];
  66. }
  67. (async () => {
  68. LDAP_SERVER = await getLdapServer(localStorage.token);
  69. })();
  70. const ldapConfig = await getLdapConfig(localStorage.token);
  71. ENABLE_LDAP = ldapConfig.ENABLE_LDAP;
  72. defaultModelId = $config.default_models ? $config?.default_models.split(',')[0] : '';
  73. });
  74. </script>
  75. <form
  76. class="flex flex-col h-full justify-between space-y-3 text-sm"
  77. on:submit|preventDefault={async () => {
  78. // console.log('submit');
  79. await setDefaultModels(localStorage.token, defaultModelId);
  80. await updateUserPermissions(localStorage.token, {
  81. chat: {
  82. deletion: chatDeletion,
  83. editing: chatEdit,
  84. temporary: chatTemporary
  85. }
  86. });
  87. await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
  88. await updateLdapServerHandler();
  89. saveHandler();
  90. await config.set(await getBackendConfig());
  91. }}
  92. >
  93. <div class=" space-y-3 overflow-y-scroll max-h-full pr-1.5">
  94. <div>
  95. <div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
  96. <div class=" flex w-full justify-between my-2 pr-2">
  97. <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div>
  98. <Switch bind:state={chatDeletion} />
  99. </div>
  100. <div class=" flex w-full justify-between my-2 pr-2">
  101. <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Editing')}</div>
  102. <Switch bind:state={chatEdit} />
  103. </div>
  104. <div class=" flex w-full justify-between my-2 pr-2">
  105. <div class=" self-center text-xs font-medium">{$i18n.t('Allow Temporary Chat')}</div>
  106. <Switch bind:state={chatTemporary} />
  107. </div>
  108. </div>
  109. <hr class=" dark:border-gray-850" />
  110. <div class=" space-y-3">
  111. <div class="mt-2 space-y-2 pr-1.5">
  112. <div class="flex justify-between items-center text-sm">
  113. <div class=" font-medium">{$i18n.t('LDAP')}</div>
  114. <div class="mt-1">
  115. <Switch
  116. bind:state={ENABLE_LDAP}
  117. on:change={async () => {
  118. updateLdapConfig(localStorage.token, ENABLE_LDAP);
  119. }}
  120. />
  121. </div>
  122. </div>
  123. {#if ENABLE_LDAP}
  124. <div class="flex flex-col gap-1">
  125. <div class="flex w-full gap-2">
  126. <div class="w-full">
  127. <div class=" self-center text-xs font-medium min-w-fit mb-1">
  128. {$i18n.t('Label')}
  129. </div>
  130. <input
  131. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  132. required
  133. placeholder={$i18n.t('Enter server label')}
  134. bind:value={LDAP_SERVER.label}
  135. />
  136. </div>
  137. <div class="w-full"></div>
  138. </div>
  139. <div class="flex w-full gap-2">
  140. <div class="w-full">
  141. <div class=" self-center text-xs font-medium min-w-fit mb-1">
  142. {$i18n.t('Host')}
  143. </div>
  144. <input
  145. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  146. required
  147. placeholder={$i18n.t('Enter server host')}
  148. bind:value={LDAP_SERVER.host}
  149. />
  150. </div>
  151. <div class="w-full">
  152. <div class=" self-center text-xs font-medium min-w-fit mb-1">
  153. {$i18n.t('Port')}
  154. </div>
  155. <Tooltip
  156. placement="top-start"
  157. content={$i18n.t('Default to 389 or 636 if TLS is enabled')}
  158. className="w-full"
  159. >
  160. <input
  161. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  162. type="number"
  163. placeholder={$i18n.t('Enter server port')}
  164. bind:value={LDAP_SERVER.port}
  165. />
  166. </Tooltip>
  167. </div>
  168. </div>
  169. <div class="flex w-full gap-2">
  170. <div class="w-full">
  171. <div class=" self-center text-xs font-medium min-w-fit mb-1">
  172. {$i18n.t('Application DN')}
  173. </div>
  174. <Tooltip
  175. content={$i18n.t('The Application Account DN you bind with for search')}
  176. placement="top-start"
  177. >
  178. <input
  179. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  180. required
  181. placeholder={$i18n.t('Enter Application DN')}
  182. bind:value={LDAP_SERVER.app_dn}
  183. />
  184. </Tooltip>
  185. </div>
  186. <div class="w-full">
  187. <div class=" self-center text-xs font-medium min-w-fit mb-1">
  188. {$i18n.t('Application DN Password')}
  189. </div>
  190. <SensitiveInput
  191. placeholder={$i18n.t('Enter Application DN Password')}
  192. bind:value={LDAP_SERVER.app_dn_password}
  193. />
  194. </div>
  195. </div>
  196. <div class="flex w-full gap-2">
  197. <div class="w-full">
  198. <div class=" self-center text-xs font-medium min-w-fit mb-1">
  199. {$i18n.t('Attribute for Username')}
  200. </div>
  201. <Tooltip
  202. content={$i18n.t(
  203. 'The LDAP attribute that maps to the username that users use to sign in.'
  204. )}
  205. placement="top-start"
  206. >
  207. <input
  208. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  209. required
  210. placeholder={$i18n.t('Example: sAMAccountName or uid or userPrincipalName')}
  211. bind:value={LDAP_SERVER.attribute_for_username}
  212. />
  213. </Tooltip>
  214. </div>
  215. </div>
  216. <div class="flex w-full gap-2">
  217. <div class="w-full">
  218. <div class=" self-center text-xs font-medium min-w-fit mb-1">
  219. {$i18n.t('Search Base')}
  220. </div>
  221. <Tooltip content={$i18n.t('The base to search for users')} placement="top-start">
  222. <input
  223. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  224. required
  225. placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')}
  226. bind:value={LDAP_SERVER.search_base}
  227. />
  228. </Tooltip>
  229. </div>
  230. </div>
  231. <div class="flex w-full gap-2">
  232. <div class="w-full">
  233. <div class=" self-center text-xs font-medium min-w-fit mb-1">
  234. {$i18n.t('Search Filters')}
  235. </div>
  236. <input
  237. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  238. placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')}
  239. bind:value={LDAP_SERVER.search_filters}
  240. />
  241. </div>
  242. </div>
  243. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  244. <a
  245. class=" text-gray-300 font-medium underline"
  246. href="https://ldap.com/ldap-filters/"
  247. target="_blank"
  248. >
  249. {$i18n.t('Click here for filter guides.')}
  250. </a>
  251. </div>
  252. <div>
  253. <div class="flex justify-between items-center text-sm">
  254. <div class=" font-medium">{$i18n.t('TLS')}</div>
  255. <div class="mt-1">
  256. <Switch bind:state={LDAP_SERVER.use_tls} />
  257. </div>
  258. </div>
  259. {#if LDAP_SERVER.use_tls}
  260. <div class="flex w-full gap-2">
  261. <div class="w-full">
  262. <div class=" self-center text-xs font-medium min-w-fit mb-1 mt-1">
  263. {$i18n.t('Certificate Path')}
  264. </div>
  265. <input
  266. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  267. required
  268. placeholder={$i18n.t('Enter certificate path')}
  269. bind:value={LDAP_SERVER.certificate_path}
  270. />
  271. </div>
  272. </div>
  273. <div class="flex w-full gap-2">
  274. <div class="w-full">
  275. <div class=" self-center text-xs font-medium min-w-fit mb-1">
  276. {$i18n.t('Ciphers')}
  277. </div>
  278. <Tooltip content={$i18n.t('Default to ALL')} placement="top-start">
  279. <input
  280. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  281. placeholder={$i18n.t('Example: ALL')}
  282. bind:value={LDAP_SERVER.ciphers}
  283. />
  284. </Tooltip>
  285. </div>
  286. <div class="w-full"></div>
  287. </div>
  288. {/if}
  289. </div>
  290. </div>
  291. {/if}
  292. </div>
  293. </div>
  294. <hr class=" dark:border-gray-850 my-2" />
  295. <div class="mt-2 space-y-3">
  296. <div>
  297. <div class="mb-2">
  298. <div class="flex justify-between items-center text-xs">
  299. <div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
  300. </div>
  301. </div>
  302. <div class=" space-y-1 mb-3">
  303. <div class="mb-2">
  304. <div class="flex justify-between items-center text-xs">
  305. <div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
  306. </div>
  307. </div>
  308. <div class="flex-1 mr-2">
  309. <select
  310. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  311. bind:value={defaultModelId}
  312. placeholder="Select a model"
  313. >
  314. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  315. {#each $models.filter((model) => model.id) as model}
  316. <option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
  317. {/each}
  318. </select>
  319. </div>
  320. </div>
  321. <div class=" space-y-1">
  322. <div class="mb-2">
  323. <div class="flex justify-between items-center text-xs my-3 pr-2">
  324. <div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
  325. <Switch bind:state={whitelistEnabled} />
  326. </div>
  327. </div>
  328. {#if whitelistEnabled}
  329. <div>
  330. <div class=" space-y-1.5">
  331. {#each whitelistModels as modelId, modelIdx}
  332. <div class="flex w-full">
  333. <div class="flex-1 mr-2">
  334. <select
  335. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  336. bind:value={modelId}
  337. placeholder="Select a model"
  338. >
  339. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  340. {#each $models.filter((model) => model.id) as model}
  341. <option value={model.id} class="bg-gray-100 dark:bg-gray-700"
  342. >{model.name}</option
  343. >
  344. {/each}
  345. </select>
  346. </div>
  347. {#if modelIdx === 0}
  348. <button
  349. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
  350. type="button"
  351. on:click={() => {
  352. if (whitelistModels.at(-1) !== '') {
  353. whitelistModels = [...whitelistModels, ''];
  354. }
  355. }}
  356. >
  357. <svg
  358. xmlns="http://www.w3.org/2000/svg"
  359. viewBox="0 0 16 16"
  360. fill="currentColor"
  361. class="w-4 h-4"
  362. >
  363. <path
  364. d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
  365. />
  366. </svg>
  367. </button>
  368. {:else}
  369. <button
  370. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
  371. type="button"
  372. on:click={() => {
  373. whitelistModels.splice(modelIdx, 1);
  374. whitelistModels = whitelistModels;
  375. }}
  376. >
  377. <svg
  378. xmlns="http://www.w3.org/2000/svg"
  379. viewBox="0 0 16 16"
  380. fill="currentColor"
  381. class="w-4 h-4"
  382. >
  383. <path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
  384. </svg>
  385. </button>
  386. {/if}
  387. </div>
  388. {/each}
  389. </div>
  390. <div class="flex justify-end items-center text-xs mt-1.5 text-right">
  391. <div class=" text-xs font-medium">
  392. {whitelistModels.length}
  393. {$i18n.t('Model(s) Whitelisted')}
  394. </div>
  395. </div>
  396. </div>
  397. {/if}
  398. </div>
  399. </div>
  400. </div>
  401. </div>
  402. <div class="flex justify-end pt-3 text-sm font-medium">
  403. <button
  404. class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
  405. type="submit"
  406. >
  407. {$i18n.t('Save')}
  408. </button>
  409. </div>
  410. </form>