AdminSettings.vue 85 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549
  1. <template>
  2. <div v-if="loaded">
  3. <div class="header bg-primary pb-2 mt-n4">
  4. <div class="container-fluid">
  5. <div class="header-body">
  6. <div class="row align-items-center py-4">
  7. <div class="col-lg-6 col-7">
  8. <p class="display-1 text-white d-inline-block mb-0">Settings</p>
  9. <p class="h3 text-white font-weight-light">Manage your server settings</p>
  10. </div>
  11. </div>
  12. </div>
  13. </div>
  14. </div>
  15. <div class="container">
  16. <div class="row">
  17. <div class="col-12 col-md-3">
  18. <div class="nav-wrapper">
  19. <div class="nav flex-column nav-pills" id="tabs-icons-text" role="tablist" aria-orientation="vertical">
  20. <div v-for="tab in tabs" class="nav-item">
  21. <a class="nav-link mb-sm-3" :class="{ active: tabIndex === tab.id }" href="#" @click.prevent="toggleTab(tab.id)">
  22. <i :class="tab.icon"></i>
  23. <span class="ml-2">{{ tab.title }}</span>
  24. </a>
  25. </div>
  26. </div>
  27. </div>
  28. </div>
  29. <div class="col-12 col-md-9">
  30. <div class="card shadow mt-3">
  31. <div class="card-body">
  32. <div class="tab-content">
  33. <div v-if="tabIndex === 1" class="tab-pane fade show active">
  34. <tab-header title="Settings" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('overview')" />
  35. <div class="row">
  36. <div class="col-12 col-md-6">
  37. <div class="card shadow-none border card-body" style="padding: 1.1rem 1.6rem">
  38. <div class="form-group mb-0">
  39. <label for="form-summary" class="font-weight-bold">Registration Status</label>
  40. <select v-model="features.registration_status" class="form-control form-control-muted">
  41. <option value="open" >Open - Anyone can register</option>
  42. <option value="filtered">Filtered - Anyone can apply (Curated Onboarding)</option>
  43. <option value="closed">Closed - Nobody can register</option>
  44. </select>
  45. </div>
  46. </div>
  47. <checkbox
  48. name="Cloud Storage"
  49. :value="features.cloud_storage"
  50. description="Store photos and videos on S3 compatible object storage providers."
  51. @change="handleChange($event, 'features', 'cloud_storage')"
  52. />
  53. <checkbox
  54. name="ActivityPub"
  55. :value="features.activitypub_enabled"
  56. description="ActivityPub federation, compatible with Pixelfed, Mastodon and other projects."
  57. @change="handleChange($event, 'features', 'activitypub_enabled')"
  58. />
  59. <checkbox
  60. name="Account Migration"
  61. :value="features.account_migration"
  62. description="Allow local accounts to migrate to other local or remote accounts."
  63. @change="handleChange($event, 'features', 'account_migration')"
  64. />
  65. </div>
  66. <div class="col-12 col-md-6">
  67. <checkbox
  68. name="Mobile APIs"
  69. :value="features.mobile_apis"
  70. description="Enable apis required for official mobile app support and 3rd party apps."
  71. @change="handleChange($event, 'features', 'mobile_apis')"
  72. />
  73. <checkbox
  74. name="Stories"
  75. :value="features.stories"
  76. description="Allow users to share federated ephemeral Stories that disappear after 24 hours."
  77. @change="handleChange($event, 'features', 'stories')"
  78. />
  79. <checkbox
  80. name="Instagram Import"
  81. :value="features.instagram_import"
  82. description="Enable users to use the <span class='font-weight-bold'>experimental</span> Instagram Import support."
  83. @change="handleChange($event, 'features', 'instagram_import')"
  84. />
  85. <checkbox
  86. name="Spam detection"
  87. :value="features.autospam_enabled"
  88. description="Detect and remove spam from timelines using the automated Autospam detection."
  89. @change="handleChange($event, 'features', 'autospam_enabled')"
  90. />
  91. </div>
  92. </div>
  93. </div>
  94. <div v-else-if="tabIndex === 'landing'" class="tab-pane fade show active" role="tabpanel">
  95. <tab-header title="Landing" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('landing')" />
  96. <div class="row">
  97. <div class="col-12 col-md-6">
  98. <div class="card shadow-none border card-body" style="padding: 1.1rem 1.6rem">
  99. <div class="form-group mb-0">
  100. <label for="form-summary" class="font-weight-bold">Admin Account</label>
  101. <select v-model="landing.current_admin" class="form-control form-control-muted">
  102. <option disabled="" value="0">Select a designated admin</option>
  103. <option v-for="(acct, index) in landing.admins" :key="'pfc-' + acct + index" :value="acct.profile_id">{{ acct.username }}</option>
  104. </select>
  105. </div>
  106. </div>
  107. <checkbox
  108. name="Show Directory"
  109. :value="landing.show_directory"
  110. description="Show the account directory on the landing page for guest users."
  111. @change="handleChange($event, 'landing', 'show_directory')"
  112. />
  113. </div>
  114. <div class="col-12 col-md-6">
  115. <checkbox
  116. name="Show Explore Feed"
  117. :value="landing.show_explore"
  118. description="Show the explore feed of popular posts on the landing page for guest users."
  119. @change="handleChange($event, 'landing', 'show_explore')"
  120. />
  121. </div>
  122. </div>
  123. </div>
  124. <div v-else-if="tabIndex === 'branding'" class="tab-pane fade show active" role="tabpanel">
  125. <tab-header title="Branding" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('branding')" />
  126. <div class="row">
  127. <div class="col-12 col-md-8">
  128. <div class="card shadow-none border card-body" style="padding: 1.1rem 1.6rem">
  129. <div class="form-group mb-1">
  130. <label for="form-summary" class="font-weight-bold">Server Name</label>
  131. <input
  132. class="form-control form-control-muted"
  133. placeholder="Pixelfed"
  134. v-model="branding.name" />
  135. </div>
  136. <p class="help-text small text-muted mb-0">
  137. The instance name used in titles, metadata and apis.
  138. </p>
  139. </div>
  140. </div>
  141. <div class="col-12 col-md-8">
  142. <div class="card shadow-none border card-body">
  143. <div class="form-group mb-1">
  144. <label for="form-summary" class="font-weight-bold">Short Description</label>
  145. <textarea
  146. class="form-control form-control-muted"
  147. placeholder="Pixelfed"
  148. rows="4"
  149. v-model="branding.short_description"></textarea>
  150. </div>
  151. <p class="help-text small text-muted mb-0">
  152. Short description of instance used on various pages and apis.
  153. </p>
  154. </div>
  155. <div class="card shadow-none border card-body">
  156. <div class="form-group mb-1">
  157. <label for="form-summary" class="font-weight-bold">Long Description</label>
  158. <textarea
  159. class="form-control form-control-muted"
  160. placeholder="Pixelfed"
  161. rows="8"
  162. v-model="branding.long_description"></textarea>
  163. </div>
  164. <p class="help-text small text-muted mb-0">
  165. Longer description of instance used on about page.
  166. </p>
  167. </div>
  168. </div>
  169. </div>
  170. </div>
  171. <div v-else-if="tabIndex === 'media'" class="tab-pane fade show active" role="tabpanel">
  172. <tab-header title="Media" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('media')" />
  173. <div class="row">
  174. <div class="col-12 col-md-6">
  175. <div class="card shadow-none border card-body">
  176. <div class="form-group mb-1">
  177. <label class="font-weight-bold text-muted">Max Media Size</label>
  178. <div class="input-group mb-0">
  179. <input
  180. type="text"
  181. class="form-control"
  182. placeholder="15000"
  183. aria-label="Max media size"
  184. aria-describedby="maxMediaSize"
  185. v-model="media.max_photo_size">
  186. <div class="input-group-append">
  187. <span class="input-group-text" id="maxMediaSize">= {{ maxMediaSizeToMb }}</span>
  188. </div>
  189. </div>
  190. </div>
  191. <p class="help-text small text-muted mb-0">
  192. Maximum file upload size in KB
  193. </p>
  194. </div>
  195. <checkbox
  196. name="Optimize Images"
  197. :value="media.optimize_image"
  198. description="Enable to optimize images and generate thumbnails for local image media uploads."
  199. @change="handleChange($event, 'media', 'optimize_image')"
  200. />
  201. <checkbox
  202. name="Optimize Video"
  203. :value="media.optimize_video"
  204. description="Enable to generate video thumbnails for local video media uploads."
  205. @change="handleChange($event, 'media', 'optimize_video')"
  206. />
  207. <div class="card shadow-none border card-body">
  208. <div class="form-group mb-1">
  209. <label class="font-weight-bold text-muted">Media Types</label>
  210. <div class="list-group">
  211. <div v-for="(mediaType, key) in mediaTypes" class="list-group-item py-2">
  212. <div class="custom-control custom-checkbox">
  213. <input
  214. type="checkbox"
  215. class="custom-control-input"
  216. :name="key"
  217. :id="key"
  218. v-model="mediaTypes[key]">
  219. <label class="custom-control-label font-weight-bold" :for="key">{{ key }}</label>
  220. </div>
  221. </div>
  222. </div>
  223. </div>
  224. <p class="help-text small text-muted mb-0">
  225. Supported mime types for media uploads
  226. </p>
  227. </div>
  228. </div>
  229. <div class="col-12 col-md-6">
  230. <div class="card shadow-none border card-body">
  231. <div class="form-group mb-1">
  232. <label class="font-weight-bold text-muted">Photo Album Limit</label>
  233. <input
  234. type="number"
  235. min="1"
  236. max="20"
  237. class="form-control"
  238. name="max_album_length"
  239. v-model="media.max_album_length">
  240. </div>
  241. <p class="help-text small text-muted mb-0">
  242. The maximum number of photos or videos per album
  243. </p>
  244. </div>
  245. <transition name="fade">
  246. <div v-if="media.optimize_image" class="card shadow-none border card-body">
  247. <div class="form-group mb-1">
  248. <label class="font-weight-bold text-muted">Image Quality</label>
  249. <input
  250. type="number"
  251. min="20"
  252. max="100"
  253. class="form-control"
  254. name="image_quality"
  255. v-model="media.image_quality">
  256. </div>
  257. <p class="help-text small text-muted mb-0">
  258. Image optimization quality from 0-100%.
  259. </p>
  260. </div>
  261. </transition>
  262. </div>
  263. </div>
  264. </div>
  265. <div v-else-if="tabIndex === 'platform'" class="tab-pane fade show active" role="tabpanel">
  266. <tab-header title="Platform" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('platform')" />
  267. <div class="row">
  268. <div class="col-12 col-md-6">
  269. <checkbox
  270. name="Allow Profile Embeds"
  271. :value="platform.allow_profile_embeds"
  272. description="Allow anyone to embed public profiles on other websites."
  273. @change="handleChange($event, 'platform', 'allow_profile_embeds')"
  274. />
  275. <div class="card shadow-none border card-body">
  276. <div class="form-group mb-0">
  277. <div class="custom-control custom-checkbox">
  278. <input
  279. type="checkbox"
  280. name="allow_app_registrations"
  281. class="custom-control-input"
  282. id="platform1"
  283. :disabled="features.registration_status !== 'open'"
  284. v-model="platform.allow_app_registration">
  285. <label class="custom-control-label font-weight-bold" for="platform1">Allow App Registrations</label>
  286. </div>
  287. <p v-if="features.registration_status !== 'open'" class="mb-0 small text-muted">Requires open registration to be enabled.</p>
  288. <p v-else class="mb-0 small">Allow users to register via the official Pixelfed mobile application.</p>
  289. </div>
  290. </div>
  291. <checkbox
  292. name="Custom Emoji"
  293. :value="platform.custom_emoji_enabled"
  294. description="Enable federated custom emoji that is compatible with Mastodon, Pleroma and others."
  295. @change="handleChange($event, 'platform', 'custom_emoji_enabled')"
  296. />
  297. <template v-if="features.registration_status === 'open' && features.allow_app_registration">
  298. <div class="card shadow-none border card-body">
  299. <div class="form-group mb-1">
  300. <label class="font-weight-bold text-muted">app_registration_rate_limit_attempts</label>
  301. <input
  302. type="number"
  303. class="form-control"
  304. name="app_registration_rate_limit_attempts"
  305. v-model="platform.app_registration_rate_limit_attempts">
  306. </div>
  307. <p class="help-text small text-muted mb-0">
  308. app_registration_rate_limit_attempts.
  309. </p>
  310. </div>
  311. <div class="card shadow-none border card-body">
  312. <div class="form-group mb-1">
  313. <label class="font-weight-bold text-muted">app_registration_rate_limit_decay</label>
  314. <input
  315. type="number"
  316. class="form-control"
  317. name="app_registration_rate_limit_decay"
  318. v-model="platform.app_registration_rate_limit_decay">
  319. </div>
  320. <p class="help-text small text-muted mb-0">
  321. app_registration_rate_limit_decay
  322. </p>
  323. </div>
  324. </template>
  325. </div>
  326. <div class="col-12 col-md-6">
  327. <checkbox
  328. name="Allow Post Embeds"
  329. :value="platform.allow_post_embeds"
  330. description="Allow anyone to embed public posts on other websites."
  331. @change="handleChange($event, 'platform', 'allow_post_embeds')"
  332. />
  333. <div class="card shadow-none border card-body">
  334. <div class="form-group mb-1">
  335. <div class="custom-control custom-checkbox">
  336. <input
  337. type="checkbox"
  338. name="hcaps"
  339. class="custom-control-input"
  340. id="hcp"
  341. v-model="platform.captcha_enabled">
  342. <label class="custom-control-label font-weight-bold" for="hcp">Enable hCaptcha</label>
  343. </div>
  344. </div>
  345. <template v-if="platform.captcha_enabled">
  346. <hr class="my-2">
  347. <div class="row">
  348. <div class="col-12 col-md-6">
  349. <div class="form-group my-1">
  350. <label class="text-muted small">hCaptcha Secret</label>
  351. <input
  352. type="text"
  353. class="form-control"
  354. name="captcha_secret"
  355. v-model="platform.captcha_secret">
  356. </div>
  357. </div>
  358. <div class="col-12 col-md-6">
  359. <div class="form-group my-1">
  360. <label class="text-muted small">hCaptcha Sitekey</label>
  361. <input
  362. type="text"
  363. class="form-control"
  364. name="captcha_sitekey"
  365. v-model="platform.captcha_sitekey">
  366. </div>
  367. </div>
  368. </div>
  369. <hr class="mt-2 mb-4">
  370. <div class="row">
  371. <div class="col-12 col-lg-6">
  372. <div class="custom-control custom-checkbox">
  373. <input
  374. type="checkbox"
  375. name="captcha_on_login"
  376. class="custom-control-input"
  377. id="captcha_on_login"
  378. v-model="platform.captcha_on_login">
  379. <label class="custom-control-label font-weight-bold" for="captcha_on_login">Login Captcha</label>
  380. </div>
  381. </div>
  382. <div class="col-12 col-lg-6">
  383. <div class="custom-control custom-checkbox">
  384. <input
  385. type="checkbox"
  386. name="captcha_on_register"
  387. class="custom-control-input"
  388. id="captcha_on_register"
  389. v-model="platform.captcha_on_register">
  390. <label class="custom-control-label font-weight-bold" for="captcha_on_register">Register Captcha</label>
  391. </div>
  392. </div>
  393. </div>
  394. <hr class="mt-4 mb-2">
  395. </template>
  396. <p class="help-text small text-muted mb-0">
  397. Enable hCaptcha on login and register pages
  398. </p>
  399. </div>
  400. <template v-if="features.registration_status === 'open' && features.allow_app_registration">
  401. <div class="card shadow-none border card-body">
  402. <div class="form-group mb-1">
  403. <label class="font-weight-bold text-muted">app_registration_confirm_rate_limit_attempts</label>
  404. <input
  405. type="number"
  406. class="form-control"
  407. name="app_registration_confirm_rate_limit_attempts"
  408. v-model="platform.app_registration_confirm_rate_limit_attempts">
  409. </div>
  410. <p class="help-text small text-muted mb-0">
  411. app_registration_confirm_rate_limit_attempts.
  412. </p>
  413. </div>
  414. <div class="card shadow-none border card-body">
  415. <div class="form-group mb-1">
  416. <label class="font-weight-bold text-muted">app_registration_confirm_rate_limit_decay</label>
  417. <input
  418. type="number"
  419. class="form-control"
  420. name="app_registration_confirm_rate_limit_decay"
  421. v-model="platform.app_registration_confirm_rate_limit_decay">
  422. </div>
  423. <p class="help-text small text-muted mb-0">
  424. app_registration_confirm_rate_limit_decay.
  425. </p>
  426. </div>
  427. </template>
  428. </div>
  429. </div>
  430. </div>
  431. <div v-else-if="tabIndex === 'posts'" class="tab-pane fade show active" role="tabpanel">
  432. <tab-header title="Posts" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('posts')" />
  433. <div class="row">
  434. <div class="col-12 col-md-6">
  435. <div class="card shadow-none border card-body">
  436. <div class="form-group mb-1">
  437. <label class="font-weight-bold text-muted">Max Caption Length</label>
  438. <input
  439. type="number"
  440. min="1"
  441. max="10000"
  442. class="form-control"
  443. name="max_caption_limit"
  444. v-model="posts.max_caption_length">
  445. </div>
  446. <p class="help-text small text-muted mb-0">
  447. The maximum character count of post captions. We recommend a limit between 500-2000.
  448. </p>
  449. </div>
  450. </div>
  451. <div class="col-12 col-md-6">
  452. <div class="card shadow-none border card-body">
  453. <div class="form-group mb-1">
  454. <label class="font-weight-bold text-muted">Max Alttext Length</label>
  455. <input
  456. type="number"
  457. min="1"
  458. max="10000"
  459. class="form-control"
  460. name="max_altext_length"
  461. v-model="posts.max_altext_length">
  462. </div>
  463. <p class="help-text small text-muted mb-0">
  464. The maximum character count of post media alttext captions. We recommend a limit between 2000-10000.
  465. </p>
  466. </div>
  467. </div>
  468. </div>
  469. </div>
  470. <div v-else-if="tabIndex === 'rules'" class="tab-pane fade show active" role="tabpanel">
  471. <tab-header title="Rules" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('rules')" />
  472. <div class="row">
  473. <div class="col-12 mb-3">
  474. <div v-if="hasDuplicateRulesComputed" class="alert alert-danger">
  475. <p class="font-weight-bold mb-0">Duplicate rules detected, you should fix this!</p>
  476. </div>
  477. <div class="position-relative">
  478. <div class="card shadow-none border">
  479. <div class="card-header py-2 bg-primary text-white font-weight-bold text-center">Active Rules</div>
  480. <div class="list-group list-group-flush">
  481. <div
  482. v-for="(rule, idx) in rulesComputed"
  483. class="list-group-item">
  484. <div class="d-flex justify-content-between align-items-start">
  485. <div class="d-flex gap-1 align-items-start">
  486. <div class="rule-badge">
  487. <div class="rule-badge-inner">{{ idx + 1 }}</div>
  488. </div>
  489. <admin-read-more
  490. :key="rule"
  491. class="text-dark rule-text"
  492. :content="rule"
  493. :maxLength="140"
  494. :initialLimit="30"
  495. fontSize="13" />
  496. </div>
  497. <button
  498. class="btn btn-link btn-sm"
  499. :disabled="isDeletingRule"
  500. @click.prevent="handleDeleteRule(rule, idx, $event)">
  501. <i class="fas fa-trash-alt text-danger"></i>
  502. </button>
  503. </div>
  504. </div>
  505. <div v-if="!rules || !rules.length" class="list-group-item">
  506. <p class="text-center mb-0">No rules set!</p>
  507. </div>
  508. </div>
  509. </div>
  510. <div v-if="!showAllRules && rules.length > 2" class="d-flex justify-content-center" style="position:absolute;width: 100%;padding-top: 10rem;bottom:0;background: linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255, 1));">
  511. <button class="btn btn-dark font-weight-bold rounded-pill btn-block" @click.prevent="showAllRules = true">Show all rules</button>
  512. </div>
  513. </div>
  514. </div>
  515. <div class="col-12 col-md-6">
  516. <div class="card shadow-none border card-body">
  517. <div class="form-group mb-1">
  518. <label class="font-weight-bold text-muted">Add New Rule</label>
  519. <textarea
  520. type="text"
  521. class="form-control"
  522. name="new_rule"
  523. rows="5"
  524. minlength="5"
  525. maxlength="1000"
  526. placeholder="Add your new rule here..."
  527. :disabled="isSubmittingNewRule || isDeletingRule"
  528. v-model="newRule"></textarea>
  529. </div>
  530. <div class="d-flex justify-content-between align-items-center">
  531. <p class="help-text small text-muted mb-0">
  532. Add a new rule
  533. </p>
  534. <p class="help-text small text-muted mb-0">
  535. {{ newRule && newRule.length ? newRule.length : 0 }}/1000
  536. </p>
  537. </div>
  538. <hr class="my-2">
  539. <p class="mb-0">
  540. <button
  541. class="btn btn-primary btn-sm btn-block font-weight-bold rounded-pill"
  542. :disabled="!newRule || !newRule.length || isSubmittingNewRule || isDeletingRule"
  543. @click.prevent="handleAddRule">Add Rule</button>
  544. </p>
  545. </div>
  546. <button v-if="rules && rules.length" class="btn btn-outline-danger rounded-pill btn-block btn-sm" @click.prevent="handleDeleteAllRules">Delete all rules</button>
  547. </div>
  548. <div v-if="suggestedRulesComputed && suggestedRulesComputed.length" class="col-12 col-md-6">
  549. <div class="border-bottom pb-2 mb-3 d-flex justify-content-between align-items-center">
  550. <p class="font-weight-bold mb-0">Suggested Rules</p>
  551. <a v-if="!rules.length" class="font-weight-bold small" href="#" @click.prevent="importAllDefaultRules">Import All</a>
  552. </div>
  553. <div class="list-group">
  554. <a
  555. v-for="rule in suggestedRulesComputed"
  556. class="list-group-item small"
  557. href="#"
  558. @click.prevent="addSuggestedRule(rule, $event)">{{ rule }}</a>
  559. </div>
  560. </div>
  561. </div>
  562. </div>
  563. <div v-else-if="tabIndex === 'storage'" class="tab-pane fade show active" role="tabpanel">
  564. <tab-header title="Storage" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('storage')" />
  565. <div class="row">
  566. <div class="col-12 col-md-6">
  567. <div class="card shadow-none border card-body" style="padding: 1.1rem 1.6rem">
  568. <div class="form-group mb-0">
  569. <label for="form-summary" class="font-weight-bold">Primary Storage Disk</label>
  570. <select v-model="storage.primary_disk" class="form-control form-control-muted">
  571. <option value="local" >Local</option>
  572. <option value="cloud">Cloud/S3</option>
  573. </select>
  574. </div>
  575. <p class="help-text small text-muted mt-2 mb-0">
  576. The storage disk where avatars and media uploads are stored.
  577. </p>
  578. </div>
  579. </div>
  580. <div class="col-12 col-md-6">
  581. <div class="card border">
  582. <div class="card-header bg-gradient-primary">
  583. <p class="text-center mb-0 text-white font-weight-bold">Cloud Disk Config</p>
  584. </div>
  585. <div v-if="!showDiskConfig" class="card-body">
  586. <p class="text-center mb-0">
  587. <a
  588. class="btn btn-primary bg-gradient-primary shadow-lg rounded-pill"
  589. href="#"
  590. @click.prevent="showDiskConfig = true">
  591. View/Edit
  592. </a>
  593. </p>
  594. </div>
  595. <div v-else class="card-body">
  596. <div class="form-group mb-4 d-flex align-items-center gap-1">
  597. <label for="form-summary" class="font-weight-bold mb-0">Disk</label>
  598. <select v-model="storage.disk_config.driver" class="form-control form-control-muted mb-0">
  599. <option value="s3" >S3</option>
  600. <option value="spaces">DigitalOcean Spaces</option>
  601. </select>
  602. </div>
  603. <form-input
  604. name="Key"
  605. :value="storage.disk_config.key"
  606. description=""
  607. :isCard="false"
  608. :isInline="true"
  609. @change="handleSubChange($event, 'storage', 'disk_config', 'key')"
  610. />
  611. <form-input
  612. name="Secret"
  613. :value="storage.disk_config.secret"
  614. description=""
  615. :isCard="false"
  616. :isInline="true"
  617. @change="handleSubChange($event, 'storage', 'disk_config', 'secret')"
  618. />
  619. <form-input
  620. name="Region"
  621. :value="storage.disk_config.region"
  622. description=""
  623. :isCard="false"
  624. :isInline="true"
  625. @change="handleSubChange($event, 'storage', 'disk_config', 'region')"
  626. />
  627. <form-input
  628. name="Bucket"
  629. :value="storage.disk_config.bucket"
  630. description=""
  631. :isCard="false"
  632. :isInline="true"
  633. @change="handleSubChange($event, 'storage', 'disk_config', 'bucket')"
  634. />
  635. <form-input
  636. name="Endpoint"
  637. :value="storage.disk_config.endpoint"
  638. description=""
  639. :isCard="false"
  640. :isInline="true"
  641. @change="handleSubChange($event, 'storage', 'disk_config', 'endpoint')"
  642. />
  643. <form-input
  644. name="Visibility"
  645. :value="storage.disk_config.visibility"
  646. description=""
  647. :isCard="false"
  648. :isInline="true"
  649. :isDisabled="true"
  650. @change="handleSubChange($event, 'storage', 'disk_config', 'visibility')"
  651. />
  652. <form-input
  653. name="Url"
  654. :value="storage.disk_config.url"
  655. description=""
  656. :isCard="false"
  657. :isInline="true"
  658. @change="handleSubChange($event, 'storage', 'disk_config', 'url')"
  659. />
  660. </div>
  661. </div>
  662. </div>
  663. </div>
  664. </div>
  665. <div v-else-if="tabIndex === 'users'" class="tab-pane fade show active" role="tabpanel">
  666. <tab-header title="Users" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('users')" />
  667. <div class="row">
  668. <div class="col-12 col-md-6">
  669. <checkbox
  670. name="Require Email Verifications"
  671. :value="users.require_email_verification"
  672. description="Require users to verify their email address is valid before they can use the account."
  673. @change="handleChange($event, 'users', 'require_email_verification')"
  674. />
  675. <form-input
  676. name="Max User Blocks"
  677. :value="users.max_user_blocks.toString()"
  678. description="The max number of account blocks per user."
  679. @change="handleChange($event, 'users', 'max_user_blocks')"
  680. />
  681. <form-input
  682. name="Max User Mutes"
  683. :value="users.max_user_mutes.toString()"
  684. description="The max number of account mutes per user."
  685. @change="handleChange($event, 'users', 'max_user_mutes')"
  686. />
  687. <form-input
  688. name="Max User Domain Blocks"
  689. :value="users.max_domain_blocks.toString()"
  690. description="The max number of domain blocks per user."
  691. @change="handleChange($event, 'users', 'max_domain_blocks')"
  692. />
  693. </div>
  694. <div class="col-12 col-md-6">
  695. <div class="card shadow-none border card-body">
  696. <div class="form-group mb-0">
  697. <div class="custom-control custom-checkbox">
  698. <input type="checkbox" name="enforce_account_limit" class="custom-control-input" id="users2" v-model="users.enforce_account_limit">
  699. <label class="custom-control-label font-weight-bold" for="users2">Enforce Account Limit</label>
  700. </div>
  701. <p class="mb-0 small">Set a storage limit per user account for all uploaded media (photo + video).</p>
  702. </div>
  703. <transition name="fade">
  704. <div v-if="users.enforce_account_limit">
  705. <hr class="my-2">
  706. <div class="form-group mb-1">
  707. <div class="input-group mb-0">
  708. <input
  709. type="text"
  710. class="form-control"
  711. placeholder="15000"
  712. aria-label="Max account size"
  713. aria-describedby="maxMediaSize"
  714. v-model="users.max_account_size">
  715. <div class="input-group-append">
  716. <span class="input-group-text">= {{maxAccountSizeToMb }}</span>
  717. </div>
  718. </div>
  719. </div>
  720. <p class="help-text small text-muted mb-0">
  721. Maximum file storage limit per user account.
  722. </p>
  723. </div>
  724. </transition>
  725. </div>
  726. <div class="card shadow-none border">
  727. <div class="card-body">
  728. <div class="form-group mb-0">
  729. <div class="custom-control custom-checkbox">
  730. <input type="checkbox" name="admin_autofollow" class="custom-control-input" id="users4" v-model="users.admin_autofollow">
  731. <label class="custom-control-label font-weight-bold" for="users4">Autofollow Accounts</label>
  732. </div>
  733. <p class="mb-0 small">Force new accounts to follow accounts you specify below</p>
  734. </div>
  735. </div>
  736. <transition name="fade">
  737. <div v-if="users.admin_autofollow" class="list-group list-group-flush">
  738. <div v-if="users.admin_autofollow_accounts?.length">
  739. <div v-for="user in users.admin_autofollow_accounts" class="list-group-item">
  740. <div class="d-flex justify-content-between align-items-center">
  741. <p class="font-weight-bold mb-0">&commat;{{ user }}</p>
  742. <button class="btn btn-link p-0" @click.prevent="removeAutofollow(user, $event)"><i class="fas fa-trash-alt text-danger"></i></button>
  743. </div>
  744. </div>
  745. </div>
  746. <div v-else class="list-group-item">
  747. <p class="text-center mb-0">No autofollow accounts active.</p>
  748. </div>
  749. </div>
  750. </transition>
  751. <transition name="fade">
  752. <div v-if="users.admin_autofollow && (users.admin_autofollow_accounts && users.admin_autofollow_accounts.length < 5)" class="card-footer">
  753. <button
  754. class="btn btn-primary btn-block rounded-pill"
  755. @click.prevent="addAutofollow">Add Autofollow Account</button>
  756. </div>
  757. </transition>
  758. </div>
  759. </div>
  760. </div>
  761. </div>
  762. </div>
  763. </div>
  764. </div>
  765. </div>
  766. </div>
  767. </div>
  768. </div>
  769. <div v-else>
  770. <div class="container my-5 py-5 text-center">
  771. <div class="spinner-border text-primary" role="status">
  772. <span class="sr-only">Loading...</span>
  773. </div>
  774. </div>
  775. </div>
  776. </template>
  777. <script type="text/javascript">
  778. import AdminReadMore from "./partial/AdminReadMore.vue";
  779. import AdminSettingsTabHeader from "./partial/AdminSettingsTabHeader.vue";
  780. import Checkbox from "./partial/AdminSettingsCheckbox.vue";
  781. import FormInput from "./partial/AdminSettingsInput.vue";
  782. export default {
  783. components: {
  784. "admin-read-more": AdminReadMore,
  785. "tab-header": AdminSettingsTabHeader,
  786. "checkbox": Checkbox,
  787. "form-input": FormInput
  788. },
  789. data() {
  790. return {
  791. loaded: false,
  792. initialData: {},
  793. tabIndex: 1,
  794. tabbies: [
  795. 'landing',
  796. 'branding',
  797. 'media',
  798. 'posts',
  799. 'platform',
  800. 'rules',
  801. 'users',
  802. 'storage'
  803. ],
  804. tabs: [
  805. { id: 1, title: "Overview", icon: "far fa-home" },
  806. // { id: 2, title: "Status", icon: "far fa-asterisk" },
  807. { id: 'landing', title: "Landing", icon: "far fa-info-circle" },
  808. { id: 'branding', title: "Branding", icon: "far fa-user-crown" },
  809. { id: 'media', title: "Media", icon: "far fa-image" },
  810. { id: 'platform', title: "Platform", icon: "far fa-database" },
  811. { id: 'posts', title: "Posts", icon: "far fa-heart" },
  812. { id: 'rules', title: "Rules", icon: "far fa-eye-slash" },
  813. { id: 'storage', title: "Storage", icon: "far fa-hdd" },
  814. { id: 'users', title: "Users", icon: "far fa-users" },
  815. ],
  816. isSubmitting: false,
  817. isSubmittingTimeout: false,
  818. isSubmittingTimeoutHandler: undefined,
  819. features: [],
  820. landing: {
  821. current_admin: 0,
  822. },
  823. branding: [],
  824. media: [],
  825. mediaTypes: {
  826. jpeg: false,
  827. png: false,
  828. gif: false,
  829. webp: false,
  830. avif: false,
  831. heic: false,
  832. mp4: false,
  833. mov: false,
  834. },
  835. rules: [],
  836. users: [],
  837. posts: [],
  838. platform: [],
  839. storage: [],
  840. newRule: undefined,
  841. isSubmittingNewRule: false,
  842. isDeletingRule: false,
  843. suggestedRules: [],
  844. hasDuplicateRules: false,
  845. showAllRules: false,
  846. showDiskConfig: false,
  847. }
  848. },
  849. computed: {
  850. maxMediaSizeToMb: {
  851. get() {
  852. if(!this.media || !this.media.max_photo_size) {
  853. return '0.00 MB';
  854. }
  855. return (this.media.max_photo_size / 1000).toFixed(2) + ' MB';
  856. }
  857. },
  858. maxAccountSizeToMb: {
  859. get() {
  860. if(!this.users || !this.users.max_account_size) {
  861. return '0.00 MB';
  862. }
  863. const mb = (this.users.max_account_size / 1000);
  864. if(mb > 1000000) {
  865. return (mb / 1000000).toFixed(1) + 'TB';
  866. }
  867. if(mb > 1000) {
  868. return (mb / 1000).toFixed(2) + 'GB';
  869. }
  870. return (this.users.max_account_size / 1000).toFixed(2) + ' MB';
  871. }
  872. },
  873. rulesComputed: {
  874. get() {
  875. if(!this.rules || !this.rules.length) {
  876. return [];
  877. }
  878. if(this.rules.length > 2) {
  879. if(!this.showAllRules) {
  880. return this.rules.slice(0, 2);
  881. }
  882. }
  883. return this.rules;
  884. }
  885. },
  886. suggestedRulesComputed: {
  887. get() {
  888. if(!this.rules || !this.rules.length) {
  889. return this.suggestedRules;
  890. }
  891. return this.suggestedRules.filter(rule => {
  892. if(this.rules.includes(rule)) {
  893. return false;
  894. }
  895. return true;
  896. });
  897. }
  898. },
  899. hasDuplicateRulesComputed: {
  900. get() {
  901. if(!this.rules || !this.rules.length) {
  902. return false;
  903. }
  904. const array = this.rules;
  905. const duplicates = array.filter((item, index) => array.indexOf(item) !== index);
  906. return duplicates.length;
  907. }
  908. },
  909. activeMediaTypes: {
  910. get() {
  911. let res = '';
  912. if(this.mediaTypes.jpeg) {
  913. res += 'image/jpeg,'
  914. }
  915. if(this.mediaTypes.png) {
  916. res += 'image/png,'
  917. }
  918. if(this.mediaTypes.gif) {
  919. res += 'image/gif,'
  920. }
  921. if(this.mediaTypes.webp) {
  922. res += 'image/webp,'
  923. }
  924. if(this.mediaTypes.mp4) {
  925. res += 'video/mp4'
  926. }
  927. if(res.endsWith(',')) {
  928. res = res.slice(0, -1);
  929. }
  930. return res;
  931. }
  932. }
  933. },
  934. mounted() {
  935. this.fetchInitialData();
  936. const params = new URL(window.location.href);
  937. if(params.searchParams.has('t')) {
  938. const tab = params.searchParams.get('t');
  939. if(this.tabbies.includes(tab)) {
  940. this.tabIndex = tab;
  941. } else {
  942. window.history.pushState(null, null, '/i/admin/settings')
  943. }
  944. }
  945. },
  946. methods: {
  947. toggleTab(idx) {
  948. clearTimeout(this.isSubmittingTimeoutHandler)
  949. this.isSubmittingTimeout = false;
  950. this.tabIndex = idx;
  951. this.showAllRules = false;
  952. if(this.tabbies.includes(idx)) {
  953. window.history.pushState(null, null, '/i/admin/settings?t=' + idx);
  954. } else {
  955. window.history.pushState(null, null, '/i/admin/settings');
  956. }
  957. },
  958. fetchInitialData() {
  959. axios.get('/i/admin/api/settings/fetch')
  960. .then(res => {
  961. this.initialData = res.data;
  962. this.features = res.data.features;
  963. this.landing = res.data.landing;
  964. this.branding = res.data.branding;
  965. this.media = res.data.media;
  966. this.setMediaTypes();
  967. this.rules = res.data.rules;
  968. this.users = res.data.users;
  969. this.suggestedRules = res.data['suggested_rules'];
  970. this.posts = res.data.posts;
  971. this.platform = res.data.platform;
  972. this.storage = res.data.storage;
  973. })
  974. .then(() => {
  975. this.loaded = true;
  976. })
  977. },
  978. setMediaTypes() {
  979. const types = this.media.media_types.split(',');
  980. if(types && types.length) {
  981. types.forEach((type) => {
  982. let mime = type.split('/')[1];
  983. if(['jpeg', 'png', 'gif', 'webp', 'mp4'].includes(mime)) {
  984. this.mediaTypes[mime] = true;
  985. }
  986. })
  987. }
  988. },
  989. formatCount(c) {
  990. return window.App.util.format.count(c);
  991. },
  992. formatDateTime(ts) {
  993. let date = new Date(ts);
  994. return new Intl.DateTimeFormat('en-US', {dateStyle: 'medium', timeStyle: 'short'}).format(date);
  995. },
  996. formatDate(ts) {
  997. let date = new Date(ts);
  998. return new Intl.DateTimeFormat('en-US', {month: 'short', year: 'numeric'}).format(date);
  999. },
  1000. formatTimestamp(ts) {
  1001. return window.App.util.format.timeAgo(ts);
  1002. },
  1003. handleSave(type) {
  1004. this.isSubmitting = true;
  1005. switch(type) {
  1006. case 'overview':
  1007. return this.saveHome();
  1008. break;
  1009. case 'landing':
  1010. return this.saveLanding();
  1011. break;
  1012. case 'branding':
  1013. return this.saveBranding();
  1014. break;
  1015. case 'posts':
  1016. return this.savePosts();
  1017. break;
  1018. case 'media':
  1019. return this.saveMedia();
  1020. break;
  1021. case 'platform':
  1022. return this.savePlatform();
  1023. break;
  1024. case 'users':
  1025. return this.saveUsers();
  1026. break;
  1027. case 'storage':
  1028. return this.saveStorage();
  1029. break;
  1030. }
  1031. },
  1032. handleAddRule($event) {
  1033. $event.currentTarget?.blur();
  1034. this.isSubmittingNewRule = true;
  1035. axios.post('/i/admin/api/settings/rules/add', {
  1036. rule: this.newRule
  1037. }).then(res => {
  1038. this.rules.push(this.newRule);
  1039. this.newRule = undefined;
  1040. this.isSubmittingNewRule = false;
  1041. this.showAllRules = true;
  1042. })
  1043. .catch(err => {
  1044. if(err.response.data && err.response.data?.message) {
  1045. swal('Error', err.response.data.message, 'error');
  1046. }
  1047. this.isSubmittingNewRule = false;
  1048. })
  1049. },
  1050. addSuggestedRule(rule, $event) {
  1051. $event.currentTarget?.blur();
  1052. this.newRule = rule;
  1053. },
  1054. importAllDefaultRules($event) {
  1055. $event.currentTarget?.blur();
  1056. this.isSubmittingNewRule = true;
  1057. this.showAllRules = true;
  1058. for (var i = this.suggestedRules.length - 1; i >= 0; i--) {
  1059. const rule = this.suggestedRules[i]
  1060. setTimeout(() => {
  1061. axios.post('/i/admin/api/settings/rules/add', {
  1062. rule: rule
  1063. }).then(res => {
  1064. this.rules.push(rule);
  1065. })
  1066. }, (i * 300))
  1067. }
  1068. this.isSubmittingNewRule = false;
  1069. },
  1070. handleDeleteRule(rule, idx, $event) {
  1071. $event.currentTarget?.blur();
  1072. this.isDeletingRule = true;
  1073. axios.post('/i/admin/api/settings/rules/delete', {
  1074. rule: rule,
  1075. }).then(res => {
  1076. this.isDeletingRule = false;
  1077. this.rules = res.data;
  1078. })
  1079. .catch(err => {
  1080. })
  1081. },
  1082. handleDeleteAllRules($event) {
  1083. $event.currentTarget?.blur();
  1084. this.isDeletingRule = true;
  1085. swal({
  1086. title: 'Confirm',
  1087. text: 'Are you sure you want to delete all rules?',
  1088. buttons: true,
  1089. dangerMode: true,
  1090. }).then(res => {
  1091. if(res === true) {
  1092. axios.post('/i/admin/api/settings/rules/delete/all')
  1093. .then(res => {
  1094. this.isDeletingRule = false;
  1095. this.rules = []
  1096. })
  1097. .catch(err => {
  1098. })
  1099. } else {
  1100. this.isDeletingRule = false;
  1101. }
  1102. })
  1103. },
  1104. removeAutofollow(username, $event) {
  1105. $event.currentTarget?.blur();
  1106. axios.post('/i/admin/api/settings/autofollow/delete', {
  1107. username: username
  1108. }).then(res => {
  1109. this.users.admin_autofollow_accounts = res.data.accounts;
  1110. }).catch(err => {
  1111. swal("Oops!", "An error occurred, please try again later!", "error");
  1112. });
  1113. },
  1114. addAutofollow($event) {
  1115. $event.currentTarget?.blur();
  1116. swal({
  1117. text: 'Enter account username',
  1118. content: "input",
  1119. button: {
  1120. text: "Add Autofollow",
  1121. closeModal: false,
  1122. },
  1123. }).then(username => {
  1124. if (!username) throw null;
  1125. axios.post('/i/admin/api/settings/autofollow/add', {
  1126. username: username
  1127. })
  1128. .then(res => {
  1129. if(!res.data.accounts.map(acc => acc.toLowerCase()).includes(username.toLowerCase())) {
  1130. swal("Oops!", "The account you attempted to add does not exist or cannot be added!", "error");
  1131. }
  1132. this.users.admin_autofollow_accounts = res.data.accounts;
  1133. swal.stopLoading();
  1134. swal.close();
  1135. })
  1136. .catch(err => {
  1137. if(err.response.data && err.response.data.message) {
  1138. swal('Error', err.response.data.message, 'error');
  1139. } else {
  1140. swal("Oops!", "The account you attempted to add does not exist or cannot be added!", "error");
  1141. }
  1142. swal.stopLoading();
  1143. swal.close();
  1144. });
  1145. })
  1146. },
  1147. saveHome() {
  1148. axios.post('/i/admin/api/settings/update/home', {
  1149. registration_status: this.features.registration_status,
  1150. cloud_storage: this.features.cloud_storage,
  1151. activitypub_enabled: this.features.activitypub_enabled,
  1152. account_migration: this.features.account_migration,
  1153. mobile_apis: this.features.mobile_apis,
  1154. stories: this.features.stories,
  1155. instagram_import: this.features.instagram_import,
  1156. autospam_enabled: this.features.autospam_enabled,
  1157. }).then(res => {
  1158. this.isSubmitting = false;
  1159. this.isSubmittingTimeout = true;
  1160. this.isSubmittingTimeoutHandler = setTimeout(() => {
  1161. this.isSubmittingTimeout = false;
  1162. }, 4000);
  1163. })
  1164. },
  1165. saveLanding() {
  1166. axios.post('/i/admin/api/settings/update/landing', {
  1167. current_admin: this.landing.current_admin,
  1168. show_directory: this.landing.show_directory,
  1169. show_explore: this.landing.show_explore
  1170. }).then(res => {
  1171. this.isSubmitting = false;
  1172. this.isSubmittingTimeout = true;
  1173. this.isSubmittingTimeoutHandler = setTimeout(() => {
  1174. this.isSubmittingTimeout = false;
  1175. }, 4000);
  1176. })
  1177. },
  1178. saveBranding() {
  1179. axios.post('/i/admin/api/settings/update/branding', {
  1180. name: this.branding.name,
  1181. short_description: this.branding.short_description,
  1182. long_description: this.branding.long_description
  1183. }).then(res => {
  1184. this.isSubmitting = false;
  1185. this.isSubmittingTimeout = true;
  1186. this.isSubmittingTimeoutHandler = setTimeout(() => {
  1187. this.isSubmittingTimeout = false;
  1188. }, 4000);
  1189. })
  1190. },
  1191. savePosts() {
  1192. axios.post('/i/admin/api/settings/update/posts', {
  1193. max_caption_length: this.posts.max_caption_length,
  1194. max_altext_length: this.posts.max_altext_length,
  1195. }).then(res => {
  1196. this.posts = res.data;
  1197. this.isSubmitting = false;
  1198. this.isSubmittingTimeout = true;
  1199. this.isSubmittingTimeoutHandler = setTimeout(() => {
  1200. this.isSubmittingTimeout = false;
  1201. }, 4000);
  1202. })
  1203. .catch(err => {
  1204. this.isSubmitting = false;
  1205. if(err.response.data && err.response.data.message) {
  1206. swal('Error', err.response.data.message, 'error');
  1207. } else {
  1208. swal('Oops!', 'An error occured', 'error');
  1209. }
  1210. })
  1211. },
  1212. saveMedia() {
  1213. axios.post('/i/admin/api/settings/update/media', {
  1214. image_quality: this.media.image_quality,
  1215. max_album_length: this.media.max_album_length,
  1216. max_photo_size: this.media.max_photo_size,
  1217. media_types: this.activeMediaTypes,
  1218. optimize_image: this.media.optimize_image,
  1219. optimize_video: this.media.optimize_video,
  1220. }).then(res => {
  1221. this.isSubmitting = false;
  1222. this.isSubmittingTimeout = true;
  1223. this.isSubmittingTimeoutHandler = setTimeout(() => {
  1224. this.isSubmittingTimeout = false;
  1225. }, 4000);
  1226. }).catch(err => {
  1227. this.isSubmitting = false;
  1228. if(err.response.data && err.response.data.message) {
  1229. swal('Error', err.response.data.message, 'error');
  1230. } else {
  1231. swal('Oops!', 'An error occured', 'error');
  1232. }
  1233. })
  1234. },
  1235. savePlatform() {
  1236. axios.post('/i/admin/api/settings/update/platform', {
  1237. allow_app_registration: this.platform.allow_app_registration,
  1238. app_registration_rate_limit_attempts: this.platform.app_registration_rate_limit_attempts,
  1239. app_registration_rate_limit_decay: this.platform.app_registration_rate_limit_decay,
  1240. app_registration_confirm_rate_limit_attempts: this.platform.app_registration_confirm_rate_limit_attempts,
  1241. app_registration_confirm_rate_limit_decay: this.platform.app_registration_confirm_rate_limit_decay,
  1242. allow_post_embeds: this.platform.allow_post_embeds,
  1243. allow_profile_embeds: this.platform.allow_profile_embeds,
  1244. captcha_enabled: this.platform.captcha_enabled,
  1245. captcha_secret: this.platform.captcha_secret,
  1246. captcha_sitekey: this.platform.captcha_sitekey,
  1247. captcha_on_login: this.platform.captcha_on_login,
  1248. captcha_on_register: this.platform.captcha_on_register,
  1249. custom_emoji_enabled: this.platform.custom_emoji_enabled,
  1250. }).then(res => {
  1251. this.platform = res.data;
  1252. this.isSubmitting = false;
  1253. this.isSubmittingTimeout = true;
  1254. this.isSubmittingTimeoutHandler = setTimeout(() => {
  1255. this.isSubmittingTimeout = false;
  1256. }, 4000);
  1257. })
  1258. .catch(err => {
  1259. this.isSubmitting = false;
  1260. if(err.response.data && err.response.data.message) {
  1261. swal('Error', err.response.data.message, 'error');
  1262. } else {
  1263. swal('Oops!', 'An error occured', 'error');
  1264. }
  1265. })
  1266. },
  1267. saveUsers() {
  1268. axios.post('/i/admin/api/settings/update/users', {
  1269. require_email_verification: this.users.require_email_verification,
  1270. enforce_account_limit: this.users.enforce_account_limit,
  1271. admin_autofollow: this.users.admin_autofollow,
  1272. admin_autofollow_accounts: this.users.admin_autofollow_accounts,
  1273. max_user_blocks: this.users.max_user_blocks,
  1274. max_user_mutes: this.users.max_user_mutes,
  1275. max_domain_blocks: this.users.max_domain_blocks,
  1276. }).then(res => {
  1277. this.isSubmitting = false;
  1278. this.isSubmittingTimeout = true;
  1279. this.isSubmittingTimeoutHandler = setTimeout(() => {
  1280. this.isSubmittingTimeout = false;
  1281. }, 4000);
  1282. }).catch(err => {
  1283. if(err.response.data.message) {
  1284. swal('Error', err.response.data.message, 'error');
  1285. } else {
  1286. swal('Error', 'An unexpected error occurred, please try again!', 'error');
  1287. }
  1288. this.isSubmitting = false;
  1289. })
  1290. },
  1291. saveStorage() {
  1292. let data = this.showDiskConfig ?
  1293. {
  1294. primary_disk: this.storage.primary_disk,
  1295. update_disk: true,
  1296. disk_config: this.storage.disk_config,
  1297. } : {
  1298. primary_disk: this.storage.primary_disk,
  1299. }
  1300. axios.post('/i/admin/api/settings/update/storage', data)
  1301. .then(res => {
  1302. this.features.cloud_storage = res.data.primary_disk === 'cloud';
  1303. this.isSubmitting = false;
  1304. this.isSubmittingTimeout = true;
  1305. this.isSubmittingTimeoutHandler = setTimeout(() => {
  1306. this.isSubmittingTimeout = false;
  1307. }, 4000);
  1308. }).catch(err => {
  1309. if(err.response.data.error) {
  1310. if(err.response.data.s3_vce) {
  1311. let el = document.createElement('div');
  1312. el.classList.add('text-left');
  1313. el.innerHTML = err.response.data.message;
  1314. let wrapper = document.createElement('div');
  1315. wrapper.appendChild(el);
  1316. swal({
  1317. title: 'Invalid S3 Credentials',
  1318. content: wrapper,
  1319. icon: 'error'
  1320. });
  1321. } else {
  1322. swal('Error', err.response.data.message, 'error');
  1323. }
  1324. }
  1325. this.isSubmitting = false;
  1326. })
  1327. },
  1328. handleChange($event, cat, type) {
  1329. switch(cat) {
  1330. case 'features':
  1331. this.features[type] = $event;
  1332. break;
  1333. case 'landing':
  1334. this.landing[type] = $event;
  1335. break;
  1336. case 'platform':
  1337. this.platform[type] = $event;
  1338. break;
  1339. case 'media':
  1340. this.media[type] = $event;
  1341. break;
  1342. case 'users':
  1343. this.users[type] = $event;
  1344. break;
  1345. case 'storage':
  1346. this.storage[type] = $event;
  1347. break;
  1348. }
  1349. console.log($event)
  1350. console.log(type)
  1351. },
  1352. handleSubChange($event, cat, type, sub) {
  1353. switch(cat) {
  1354. case 'features':
  1355. this.features[type][sub] = $event;
  1356. break;
  1357. case 'landing':
  1358. this.landing[type][sub] = $event;
  1359. break;
  1360. case 'platform':
  1361. this.platform[type][sub] = $event;
  1362. break;
  1363. case 'media':
  1364. this.media[type][sub] = $event;
  1365. break;
  1366. case 'users':
  1367. this.users[type][sub] = $event;
  1368. break;
  1369. case 'storage':
  1370. this.storage[type][sub] = $event;
  1371. break;
  1372. }
  1373. console.log($event)
  1374. console.log(type)
  1375. },
  1376. },
  1377. watch: {
  1378. }
  1379. }
  1380. </script>
  1381. <style lang="scss" scoped>
  1382. .rule-badge {
  1383. display: flex;
  1384. width: 34px;
  1385. height: 34px;
  1386. justify-content: center;
  1387. align-items: center;
  1388. background-color: #fff;
  1389. border-radius: 34px;
  1390. border: 2px solid var(--primary);
  1391. &-inner {
  1392. display: flex;
  1393. justify-content: center;
  1394. align-items: center;
  1395. width: 26px;
  1396. height: 26px;
  1397. border-radius: 26px;
  1398. background-color: var(--primary);
  1399. color: #fff;
  1400. font-weight: bold;
  1401. font-size: 13px;
  1402. }
  1403. }
  1404. .rule-text {
  1405. max-width: 90%;
  1406. margin-bottom: 0px;
  1407. font-size: 14px;
  1408. }
  1409. .gap-1 {
  1410. gap: 1rem;
  1411. }
  1412. </style>