Source: server/auth/admin.js

  1. /**
  2. * Auth / Admin
  3. * @type {module:express-promise-router}
  4. */
  5. const Router = require("express-promise-router");
  6. const { contract, is } = require("contractis");
  7. const db = require("@saltcorn/data/db");
  8. const User = require("@saltcorn/data/models/user");
  9. const View = require("@saltcorn/data/models/view");
  10. const Field = require("@saltcorn/data/models/field");
  11. const Form = require("@saltcorn/data/models/form");
  12. const {
  13. mkTable,
  14. renderForm,
  15. link,
  16. post_btn,
  17. settingsDropdown,
  18. post_dropdown_item,
  19. } = require("@saltcorn/markup");
  20. const {
  21. isAdmin,
  22. setTenant,
  23. error_catcher,
  24. } = require("../routes/utils");
  25. const { send_reset_email } = require("./resetpw");
  26. const { getState } = require("@saltcorn/data/db/state");
  27. const {
  28. a,
  29. div,
  30. text,
  31. span,
  32. code,
  33. h5,
  34. i,
  35. p,
  36. } = require("@saltcorn/markup/tags");
  37. const Table = require("@saltcorn/data/models/table");
  38. const {
  39. send_users_page,
  40. config_fields_form,
  41. save_config_from_form,
  42. getBaseDomain,
  43. hostname_matches_baseurl,
  44. is_hsts_tld,
  45. } = require("../markup/admin");
  46. const { send_verification_email } = require("@saltcorn/data/models/email");
  47. const router = new Router();
  48. module.exports = router;
  49. const getUserFields = async () => {
  50. const userTable = await Table.findOne({ name: "users" });
  51. const userFields = (await userTable.getFields()).filter(
  52. (f) => !f.calculated && f.name !== "id"
  53. );
  54. const iterForm = async (cfgField) => {
  55. const signup_form_name = getState().getConfig(cfgField, "");
  56. if (signup_form_name) {
  57. const signup_form = await View.findOne({ name: signup_form_name });
  58. if (signup_form) {
  59. (signup_form.configuration.columns || []).forEach((f) => {
  60. const uf = userFields.find((uff) => uff.name === f.field_name);
  61. if (uf) {
  62. uf.fieldview = f.fieldview;
  63. uf.attributes = { ...f.configuration, ...uf.attributes };
  64. }
  65. });
  66. }
  67. }
  68. };
  69. await iterForm("signup_form");
  70. await iterForm("new_user_form");
  71. return userFields;
  72. };
  73. const userForm = contract(
  74. is.fun(
  75. [is.obj({}), is.maybe(is.class("User"))],
  76. is.promise(is.class("Form"))
  77. ),
  78. async (req, user) => {
  79. const roleField = new Field({
  80. label: req.__("Role"),
  81. name: "role_id",
  82. type: "Key",
  83. reftable_name: "roles",
  84. });
  85. const roles = (await User.get_roles()).filter((r) => r.role !== "public");
  86. roleField.options = roles.map((r) => ({ label: r.role, value: r.id }));
  87. const can_reset = getState().getConfig("smtp_host", "") !== "";
  88. const userFields = await getUserFields();
  89. const form = new Form({
  90. fields: [roleField, ...userFields],
  91. action: "/useradmin/save",
  92. submitLabel: user ? req.__("Save") : req.__("Create"),
  93. });
  94. if (!user) {
  95. form.fields.push(
  96. new Field({
  97. label: req.__("Set random password"),
  98. name: "rnd_password",
  99. type: "Bool",
  100. default: true,
  101. })
  102. );
  103. form.fields.push(
  104. new Field({
  105. label: req.__("Password"),
  106. name: "password",
  107. input_type: "password",
  108. showIf: { rnd_password: false },
  109. })
  110. );
  111. can_reset &&
  112. form.fields.push(
  113. new Field({
  114. label: req.__("Send password reset email"),
  115. name: "send_pwreset_email",
  116. type: "Bool",
  117. default: true,
  118. showIf: { rnd_password: true },
  119. })
  120. );
  121. }
  122. if (user) {
  123. form.hidden("id");
  124. form.values = user;
  125. delete form.values.password;
  126. } else {
  127. form.values.role_id = roles[roles.length - 1].id;
  128. }
  129. return form;
  130. }
  131. );
  132. /**
  133. * Dropdown for User Info in left menu
  134. * @param user
  135. * @param req
  136. * @param can_reset
  137. * @returns {string}
  138. */
  139. const user_dropdown = (user, req, can_reset) =>
  140. settingsDropdown(`dropdownMenuButton${user.id}`, [
  141. a(
  142. {
  143. class: "dropdown-item",
  144. href: `/useradmin/${user.id}`,
  145. },
  146. '<i class="fas fa-edit"></i>&nbsp;' + req.__("Edit")
  147. ),
  148. post_dropdown_item(
  149. `/useradmin/set-random-password/${user.id}`,
  150. '<i class="fas fa-random"></i>&nbsp;' + req.__("Set random password"),
  151. req
  152. ),
  153. can_reset &&
  154. post_dropdown_item(
  155. `/useradmin/reset-password/${user.id}`,
  156. '<i class="fas fa-envelope"></i>&nbsp;' +
  157. req.__("Send password reset email"),
  158. req
  159. ),
  160. can_reset &&
  161. !user.verified_on &&
  162. getState().getConfig("verification_view", "") &&
  163. post_dropdown_item(
  164. `/useradmin/send-verification/${user.id}`,
  165. '<i class="fas fa-envelope"></i>&nbsp;' +
  166. req.__("Send verification email"),
  167. req
  168. ),
  169. user.disabled &&
  170. post_dropdown_item(
  171. `/useradmin/enable/${user.id}`,
  172. '<i class="fas fa-play"></i>&nbsp;' + req.__("Enable"),
  173. req
  174. ),
  175. !user.disabled &&
  176. post_dropdown_item(
  177. `/useradmin/disable/${user.id}`,
  178. '<i class="fas fa-pause"></i>&nbsp;' + req.__("Disable"),
  179. req
  180. ),
  181. div({ class: "dropdown-divider" }),
  182. post_dropdown_item(
  183. `/useradmin/delete/${user.id}`,
  184. '<i class="far fa-trash-alt"></i>&nbsp;' + req.__("Delete"),
  185. req,
  186. true,
  187. user.email
  188. ),
  189. ]);
  190. router.get(
  191. "/",
  192. setTenant,
  193. isAdmin,
  194. error_catcher(async (req, res) => {
  195. const users = await User.find({}, { orderBy: "id" });
  196. const roles = await User.get_roles();
  197. var roleMap = {};
  198. roles.forEach((r) => {
  199. roleMap[r.id] = r.role;
  200. });
  201. const can_reset = getState().getConfig("smtp_host", "") !== "";
  202. send_users_page({
  203. res,
  204. req,
  205. active_sub: "Users",
  206. contents: {
  207. type: "card",
  208. title: req.__("Users"),
  209. contents: [
  210. mkTable(
  211. [
  212. { label: req.__("ID"), key: "id" },
  213. {
  214. label: req.__("Email"),
  215. key: (r) => link(`/useradmin/${r.id}`, r.email),
  216. },
  217. {
  218. label: "",
  219. key: (r) =>
  220. r.disabled
  221. ? span({ class: "badge badge-danger" }, "Disabled")
  222. : "",
  223. },
  224. {
  225. label: req.__("Verified"),
  226. key: (r) =>
  227. !!r.verified_on
  228. ? i({
  229. class: "fas fa-check-circle text-success",
  230. })
  231. : "",
  232. },
  233. { label: req.__("Role"), key: (r) => roleMap[r.role_id] },
  234. {
  235. label: "",
  236. key: (r) => user_dropdown(r, req, can_reset),
  237. },
  238. ],
  239. users,
  240. { hover: true }
  241. ),
  242. link(`/useradmin/new`, req.__("Create user")),
  243. ],
  244. },
  245. });
  246. })
  247. );
  248. router.get(
  249. "/new",
  250. setTenant,
  251. isAdmin,
  252. error_catcher(async (req, res) => {
  253. const form = await userForm(req);
  254. send_users_page({
  255. res,
  256. req,
  257. active_sub: "Users",
  258. sub2_page: "New",
  259. contents: {
  260. type: "card",
  261. title: req.__("New user"),
  262. contents: [renderForm(form, req.csrfToken())],
  263. },
  264. });
  265. })
  266. );
  267. const user_settings_form = (req) =>
  268. config_fields_form({
  269. req,
  270. field_names: [
  271. "allow_signup",
  272. "login_menu",
  273. "new_user_form",
  274. "login_form",
  275. "signup_form",
  276. "user_settings_form",
  277. "verification_view",
  278. "elevate_verified",
  279. "min_role_upload",
  280. "timeout",
  281. "email_mask",
  282. "allow_forgot",
  283. "cookie_sessions",
  284. "custom_http_headers",
  285. ],
  286. action: "/useradmin/settings",
  287. submitLabel: req.__("Save"),
  288. });
  289. router.get(
  290. "/settings",
  291. setTenant,
  292. isAdmin,
  293. error_catcher(async (req, res) => {
  294. const form = await user_settings_form(req);
  295. send_users_page({
  296. res,
  297. req,
  298. active_sub: "Settings",
  299. contents: {
  300. type: "card",
  301. title: req.__("Authentication settings"),
  302. contents: [renderForm(form, req.csrfToken())],
  303. },
  304. });
  305. })
  306. );
  307. router.post(
  308. "/settings",
  309. setTenant,
  310. isAdmin,
  311. error_catcher(async (req, res) => {
  312. const form = await user_settings_form(req);
  313. form.validate(req.body);
  314. if (form.hasErrors) {
  315. send_users_page({
  316. res,
  317. req,
  318. active_sub: "Settings",
  319. contents: {
  320. type: "card",
  321. title: req.__("Authentication settings"),
  322. contents: [renderForm(form, req.csrfToken())],
  323. },
  324. });
  325. } else {
  326. await save_config_from_form(form);
  327. req.flash("success", req.__("User settings updated"));
  328. res.redirect("/useradmin/settings");
  329. }
  330. })
  331. );
  332. router.get(
  333. "/ssl",
  334. setTenant,
  335. isAdmin,
  336. error_catcher(async (req, res) => {
  337. const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
  338. if (!isRoot) {
  339. req.flash(
  340. "warning",
  341. req.__("SSL settings not available for subdomain tenants")
  342. );
  343. res.redirect("/useradmin");
  344. return;
  345. }
  346. // TBD describe logic around letsencrypt
  347. const letsencrypt = getState().getConfig("letsencrypt", false);
  348. const has_custom =
  349. getState().getConfig("custom_ssl_certificate", false) &&
  350. getState().getConfig("custom_ssl_private_key", false);
  351. const show_warning =
  352. !hostname_matches_baseurl(req, getBaseDomain()) &&
  353. is_hsts_tld(getBaseDomain());
  354. send_users_page({
  355. res,
  356. req,
  357. active_sub: "SSL",
  358. contents: {
  359. above: [
  360. ...(letsencrypt && has_custom
  361. ? [
  362. {
  363. type: "card",
  364. contents: p(
  365. req.__(
  366. "You have enabled both Let's Encrypt certificates and custom SSL certificates. Let's Encrypt takes priority and the custom certificates will be ignored."
  367. )
  368. ),
  369. },
  370. ]
  371. : []),
  372. {
  373. type: "card",
  374. title: req.__(
  375. "HTTPS encryption with Let's Encrypt SSL certificate"
  376. ),
  377. contents: [
  378. p(
  379. req.__(
  380. `Saltcorn can automatically obtain an SSL certificate from <a href="https://letsencrypt.org/">Let's Encrypt</a> for single domains`
  381. )
  382. ),
  383. h5(
  384. req.__("Currently: "),
  385. letsencrypt
  386. ? span({ class: "badge badge-primary" }, req.__("Enabled"))
  387. : span({ class: "badge badge-secondary" }, req.__("Disabled"))
  388. ),
  389. letsencrypt
  390. ? post_btn(
  391. "/config/delete/letsencrypt",
  392. req.__("Disable LetsEncrypt HTTPS"),
  393. req.csrfToken(),
  394. { btnClass: "btn-danger", req }
  395. )
  396. : post_btn(
  397. "/admin/enable-letsencrypt",
  398. req.__("Enable LetsEncrypt HTTPS"),
  399. req.csrfToken(),
  400. { confirm: true, req }
  401. ),
  402. !letsencrypt &&
  403. show_warning &&
  404. !has_custom &&
  405. div(
  406. { class: "mt-3 alert alert-danger" },
  407. p(
  408. req.__(
  409. "The address you are using to reach Saltcorn does not match the Base URL."
  410. )
  411. ),
  412. p(
  413. req.__(
  414. "The DNS A records (for * and @, or a subdomain) should point to this server's IP address before enabling LetsEncrypt"
  415. )
  416. )
  417. ),
  418. ],
  419. },
  420. {
  421. type: "card",
  422. title: req.__("HTTPS encryption with custom SSL certificate"),
  423. contents: [
  424. p(
  425. req.__(
  426. `Or use custom SSL certificates, including wildcard certificates for multitenant applications`
  427. )
  428. ),
  429. h5(
  430. req.__("Currently: "),
  431. has_custom
  432. ? span({ class: "badge badge-primary" }, req.__("Enabled"))
  433. : span({ class: "badge badge-secondary" }, req.__("Disabled"))
  434. ),
  435. // TBD change to button
  436. link("/useradmin/ssl/custom", req.__("Edit custom SSL certificates")),
  437. ],
  438. },
  439. ],
  440. },
  441. });
  442. })
  443. );
  444. const ssl_form = (req) =>
  445. config_fields_form({
  446. req,
  447. field_names: ["custom_ssl_certificate", "custom_ssl_private_key"],
  448. action: "/useradmin/ssl/custom",
  449. });
  450. router.get(
  451. "/ssl/custom",
  452. setTenant,
  453. isAdmin,
  454. error_catcher(async (req, res) => {
  455. const form = await ssl_form(req);
  456. send_users_page({
  457. res,
  458. req,
  459. active_sub: "Settings",
  460. contents: {
  461. type: "card",
  462. title: req.__("Authentication settings"),
  463. sub2_page: req.__("Custom SSL certificates"),
  464. contents: [renderForm(form, req.csrfToken())],
  465. },
  466. });
  467. })
  468. );
  469. router.post(
  470. "/ssl/custom",
  471. setTenant,
  472. isAdmin,
  473. error_catcher(async (req, res) => {
  474. const form = await ssl_form(req);
  475. form.validate(req.body);
  476. if (form.hasErrors) {
  477. send_users_page({
  478. res,
  479. req,
  480. active_sub: "Settings",
  481. contents: {
  482. type: "card",
  483. title: req.__("Authentication settings"),
  484. sub2_page: req.__("Custom SSL certificates"),
  485. contents: [renderForm(form, req.csrfToken())],
  486. },
  487. });
  488. } else {
  489. await save_config_from_form(form);
  490. req.flash(
  491. "success",
  492. req.__("Custom SSL enabled. Restart for changes to take effect.") +
  493. " " +
  494. a({ href: "/admin/system" }, req.__("Restart here"))
  495. );
  496. res.redirect("/useradmin/ssl");
  497. }
  498. })
  499. );
  500. router.get(
  501. "/:id",
  502. setTenant,
  503. isAdmin,
  504. error_catcher(async (req, res) => {
  505. const { id } = req.params;
  506. const user = await User.findOne({ id });
  507. const form = await userForm(req, user);
  508. send_users_page({
  509. res,
  510. req,
  511. active_sub: "Users",
  512. sub2_page: user.email,
  513. contents: {
  514. above: [
  515. {
  516. type: "card",
  517. title: req.__("Edit user %s", user.email),
  518. contents: renderForm(form, req.csrfToken()),
  519. },
  520. {
  521. type: "card",
  522. title: req.__("API token"),
  523. contents: [
  524. // api token for user
  525. div(
  526. user.api_token
  527. ? span({ class: "mr-1" }, req.__("API token for this user: ")) +
  528. code(user.api_token)
  529. : req.__("No API token issued")
  530. ),
  531. // button for reset or generate api token
  532. div(
  533. { class: "mt-4" },
  534. post_btn(
  535. `/useradmin/gen-api-token/${user.id}`,
  536. user.api_token ? req.__("Reset") : req.__("Generate"),
  537. req.csrfToken()
  538. )
  539. ),
  540. // button for remove api token
  541. user.api_token && div(
  542. { class: "mt-4" },
  543. post_btn(
  544. `/useradmin/remove-api-token/${user.id}`,
  545. // TBD localization
  546. user.api_token ? req.__("Remove") : req.__("Generate"),
  547. req.csrfToken(),
  548. { req: req, confirm: true }
  549. )
  550. ),
  551. ],
  552. },
  553. ],
  554. },
  555. });
  556. })
  557. );
  558. /**
  559. * Save user data
  560. */
  561. router.post(
  562. "/save",
  563. setTenant,
  564. isAdmin,
  565. error_catcher(async (req, res) => {
  566. let {
  567. email,
  568. password,
  569. role_id,
  570. id,
  571. rnd_password,
  572. send_pwreset_email,
  573. _csrf,
  574. ...rest
  575. } = req.body;
  576. if (id) {
  577. try {
  578. await db.update("users", { email, role_id, ...rest }, id);
  579. req.flash("success", req.__(`User %s saved`, email));
  580. } catch (e) {
  581. req.flash("error", req.__(`Error editing user: %s`, e.message));
  582. }
  583. } else {
  584. if (rnd_password) password = User.generate_password();
  585. const u = await User.create({
  586. email,
  587. password,
  588. role_id: +role_id,
  589. ...rest,
  590. });
  591. const pwflash =
  592. rnd_password && !send_pwreset_email
  593. ? req.__(` with password %s`, code(password))
  594. : "";
  595. if (u.error) req.flash("error", u.error);
  596. else req.flash("success", req.__(`User %s created`, email) + pwflash);
  597. if (rnd_password && send_pwreset_email) await send_reset_email(u, req);
  598. }
  599. res.redirect(`/useradmin`);
  600. })
  601. );
  602. /**
  603. * Reset password for yser
  604. */
  605. router.post(
  606. "/reset-password/:id",
  607. setTenant,
  608. isAdmin,
  609. error_catcher(async (req, res) => {
  610. const { id } = req.params;
  611. const u = await User.findOne({ id });
  612. await send_reset_email(u, req);
  613. req.flash("success", req.__(`Reset password link sent to %s`, u.email));
  614. res.redirect(`/useradmin`);
  615. })
  616. );
  617. /**
  618. * Send verification email for user
  619. */
  620. router.post(
  621. "/send-verification/:id",
  622. setTenant,
  623. isAdmin,
  624. error_catcher(async (req, res) => {
  625. const { id } = req.params;
  626. const u = await User.findOne({ id });
  627. const result = await send_verification_email(u);
  628. console.log(result);
  629. if (result.error) req.flash("danger", result.error);
  630. else
  631. req.flash(
  632. "success",
  633. req.__(`Email verification link sent to %s`, u.email)
  634. );
  635. res.redirect(`/useradmin`);
  636. })
  637. );
  638. /**
  639. * Get new api token
  640. */
  641. router.post(
  642. "/gen-api-token/:id",
  643. setTenant,
  644. isAdmin,
  645. error_catcher(async (req, res) => {
  646. const { id } = req.params;
  647. const u = await User.findOne({ id });
  648. await u.getNewAPIToken();
  649. req.flash("success", req.__(`New API token generated`));
  650. res.redirect(`/useradmin/${u.id}`);
  651. })
  652. );
  653. /**
  654. * Remove api token
  655. */
  656. router.post(
  657. "/remove-api-token/:id",
  658. setTenant,
  659. isAdmin,
  660. error_catcher(async (req, res) => {
  661. const { id } = req.params;
  662. const u = await User.findOne({ id });
  663. await u.removeAPIToken();
  664. req.flash("success", req.__(`API token removed`));
  665. res.redirect(`/useradmin/${u.id}`);
  666. })
  667. );
  668. /**
  669. * Set random password
  670. */
  671. router.post(
  672. "/set-random-password/:id",
  673. setTenant,
  674. isAdmin,
  675. error_catcher(async (req, res) => {
  676. const { id } = req.params;
  677. const u = await User.findOne({ id });
  678. const newpw = User.generate_password();
  679. await u.changePasswordTo(newpw);
  680. await u.destroy_sessions();
  681. req.flash(
  682. "success",
  683. req.__(`Changed password for user %s to %s`, u.email, newpw)
  684. );
  685. res.redirect(`/useradmin`);
  686. })
  687. );
  688. router.post(
  689. "/disable/:id",
  690. setTenant,
  691. isAdmin,
  692. error_catcher(async (req, res) => {
  693. const { id } = req.params;
  694. const u = await User.findOne({ id });
  695. await u.update({ disabled: true });
  696. await u.destroy_sessions();
  697. req.flash("success", req.__(`Disabled user %s`, u.email));
  698. res.redirect(`/useradmin`);
  699. })
  700. );
  701. router.post(
  702. "/enable/:id",
  703. setTenant,
  704. isAdmin,
  705. error_catcher(async (req, res) => {
  706. const { id } = req.params;
  707. const u = await User.findOne({ id });
  708. await u.update({ disabled: false });
  709. req.flash("success", req.__(`Enabled user %s`, u.email));
  710. res.redirect(`/useradmin`);
  711. })
  712. );
  713. router.post(
  714. "/delete/:id",
  715. setTenant,
  716. isAdmin,
  717. error_catcher(async (req, res) => {
  718. const { id } = req.params;
  719. const u = await User.findOne({ id });
  720. await u.delete();
  721. req.flash("success", req.__(`User %s deleted`, u.email));
  722. res.redirect(`/useradmin`);
  723. })
  724. );