feat: Re-enable signup and login

This commit is contained in:
aldrin 2021-08-02 10:14:38 +02:00
parent 3ad75c2f0d
commit 74afc805dc
12 changed files with 168 additions and 87 deletions

View file

@ -8,7 +8,7 @@ export class APIError extends Error {
} }
} }
async function request( async function api_request(
method: HTTPMethod, method: HTTPMethod,
endpoint: string, endpoint: string,
successStatus: number, successStatus: number,
@ -29,7 +29,7 @@ async function request(
} }
init.headers.set("Accept", "application/json"); init.headers.set("Accept", "application/json");
init.headers.set("Content-Type", "application/json"); init.headers.set("Content-Type", "application/json");
const response = await fetch(`/${endpoint}/`, init); const response = await fetch(`/api/${endpoint}/`, init);
if (response.status !== 204) { if (response.status !== 204) {
if (response.status === successStatus) { if (response.status === successStatus) {
return await response.json(); return await response.json();
@ -39,16 +39,6 @@ async function request(
} }
} }
async function api_request(
method: HTTPMethod,
endpoint: string,
successStatus: number,
data: any,
authToken?: string
) {
return request(method, `api/${endpoint}`, successStatus, data, authToken);
}
export class User { export class User {
email: string | undefined; email: string | undefined;
password: string | undefined; password: string | undefined;
@ -62,7 +52,7 @@ export class User {
} }
async login(): Promise<void> { async login(): Promise<void> {
if (!this.email || !this.password) throw new APIError("", ""); if (!this.username || !this.password) throw new APIError("", "");
// logout any existing sessions // logout any existing sessions
//await logout() //await logout()
@ -73,7 +63,7 @@ export class User {
// authenticate us // authenticate us
const body = new window.FormData(); const body = new window.FormData();
body.append("username", this.email); body.append("username", this.username);
body.append("password", this.password); body.append("password", this.password);
const csrf_token = Cookies.get("csrftoken"); const csrf_token = Cookies.get("csrftoken");
if (csrf_token) body.append("csrfmiddlewaretoken", csrf_token); if (csrf_token) body.append("csrfmiddlewaretoken", csrf_token);
@ -102,7 +92,7 @@ export class User {
async signup(): Promise<void> { async signup(): Promise<void> {
await api_request("POST", "users", 201, { await api_request("POST", "users", 201, {
email: this.email, username: this.username,
password: this.password, password: this.password,
}); });
} }

View file

@ -1,38 +1,45 @@
<template> <template>
<form class="box" @submit.prevent="doAction"> <form class="box" @submit.prevent="doAction">
<b-field label="E-Mail-Adresse"> <div v-if="errorMessage" class="notification is-danger">
<b-input type="email" v-model="user.email" /> {{ errorMessage }}
</div>
<b-field label="Benutzername">
<b-input type="text" v-model="user.username" />
</b-field> </b-field>
<b-field label="Kennwort"> <b-field label="Kennwort">
<b-input type="password" v-model="user.password" /> <b-input type="password" v-model="user.password" />
</b-field> </b-field>
<b-field v-if="mode === 'signup'" label="Kennwort wiederholen">
<div v-if="false" class="notification is-danger"> <b-input type="password" v-model="password2" />
Email or password was wrong. </b-field>
</div>
<div class="buttons"> <div class="buttons">
<b-button <b-button native-type="submit" type="is-primary">
native-type="submit"
type="is-primary"
style="margin-left: auto; margin-right: 0; order: 10"
>
{{ mode === "login" ? "Anmelden" : "Konto anlegen" }} {{ mode === "login" ? "Anmelden" : "Konto anlegen" }}
</b-button> </b-button>
<router-link v-if="mode === 'login'" to="/signup"
>Konto anlegen</router-link
>
<router-link v-else to="/login">Anmelden</router-link>
</div> </div>
</form> </form>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Watch } from "vue-property-decorator";
import { mixins } from "vue-class-component";
import { User } from "@/api"; import { User } from "@/api";
import { Route } from "vue-router"; import { Route } from "vue-router";
import { NotifyMixin } from "@/mixins";
@Component @Component
export default class LoginForm extends Vue { export default class LoginForm extends mixins(NotifyMixin) {
@Prop() private user!: User; @Prop() private user!: User;
private mode: "login" | "signup" = "login"; private mode: "login" | "signup" = "login";
private password2 = "";
private errorMessage = "";
public created(): void { public created(): void {
if (this.$route.name === "signup") this.mode = "signup"; if (this.$route.name === "signup") this.mode = "signup";
@ -48,19 +55,20 @@ export default class LoginForm extends Vue {
} }
private async doAction() { private async doAction() {
try {
if (this.mode === "login") { if (this.mode === "login") {
await this.user.login(); await this.user.login();
if (this.$route.name !== "home") this.$router.push({ name: "home" }); if (this.$route.name !== "home") this.$router.push({ name: "home" });
} else { } else {
await this.user.signup(); await this.user.signup();
this.$router.push({ name: "login" }); this.$router.push({ name: "login" });
this.$buefy.toast.open({ this.showSuccess("Du kannst dich nun anmelden.");
message: }
"Eine E-Mail zur Bestätigung deiner E-Mail-Adresse wurde versendet.", } catch {
type: "is-success", this.errorMessage = "Fehler";
});
} }
// TODO: error handling, show confirmation page
} }
} }
</script> </script>
<style lang="scss"></style>

View file

@ -2,35 +2,12 @@
<b-navbar> <b-navbar>
<template #brand> <template #brand>
<b-navbar-item tag="router-link" :to="{ path: '/' }"> <b-navbar-item tag="router-link" :to="{ path: '/' }">
<img <img src="/img/logo_text.png" alt="userausfall" />
src="/img/logo_text.png"
alt="Lightweight UI components for Vue.js based on Bulma"
/>
</b-navbar-item> </b-navbar-item>
</template> </template>
<template #start> <template #start> </template>
<!--
<b-navbar-item href="#"> Home </b-navbar-item>
<b-navbar-item href="#"> Documentation </b-navbar-item>
<b-navbar-dropdown label="Info">
<b-navbar-item href="#"> About </b-navbar-item>
<b-navbar-item href="#"> Contact </b-navbar-item>
</b-navbar-dropdown>
-->
</template>
<template #end> <template #end> </template>
<b-navbar-item tag="div">
<div class="buttons">
<router-link :to="{ name: 'signup' }" class="button is-primary">
<strong>Konto anlegen</strong>
</router-link>
<router-link :to="{ name: 'login' }" class="button is-light"
>Anmelden</router-link
>
</div>
</b-navbar-item>
</template>
</b-navbar> </b-navbar>
</template> </template>

View file

@ -3,17 +3,22 @@ import Vue from "vue";
@Component @Component
export class NotifyMixin extends Vue { export class NotifyMixin extends Vue {
showError(): void { public showError(): void {
this.$buefy.toast.open({ this.showMessage("Es ist leider ein Fehler aufgetreten.", "is-danger");
message: "Es ist leider ein Fehler aufgetreten.",
type: "is-danger",
});
} }
showSuccess(message: string): void { showSuccess(message: string): void {
this.$buefy.toast.open({ this.showMessage(message, "is-success");
message, }
type: "is-success",
}); showWarning(message: string): void {
this.showMessage(message, "is-warning");
}
private showMessage(
message: string,
type: "is-success" | "is-danger" | "is-warning"
): void {
this.$buefy.toast.open({ message, type });
} }
} }

View file

@ -29,7 +29,7 @@ export default class Home extends mixins(NotifyMixin) {
public async created(): Promise<void> { public async created(): Promise<void> {
if (this.$route.name === "confirm") { if (this.$route.name === "confirm") {
await this.doConfirm(); await this.doConfirm();
} else if (!this.user.isAuthenticated) { } else if (!this.user.isAuthenticated && this.$route.name !== "login") {
this.$router.push({ name: "login" }); this.$router.push({ name: "login" });
} }
} }

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.20 on 2021-07-29 10:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('userausfall', '0006_auto_20210521_0805'),
]
operations = [
migrations.AddField(
model_name='user',
name='email_unconfirmed',
field=models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address'),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 2.2.20 on 2021-08-02 07:44
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('userausfall', '0007_user_email_unconfirmed'),
]
operations = [
migrations.AlterField(
model_name='user',
name='confidant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confidants', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='user',
name='confidant_unconfirmed',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='unconfirmed_confidants', to=settings.AUTH_USER_MODEL),
),
]

View file

@ -0,0 +1,29 @@
# Generated by Django 2.2.20 on 2021-08-02 07:45
import django.contrib.auth.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('userausfall', '0008_auto_20210802_0744'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(blank=True, max_length=254, verbose_name='email address'),
),
migrations.AlterField(
model_name='user',
name='email_unconfirmed',
field=models.EmailField(blank=True, max_length=254, verbose_name='email address'),
),
migrations.AlterField(
model_name='user',
name='username',
field=models.CharField(blank=True, error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
),
]

View file

@ -24,21 +24,21 @@ class PasswordMismatch(Exception):
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
use_in_migrations = True use_in_migrations = True
def _create_user(self, email, password, **extra_fields): def _create_user(self, username, password, **extra_fields):
""" """
Create and save a user with the given username, email, and password. Create and save a user with the given username, email, and password.
""" """
email = self.normalize_email(email) # email = self.normalize_email(email)
# TODO: username = self.model.normalize_username(username) username = self.model.normalize_username(username)
user = self.model(email=email, **extra_fields) user = self.model(username=username, **extra_fields)
user.set_password(password) user.set_password(password)
user.save(using=self._db) user.save(using=self._db)
return user return user
def create_user(self, email, password=None, **extra_fields): def create_user(self, username, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False) extra_fields.setdefault("is_superuser", False)
return self._create_user(email, password, **extra_fields) return self._create_user(username, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields): def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_staff", True)
@ -63,9 +63,11 @@ class User(AbstractBaseUser, PermissionsMixin):
), ),
validators=[username_validator], validators=[username_validator],
error_messages={"unique": _("A user with that username already exists.")}, error_messages={"unique": _("A user with that username already exists.")},
unique=True,
blank=True, blank=True,
) )
email = models.EmailField(_("email address"), unique=True, blank=True) email = models.EmailField(_("email address"), blank=True)
email_unconfirmed = models.EmailField(_("email address"), blank=True)
is_staff = models.BooleanField( is_staff = models.BooleanField(
_("staff status"), _("staff status"),
default=False, default=False,
@ -73,19 +75,20 @@ class User(AbstractBaseUser, PermissionsMixin):
) )
date_joined = models.DateTimeField(_("date joined"), default=timezone.now) date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
confidant = models.ForeignKey( confidant = models.ForeignKey(
"User", on_delete=models.SET_NULL, null=True, related_name="confidants" "User", on_delete=models.SET_NULL, null=True, blank=True, related_name="confidants"
) )
confidant_unconfirmed = models.ForeignKey( confidant_unconfirmed = models.ForeignKey(
"User", "User",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True,
related_name="unconfirmed_confidants", related_name="unconfirmed_confidants",
) )
objects = UserManager() objects = UserManager()
EMAIL_FIELD = "email" EMAIL_FIELD = "email"
USERNAME_FIELD = "email" USERNAME_FIELD = "username"
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
class Meta: class Meta:

View file

@ -0,0 +1,11 @@
from rest_framework import permissions
class UserPermission(permissions.BasePermission):
def has_permission(self, request, view):
if request.method == "POST":
return True
return False
def has_object_permission(self, request, view, obj):
return False

View file

@ -13,9 +13,19 @@ class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ("pk", "email", "username", "confidant_email") fields = ("pk", "email", "username", "confidant_email")
read_only_fields = ("email",)
def update(self, instance: User, validated_data): def update(self, instance: User, validated_data):
confidant_email = validated_data.pop("confidant_email") confidant_email = validated_data.pop("confidant_email")
confidant, _ = User.objects.get_or_create(email=confidant_email) confidant, _ = User.objects.get_or_create(email=confidant_email)
instance.confidant_unconfirmed = confidant instance.confidant_unconfirmed = confidant
return super().update(instance, validated_data) return super().update(instance, validated_data)
class CreateUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("username", "password")
def create(self, validated_data):
return User.objects.create_user(**validated_data)

View file

@ -5,7 +5,8 @@ from rest_framework.response import Response
from djeveric import ConfirmationView from djeveric import ConfirmationView
from userausfall.models import User, MissingUserAttribute, PasswordMismatch from userausfall.models import User, MissingUserAttribute, PasswordMismatch
from userausfall.confirmations import ConfidantConfirmation from userausfall.confirmations import ConfidantConfirmation
from userausfall.rest_api.serializers import UserSerializer, UserActivationSerializer from userausfall.rest_api.permissions import UserPermission
from userausfall.rest_api.serializers import UserSerializer, UserActivationSerializer, CreateUserSerializer
class ConfidantConfirmationView(ConfirmationView): class ConfidantConfirmationView(ConfirmationView):
@ -13,8 +14,8 @@ class ConfidantConfirmationView(ConfirmationView):
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):
permission_classes = [UserPermission]
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer
@action(detail=True, methods=["post"]) @action(detail=True, methods=["post"])
def activate(self, request, pk=None): def activate(self, request, pk=None):
@ -29,3 +30,7 @@ class UserViewSet(viewsets.ModelViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
else: else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get_serializer_class(self):
if self.action == "create":
return CreateUserSerializer