Added proper login and registration flows.
This commit is contained in:
parent
6b1e20d544
commit
c58e29cbd8
|
@ -12,3 +12,12 @@ services:
|
|||
environment:
|
||||
POSTGRES_USER: gymboard-api-dev
|
||||
POSTGRES_PASSWORD: testpass
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog
|
||||
restart: always
|
||||
expose:
|
||||
- "8025"
|
||||
ports:
|
||||
- "1025:1025" # SMTP
|
||||
- "8025:8025" # Web UI
|
||||
|
|
|
@ -37,6 +37,10 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
|
|
|
@ -51,7 +51,9 @@ public class SecurityConfig {
|
|||
HttpMethod.POST,
|
||||
"/gyms/submissions",
|
||||
"/gyms/submissions/upload",
|
||||
"/auth/token"
|
||||
"/auth/token",
|
||||
"/auth/register",
|
||||
"/auth/activate"
|
||||
).permitAll()
|
||||
// Everything else must be authenticated, just to be safe.
|
||||
.anyRequest().authenticated();
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package nl.andrewlalis.gymboard_api.controller;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.TokenCredentials;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.TokenResponse;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserResponse;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.*;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.service.auth.TokenService;
|
||||
import nl.andrewlalis.gymboard_api.service.auth.UserService;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
@ -15,9 +14,35 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
@RestController
|
||||
public class AuthController {
|
||||
private final TokenService tokenService;
|
||||
private final UserService userService;
|
||||
|
||||
public AuthController(TokenService tokenService) {
|
||||
public AuthController(TokenService tokenService, UserService userService) {
|
||||
this.tokenService = tokenService;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for registering a new user in the system. <strong>This is a
|
||||
* public endpoint.</strong> If the user is successfully created, an email
|
||||
* will be sent to them with a link for activating the account.
|
||||
* @param payload The payload.
|
||||
* @return The created user.
|
||||
*/
|
||||
@PostMapping(path = "/auth/register")
|
||||
public UserResponse registerNewUser(@RequestBody UserCreationPayload payload) {
|
||||
return userService.createUser(payload, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for activating a new user via an activation code. <strong>This
|
||||
* is a public endpoint.</strong> If the code is recent (within 24 hours)
|
||||
* and the user exists, then they'll be activated and able to log in.
|
||||
* @param payload The payload containing the activation code.
|
||||
* @return The activated user.
|
||||
*/
|
||||
@PostMapping(path = "/auth/activate")
|
||||
public UserResponse activateUser(@RequestBody UserActivationPayload payload) {
|
||||
return userService.activateUser(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
|
||||
public record UserActivationPayload(String code) {}
|
|
@ -0,0 +1,12 @@
|
|||
package nl.andrewlalis.gymboard_api.dao.auth;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.auth.UserActivationCode;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserActivationCodeRepository extends JpaRepository<UserActivationCode, Long> {
|
||||
Optional<UserActivationCode> findByCode(String code);
|
||||
}
|
|
@ -144,7 +144,7 @@ public class SampleDataLoader implements ApplicationListener<ContextRefreshedEve
|
|||
String[] roleNames = record.get(3).split("\\s*\\|\\s*");
|
||||
|
||||
UserCreationPayload payload = new UserCreationPayload(email, password, name);
|
||||
var resp = userService.createUser(payload);
|
||||
var resp = userService.createUser(payload, false);
|
||||
User user = userRepository.findByIdWithRoles(resp.id()).orElseThrow();
|
||||
for (var roleName : roleNames) {
|
||||
if (roleName.isBlank()) continue;
|
||||
|
|
|
@ -60,6 +60,10 @@ public class User {
|
|||
return activated;
|
||||
}
|
||||
|
||||
public void setActivated(boolean activated) {
|
||||
this.activated = activated;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package nl.andrewlalis.gymboard_api.model.auth;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "auth_user_activation_code")
|
||||
public class UserActivationCode {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private User user;
|
||||
|
||||
@Column(nullable = false, unique = true, updatable = false, length = 127)
|
||||
private String code;
|
||||
|
||||
public UserActivationCode() {}
|
||||
|
||||
public UserActivationCode(User user, String code) {
|
||||
this.user = user;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
|
@ -1,26 +1,54 @@
|
|||
package nl.andrewlalis.gymboard_api.service.auth;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserActivationPayload;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserCreationPayload;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserActivationCodeRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.UserActivationCode;
|
||||
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
private static final Logger log = LoggerFactory.getLogger(UserService.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final UserActivationCodeRepository activationCodeRepository;
|
||||
private final ULID ulid;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
public UserService(UserRepository userRepository, ULID ulid, PasswordEncoder passwordEncoder) {
|
||||
@Value("${app.web-origin}")
|
||||
private String webOrigin;
|
||||
|
||||
public UserService(
|
||||
UserRepository userRepository,
|
||||
UserActivationCodeRepository activationCodeRepository, ULID ulid,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JavaMailSender mailSender
|
||||
) {
|
||||
this.userRepository = userRepository;
|
||||
this.activationCodeRepository = activationCodeRepository;
|
||||
this.ulid = ulid;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.mailSender = mailSender;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
@ -31,15 +59,71 @@ public class UserService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public UserResponse createUser(UserCreationPayload payload) {
|
||||
public UserResponse createUser(UserCreationPayload payload, boolean requireActivation) {
|
||||
// TODO: Validate user payload.
|
||||
User user = userRepository.save(new User(
|
||||
ulid.nextULID(),
|
||||
true, // TODO: Change this to false once email activation is in.
|
||||
!requireActivation,
|
||||
payload.email(),
|
||||
passwordEncoder.encode(payload.password()),
|
||||
payload.name()
|
||||
));
|
||||
if (requireActivation) {
|
||||
generateAndSendActivationCode(user);
|
||||
}
|
||||
return new UserResponse(user);
|
||||
}
|
||||
|
||||
private void generateAndSendActivationCode(User user) {
|
||||
Random random = new SecureRandom();
|
||||
StringBuilder sb = new StringBuilder(127);
|
||||
final String alphabet = "bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ23456789";
|
||||
for (int i = 0; i < 127; i++) {
|
||||
sb.append(alphabet.charAt(random.nextInt(alphabet.length())));
|
||||
}
|
||||
String rawCode = sb.toString();
|
||||
UserActivationCode activationCode = activationCodeRepository.save(new UserActivationCode(user, rawCode));
|
||||
// Send email.
|
||||
String activationLink = webOrigin + "/activate?code=" + activationCode.getCode();
|
||||
String emailContent = String.format(
|
||||
"""
|
||||
<p>Hello %s,</p>
|
||||
|
||||
<p>
|
||||
Thank you for registering a new account at Gymboard!
|
||||
</p>
|
||||
<p>
|
||||
Please click <a href="%s">here</a> to activate your account.
|
||||
</p>
|
||||
""",
|
||||
user.getName(),
|
||||
activationLink
|
||||
);
|
||||
MimeMessage msg = mailSender.createMimeMessage();
|
||||
try {
|
||||
MimeMessageHelper helper = new MimeMessageHelper(msg, "UTF-8");
|
||||
helper.setFrom("Gymboard <noreply@gymboard.io>");
|
||||
helper.setSubject("Activate Your Gymboard Account");
|
||||
helper.setTo(user.getEmail());
|
||||
helper.setText(emailContent, true);
|
||||
mailSender.send(msg);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Error sending user activation email.", e);
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserResponse activateUser(UserActivationPayload payload) {
|
||||
UserActivationCode activationCode = activationCodeRepository.findByCode(payload.code())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
||||
LocalDateTime cutoff = LocalDateTime.now().minusDays(1);
|
||||
if (activationCode.getCreatedAt().isBefore(cutoff)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Code is expired.");
|
||||
}
|
||||
User user = activationCode.getUser();
|
||||
user.setActivated(true);
|
||||
userRepository.save(user);
|
||||
return new UserResponse(user);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,11 @@ spring.jpa.show-sql=false
|
|||
spring.task.execution.pool.core-size=3
|
||||
spring.task.execution.pool.max-size=10
|
||||
|
||||
app.auth.private-key-location=./private_key.der
|
||||
spring.mail.host=127.0.0.1
|
||||
spring.mail.port=1025
|
||||
spring.mail.protocol=smtp
|
||||
spring.mail.properties.mail.smtp.timeout=10000
|
||||
|
||||
app.auth.private-key-location=./private_key.der
|
||||
app.web-origin=http://localhost:9000
|
||||
|
||||
|
|
|
@ -111,7 +111,9 @@ module.exports = configure(function (ctx) {
|
|||
// directives: [],
|
||||
|
||||
// Quasar plugins
|
||||
plugins: [],
|
||||
plugins: [
|
||||
'Notify'
|
||||
],
|
||||
},
|
||||
|
||||
// animations: 'all', // --- includes all animations
|
||||
|
|
|
@ -14,6 +14,12 @@ export interface TokenCredentials {
|
|||
password: string;
|
||||
}
|
||||
|
||||
export interface UserCreationPayload {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
class AuthModule {
|
||||
private static readonly TOKEN_REFRESH_INTERVAL_MS = 30000;
|
||||
|
||||
|
@ -35,6 +41,16 @@ class AuthModule {
|
|||
clearTimeout(this.tokenRefreshTimer);
|
||||
}
|
||||
|
||||
public async register(payload: UserCreationPayload) {
|
||||
const response = await api.post('/auth/register', payload);
|
||||
console.log(response);
|
||||
}
|
||||
|
||||
public async activateUser(code: string): Promise<User> {
|
||||
const response = await api.post('/auth/activate', {code: code});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
private async fetchNewToken(credentials: TokenCredentials): Promise<string> {
|
||||
const response = await api.post('/auth/token', credentials);
|
||||
return response.data.token;
|
||||
|
|
|
@ -28,6 +28,17 @@ export default boot(({ app }) => {
|
|||
messages,
|
||||
});
|
||||
|
||||
// Set the locale to the preferred locale, if possible.
|
||||
const userLocale = window.navigator.language;
|
||||
if (userLocale === 'nl-NL') {
|
||||
i18n.global.locale.value = userLocale;
|
||||
} else {
|
||||
i18n.global.locale.value = 'en-US';
|
||||
}
|
||||
|
||||
// Temporary override if you want to test a particular locale.
|
||||
i18n.global.locale.value = 'nl-NL';
|
||||
|
||||
// Set i18n instance on app
|
||||
app.use(i18n);
|
||||
});
|
||||
|
|
|
@ -10,14 +10,14 @@
|
|||
<q-list>
|
||||
<q-item clickable v-close-popup @click="api.auth.logout(authStore)">
|
||||
<q-item-section>
|
||||
<q-item-label>Log out</q-item-label>
|
||||
<q-item-label>{{ $t('accountMenuItem.logOut') }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<q-btn
|
||||
color="primary"
|
||||
:label="$t('Login')"
|
||||
:label="$t('accountMenuItem.logIn')"
|
||||
v-if="!authStore.loggedIn"
|
||||
no-caps
|
||||
icon="person"
|
||||
|
|
|
@ -3,6 +3,21 @@ export default {
|
|||
language: 'Language',
|
||||
pages: 'Pages',
|
||||
},
|
||||
registerPage: {
|
||||
title: 'Create a Gymboard Account',
|
||||
name: 'Name',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
register: 'Register',
|
||||
error: 'An error occurred.'
|
||||
},
|
||||
loginPage: {
|
||||
title: 'Login to Gymboard',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
logIn: 'Log in',
|
||||
createAccount: 'Create an account'
|
||||
},
|
||||
indexPage: {
|
||||
searchHint: 'Search for a Gym',
|
||||
},
|
||||
|
@ -24,4 +39,8 @@ export default {
|
|||
submit: 'Submit',
|
||||
},
|
||||
},
|
||||
accountMenuItem: {
|
||||
logIn: 'Login',
|
||||
logOut: 'Log out'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,6 +3,21 @@ export default {
|
|||
language: 'Taal',
|
||||
pages: "Pagina's",
|
||||
},
|
||||
registerPage: {
|
||||
title: 'Maak een nieuwe Gymboard account aan',
|
||||
name: 'Naam',
|
||||
email: 'E-mail',
|
||||
password: 'Wachtwoord',
|
||||
register: 'Registreren',
|
||||
error: 'Er is een fout opgetreden.'
|
||||
},
|
||||
loginPage: {
|
||||
title: 'Inloggen bij Gymboard',
|
||||
email: 'E-mail',
|
||||
password: 'Wachtwoord',
|
||||
logIn: 'Inloggen',
|
||||
createAccount: 'Account aanmaken'
|
||||
},
|
||||
indexPage: {
|
||||
searchHint: 'Zoek een sportschool',
|
||||
},
|
||||
|
@ -24,4 +39,8 @@ export default {
|
|||
submit: 'Sturen',
|
||||
},
|
||||
},
|
||||
accountMenuItem: {
|
||||
logIn: 'Inloggen',
|
||||
logOut: 'Uitloggen'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<StandardCenteredPage>
|
||||
<h3 class="text-center">{{ statusText }}</h3>
|
||||
</StandardCenteredPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {useRoute, useRouter} from 'vue-router';
|
||||
import api from 'src/api/main';
|
||||
import {sleep} from 'src/utils';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const t = useI18n().t;
|
||||
|
||||
const statusText = ref('');
|
||||
statusText.value = t('activationPage.activating');
|
||||
|
||||
onMounted(async () => {
|
||||
await sleep(500);
|
||||
const code = route.query.code as string;
|
||||
try {
|
||||
const user = api.auth.activateUser(code);
|
||||
statusText.value = t('activationPage.success');
|
||||
console.log(user);
|
||||
await sleep(2000);
|
||||
await router.replace('/login');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
statusText.value = t('activationPage.failure');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,12 +1,13 @@
|
|||
<template>
|
||||
<StandardCenteredPage>
|
||||
<h3 class="text-center">Login to Gymboard</h3>
|
||||
<h3 class="text-center">{{ $t('loginPage.title') }}</h3>
|
||||
<q-form @submit="tryLogin" @reset="resetLogin">
|
||||
<SlimForm>
|
||||
<div class="row">
|
||||
<q-input
|
||||
:label="$t('loginPage.email')"
|
||||
v-model="loginModel.email"
|
||||
type="email"
|
||||
class="col-12"
|
||||
/>
|
||||
</div>
|
||||
|
@ -27,10 +28,15 @@
|
|||
</q-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-btn type="submit" label="Log in" color="primary" class="q-mt-md col-12" no-caps/>
|
||||
<q-btn type="submit" :label="$t('loginPage.logIn')" color="primary" class="q-mt-md col-12" no-caps/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-btn flat no-caps label="Create an account" color="secondary" class="q-mt-md col-12" style="text-decoration: underline"/>
|
||||
<router-link
|
||||
:to="{ path: '/register', query: route.query.next ? { next: route.query.next } : {} }"
|
||||
class="q-mt-md text-primary text-center col-12"
|
||||
>
|
||||
{{ $t('loginPage.createAccount') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</SlimForm>
|
||||
</q-form>
|
||||
|
@ -38,7 +44,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import StandardCenteredPage from 'src/components/StandardCenteredPage.vue';
|
||||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
||||
import SlimForm from 'components/SlimForm.vue';
|
||||
import {ref} from 'vue';
|
||||
import api from 'src/api/main';
|
||||
|
@ -56,7 +62,6 @@ const loginModel = ref({
|
|||
const passwordVisible = ref(false);
|
||||
|
||||
async function tryLogin() {
|
||||
console.log('logging in...');
|
||||
try {
|
||||
await api.auth.login(authStore, loginModel.value);
|
||||
const dest = route.query.next ? decodeURIComponent(route.query.next as string) : '/';
|
||||
|
@ -70,7 +75,6 @@ function resetLogin() {
|
|||
loginModel.value.email = '';
|
||||
loginModel.value.password = '';
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<StandardCenteredPage>
|
||||
<h3 class="text-center">{{ $t('registerPage.title') }}</h3>
|
||||
<q-form @submit="tryRegister" @reset="resetForm">
|
||||
<SlimForm>
|
||||
<div class="row">
|
||||
<q-input
|
||||
:label="$t('registerPage.name')"
|
||||
v-model="registerModel.name"
|
||||
type="text"
|
||||
class="col-12"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-input
|
||||
:label="$t('registerPage.email')"
|
||||
v-model="registerModel.email"
|
||||
type="email"
|
||||
class="col-12"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-input
|
||||
:label="$t('registerPage.password')"
|
||||
v-model="registerModel.password"
|
||||
:type="passwordVisible ? 'text' : 'password'"
|
||||
class="col-12"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="passwordVisible ? 'visibility' : 'visibility_off'"
|
||||
class="cursor-pointer"
|
||||
@click="passwordVisible = !passwordVisible"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-btn
|
||||
type="submit"
|
||||
:label="$t('registerPage.register')"
|
||||
color="primary"
|
||||
class="q-mt-md col-12"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</SlimForm>
|
||||
</q-form>
|
||||
</StandardCenteredPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SlimForm from 'components/SlimForm.vue';
|
||||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
||||
import api from 'src/api/main';
|
||||
import {useRouter} from 'vue-router';
|
||||
import {ref} from 'vue';
|
||||
import {useQuasar} from 'quasar';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const registerModel = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
const passwordVisible = ref(false);
|
||||
|
||||
const t = useI18n().t;
|
||||
const quasar = useQuasar();
|
||||
|
||||
async function tryRegister() {
|
||||
try {
|
||||
await api.auth.register(registerModel.value);
|
||||
await router.push('/register/success');
|
||||
} catch (error) {
|
||||
quasar.notify({
|
||||
message: t('registerPage.error'),
|
||||
type: 'negative'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
registerModel.value.name = '';
|
||||
registerModel.value.email = '';
|
||||
registerModel.value.password = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<StandardCenteredPage>
|
||||
<h3 class="text-center">{{ $t('registrationSuccessPage.title') }}</h3>
|
||||
<p>Check your email for the link to activate your account.</p>
|
||||
<p>You may safely close this page.</p>
|
||||
</StandardCenteredPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -6,9 +6,19 @@ import GymSubmissionPage from 'pages/gym/GymSubmissionPage.vue';
|
|||
import GymHomePage from 'pages/gym/GymHomePage.vue';
|
||||
import GymLeaderboardsPage from 'pages/gym/GymLeaderboardsPage.vue';
|
||||
import TestingPage from 'pages/TestingPage.vue';
|
||||
import LoginPage from 'pages/LoginPage.vue';
|
||||
import LoginPage from 'pages/auth/LoginPage.vue';
|
||||
import RegisterPage from "pages/auth/RegisterPage.vue";
|
||||
import RegistrationSuccessPage from "pages/auth/RegistrationSuccessPage.vue";
|
||||
import ActivationPage from "pages/auth/ActivationPage.vue";
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
// Auth-related pages, which live outside the main layout.
|
||||
{ path: '/login', component: LoginPage },
|
||||
{ path: '/register', component: RegisterPage },
|
||||
{ path: '/register/success', component: RegistrationSuccessPage },
|
||||
{ path: '/activate', component: ActivationPage },
|
||||
|
||||
// Main app:
|
||||
{
|
||||
path: '/',
|
||||
component: MainLayout,
|
||||
|
@ -26,7 +36,6 @@ const routes: RouteRecordRaw[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{ path: '/login', component: LoginPage },
|
||||
|
||||
// Always leave this as last one,
|
||||
// but you can also remove it
|
||||
|
|
Loading…
Reference in New Issue