Register.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <script setup lang="ts">
  2. import { defineAsyncComponent, ref, watch, onMounted } from "vue";
  3. import { useRoute } from "vue-router";
  4. import Toast from "toasters";
  5. import { useUserAuthStore } from "@/stores/userAuth";
  6. import { useModalsStore } from "@/stores/modals";
  7. import validation from "@/validation";
  8. const InputHelpBox = defineAsyncComponent(
  9. () => import("@/components/InputHelpBox.vue")
  10. );
  11. const route = useRoute();
  12. const username = ref({
  13. value: "",
  14. valid: false,
  15. entered: false,
  16. message: "Only letters, numbers and underscores are allowed."
  17. });
  18. const email = ref({
  19. value: "",
  20. valid: false,
  21. entered: false,
  22. message: "Please enter a valid email address."
  23. });
  24. const password = ref({
  25. value: "",
  26. valid: false,
  27. entered: false,
  28. visible: false,
  29. message:
  30. "Include at least one lowercase letter, one uppercase letter, one number and one special character."
  31. });
  32. const recaptcha = ref({
  33. key: "",
  34. token: "",
  35. enabled: false
  36. });
  37. const apiDomain = ref("");
  38. const siteSettings = ref({
  39. registrationDisabled: false,
  40. githubAuthentication: false
  41. });
  42. const passwordElement = ref();
  43. const { register } = useUserAuthStore();
  44. const { openModal, closeCurrentModal } = useModalsStore();
  45. const submitModal = () => {
  46. if (!username.value.valid || !email.value.valid || !password.value.valid)
  47. return new Toast("Please ensure all fields are valid.");
  48. return register({
  49. username: username.value.value,
  50. email: email.value.value,
  51. password: password.value.value,
  52. recaptchaToken: recaptcha.value.token
  53. })
  54. .then((res: any) => {
  55. if (res.status === "success") window.location.reload();
  56. })
  57. .catch(err => new Toast(err.message));
  58. };
  59. const togglePasswordVisibility = () => {
  60. if (passwordElement.value.type === "password") {
  61. passwordElement.value.type = "text";
  62. password.value.visible = true;
  63. } else {
  64. passwordElement.value.type = "password";
  65. password.value.visible = false;
  66. }
  67. };
  68. const changeToLoginModal = () => {
  69. closeCurrentModal();
  70. openModal("login");
  71. };
  72. const githubRedirect = () => {
  73. localStorage.setItem("github_redirect", route.path);
  74. };
  75. watch(
  76. () => username.value.value,
  77. value => {
  78. username.value.entered = true;
  79. if (!validation.isLength(value, 2, 32)) {
  80. username.value.message =
  81. "Username must have between 2 and 32 characters.";
  82. username.value.valid = false;
  83. } else if (!validation.regex.azAZ09_.test(value)) {
  84. username.value.message =
  85. "Invalid format. Allowed characters: a-z, A-Z, 0-9 and _.";
  86. username.value.valid = false;
  87. } else if (value.replaceAll(/[_]/g, "").length === 0) {
  88. username.value.message =
  89. "Invalid format. Allowed characters: a-z, A-Z, 0-9 and _, and there has to be at least one letter or number.";
  90. username.value.valid = false;
  91. } else {
  92. username.value.message = "Everything looks great!";
  93. username.value.valid = true;
  94. }
  95. }
  96. );
  97. watch(
  98. () => email.value.value,
  99. value => {
  100. email.value.entered = true;
  101. if (!validation.isLength(value, 3, 254)) {
  102. email.value.message =
  103. "Email must have between 3 and 254 characters.";
  104. email.value.valid = false;
  105. } else if (
  106. value.indexOf("@") !== value.lastIndexOf("@") ||
  107. !validation.regex.emailSimple.test(value)
  108. ) {
  109. email.value.message = "Invalid format.";
  110. email.value.valid = false;
  111. } else {
  112. email.value.message = "Everything looks great!";
  113. email.value.valid = true;
  114. }
  115. }
  116. );
  117. watch(
  118. () => password.value.value,
  119. value => {
  120. password.value.entered = true;
  121. if (!validation.isLength(value, 6, 200)) {
  122. password.value.message =
  123. "Password must have between 6 and 200 characters.";
  124. password.value.valid = false;
  125. } else if (!validation.regex.password.test(value)) {
  126. password.value.message =
  127. "Include at least one lowercase letter, one uppercase letter, one number and one special character.";
  128. password.value.valid = false;
  129. } else {
  130. password.value.message = "Everything looks great!";
  131. password.value.valid = true;
  132. }
  133. }
  134. );
  135. onMounted(async () => {
  136. apiDomain.value = await lofig.get("backend.apiDomain");
  137. lofig.get("siteSettings").then(settings => {
  138. if (settings.registrationDisabled) {
  139. new Toast("Registration is disabled.");
  140. closeCurrentModal();
  141. } else {
  142. siteSettings.value = settings;
  143. }
  144. });
  145. lofig.get("recaptcha").then(obj => {
  146. recaptcha.value.enabled = obj.enabled;
  147. if (obj.enabled === true) {
  148. recaptcha.value.key = obj.key;
  149. const recaptchaScript = document.createElement("script");
  150. recaptchaScript.onload = () => {
  151. grecaptcha.ready(() => {
  152. grecaptcha
  153. .execute(recaptcha.value.key, { action: "login" })
  154. .then(token => {
  155. recaptcha.value.token = token;
  156. });
  157. });
  158. };
  159. recaptchaScript.setAttribute(
  160. "src",
  161. `https://www.google.com/recaptcha/api.js?render=${recaptcha.value.key}`
  162. );
  163. document.head.appendChild(recaptchaScript);
  164. }
  165. });
  166. });
  167. </script>
  168. <template>
  169. <div>
  170. <modal
  171. title="Register"
  172. class="register-modal"
  173. :size="'slim'"
  174. @closed="closeCurrentModal()"
  175. >
  176. <template #body>
  177. <!-- email address -->
  178. <p class="control">
  179. <label class="label">Email</label>
  180. <input
  181. v-model="email.value"
  182. class="input"
  183. type="email"
  184. placeholder="Email..."
  185. @keyup.enter="submitModal()"
  186. autofocus
  187. />
  188. </p>
  189. <transition name="fadein-helpbox">
  190. <input-help-box
  191. :entered="email.entered"
  192. :valid="email.valid"
  193. :message="email.message"
  194. />
  195. </transition>
  196. <!-- username -->
  197. <p class="control">
  198. <label class="label">Username</label>
  199. <input
  200. v-model="username.value"
  201. class="input"
  202. type="text"
  203. placeholder="Username..."
  204. @keyup.enter="submitModal()"
  205. />
  206. </p>
  207. <transition name="fadein-helpbox">
  208. <input-help-box
  209. :entered="username.entered"
  210. :valid="username.valid"
  211. :message="username.message"
  212. />
  213. </transition>
  214. <!-- password -->
  215. <p class="control">
  216. <label class="label">Password</label>
  217. </p>
  218. <div id="password-visibility-container">
  219. <input
  220. v-model="password.value"
  221. class="input"
  222. type="password"
  223. ref="passwordElement"
  224. placeholder="Password..."
  225. @keyup.enter="submitModal()"
  226. />
  227. <a @click="togglePasswordVisibility()">
  228. <i class="material-icons">
  229. {{
  230. !password.visible
  231. ? "visibility"
  232. : "visibility_off"
  233. }}
  234. </i>
  235. </a>
  236. </div>
  237. <transition name="fadein-helpbox">
  238. <input-help-box
  239. :valid="password.valid"
  240. :entered="password.entered"
  241. :message="password.message"
  242. />
  243. </transition>
  244. <br />
  245. <p>
  246. By registering you agree to our
  247. <router-link to="/terms" @click="closeCurrentModal()">
  248. Terms of Service
  249. </router-link>
  250. and
  251. <router-link to="/privacy" @click="closeCurrentModal()">
  252. Privacy Policy</router-link
  253. >.
  254. </p>
  255. </template>
  256. <template #footer>
  257. <div id="actions">
  258. <button class="button is-primary" @click="submitModal()">
  259. Register
  260. </button>
  261. <a
  262. v-if="siteSettings.githubAuthentication"
  263. class="button is-github"
  264. :href="apiDomain + '/auth/github/authorize'"
  265. @click="githubRedirect()"
  266. >
  267. <div class="icon">
  268. <img
  269. class="invert"
  270. src="/assets/social/github.svg"
  271. />
  272. </div>
  273. &nbsp;&nbsp;Register with GitHub
  274. </a>
  275. </div>
  276. <p class="content-box-optional-helper">
  277. <a @click="changeToLoginModal()">
  278. Already have an account?
  279. </a>
  280. </p>
  281. </template>
  282. </modal>
  283. </div>
  284. </template>
  285. <style lang="less" scoped>
  286. .night-mode {
  287. .modal-card,
  288. .modal-card-head,
  289. .modal-card-body,
  290. .modal-card-foot {
  291. background-color: var(--dark-grey-3);
  292. }
  293. .label,
  294. p:not(.help) {
  295. color: var(--light-grey-2);
  296. }
  297. }
  298. .control {
  299. margin-bottom: 2px !important;
  300. }
  301. .modal-card-foot {
  302. display: flex;
  303. justify-content: space-between;
  304. flex-wrap: wrap;
  305. .content-box-optional-helper {
  306. margin-top: 0;
  307. }
  308. }
  309. .button.is-github {
  310. background-color: var(--dark-grey-2);
  311. color: var(--white) !important;
  312. }
  313. .is-github:focus {
  314. background-color: var(--dark-grey-4);
  315. }
  316. .invert {
  317. filter: brightness(5);
  318. }
  319. #recaptcha {
  320. padding: 10px 0;
  321. }
  322. a {
  323. color: var(--primary-color);
  324. }
  325. </style>
  326. <style lang="less">
  327. .grecaptcha-badge {
  328. z-index: 2000;
  329. }
  330. </style>