AdminSettings.vue 87 KB

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