Register.vue 7.9 KB

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