Compare commits
17 commits
Author | SHA1 | Date | |
---|---|---|---|
bfb1076b13 | |||
8ca0f66777 | |||
bba1d7c8aa | |||
9449f7e665 | |||
330e3c89ea | |||
1dfc868345 | |||
0d3e1e3891 | |||
2ebb0b7ee4 | |||
818b19cf6b | |||
b4c686bfaa | |||
a887be1368 | |||
ceb032dddb | |||
79b7bc8364 | |||
96f4a81a3b | |||
5206f95e3f | |||
517f79c9f9 | |||
9c763c4b3b |
42 changed files with 877 additions and 624 deletions
17
.editorconfig
Normal file
17
.editorconfig
Normal file
|
@ -0,0 +1,17 @@
|
|||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.py]
|
||||
indent_size = 4
|
||||
|
||||
[{Makefile,debian/rules,make.d/*}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
46
.gitignore
vendored
46
.gitignore
vendored
|
@ -1,35 +1,27 @@
|
|||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
__pycache__/
|
||||
|
||||
/data/*
|
||||
!/data/.gitkeep
|
||||
/db.sqlite3
|
||||
/venv/
|
||||
/media/
|
||||
|
||||
/dist/
|
||||
*.egg-info/
|
||||
/build/
|
||||
/.pybuild/
|
||||
/.tox/
|
||||
.coverage*
|
||||
/debian/*debhelper*
|
||||
/debian/*.substvars
|
||||
/debian/files
|
||||
/debian/python3-userausfall/
|
||||
/debian/userausfall/
|
||||
/debian/userausfall-webapp/
|
||||
/.pybuild/
|
||||
/build/
|
||||
/userausfall.egg-info/
|
||||
|
||||
node_modules/
|
||||
app/dist/
|
||||
npm-debug.log*
|
||||
|
||||
.idea/
|
||||
*.env*
|
||||
*.swp
|
||||
|
|
55
README.md
55
README.md
|
@ -1,36 +1,31 @@
|
|||
# userausfall
|
||||
# Userausfall
|
||||
|
||||
## Backend: Development server
|
||||
User account management for systemausfall.org
|
||||
|
||||
## Quick Development Setup
|
||||
|
||||
Bootstrap your venv and project (you’ll need to do this only once):
|
||||
|
||||
```shell
|
||||
export DJANGO_SETTINGS_MODULE=userausfall.settings USERAUSFALL_SECRET_KEY=dev
|
||||
# Create a virtual environment
|
||||
python3 -m venv --system-site-packages venv
|
||||
# Activate your venv
|
||||
. venv/bin/activate
|
||||
# Install dependencies
|
||||
pip install --editable .
|
||||
```
|
||||
|
||||
In the future just run:
|
||||
```shell
|
||||
# Activate your venv
|
||||
. venv/bin/activate
|
||||
# Configure the settings
|
||||
export DJANGO_SETTINGS_MODULE=userausfall.settings
|
||||
# Apply database migrations
|
||||
python3 -m django migrate
|
||||
```
|
||||
|
||||
Start the API development server with:
|
||||
```shell
|
||||
python3 -m django runserver
|
||||
```
|
||||
|
||||
## Frontend: Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
||||
## Credits
|
||||
|
||||
* Parts of initial code are based on [schnipsel](https://git.hack-hro.de/kmohrf/schnipsel).
|
1
debian/control
vendored
1
debian/control
vendored
|
@ -31,6 +31,7 @@ Depends:
|
|||
${python3:Depends},
|
||||
python3-django (>= 2.2),
|
||||
python3-django-imagekit,
|
||||
python3-ldap3,
|
||||
# python3-djangorestframework,
|
||||
# python3-djoser (>= 2.1),
|
||||
Description: Python backend for the userausfall web application
|
||||
|
|
|
@ -1,154 +0,0 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
def encode_pk(resource):
|
||||
"""Encode the primary key of a resource with Base64 for usage in URLs."""
|
||||
return urlsafe_base64_encode(force_bytes(resource.pk))
|
||||
|
||||
|
||||
class ConfirmationError(Exception):
|
||||
"""An error occurring while checking a confirmation."""
|
||||
|
||||
def __init__(self, message=None, errors=None):
|
||||
if errors is None:
|
||||
self.errors = {}
|
||||
else:
|
||||
self.errors = errors
|
||||
if message is not None:
|
||||
non_field_errors = self.errors.get(api_settings.NON_FIELD_ERRORS_KEY, [])
|
||||
non_field_errors.append(message)
|
||||
self.errors[api_settings.NON_FIELD_ERRORS_KEY] = non_field_errors
|
||||
|
||||
|
||||
class Email:
|
||||
"""
|
||||
Base class for an email message.
|
||||
"""
|
||||
subject = ""
|
||||
|
||||
def __init__(self, user):
|
||||
self.user = user
|
||||
|
||||
def get_subject(self):
|
||||
return self.subject
|
||||
|
||||
def get_message(self, context):
|
||||
raise NotImplementedError()
|
||||
|
||||
def send(self, context):
|
||||
self.user.email_user(self.get_subject(), self.get_message(context))
|
||||
|
||||
|
||||
class Confirmation:
|
||||
"""
|
||||
Base class for handling a confirmation process.
|
||||
"""
|
||||
email_class = Email
|
||||
|
||||
def __init__(self, user, resource):
|
||||
self.user = user
|
||||
self.resource = resource
|
||||
|
||||
def check(self):
|
||||
if not self.has_permission(self.user, self.resource):
|
||||
raise ConfirmationError("Permission denied")
|
||||
if self.is_confirmed(self.resource):
|
||||
raise ConfirmationError("Already confirmed")
|
||||
self.confirm(self.resource)
|
||||
|
||||
def confirm(self, resource):
|
||||
"""Overwrite this method to supply operations to confirm the resource."""
|
||||
pass
|
||||
|
||||
def get_email(self):
|
||||
return self.email_class(self.user)
|
||||
|
||||
def has_permission(self, user, resource) -> bool:
|
||||
"""Overwrite this method returning if a user may confirm a resource."""
|
||||
return False
|
||||
|
||||
def is_confirmed(self, resource) -> bool:
|
||||
"""Overwrite this method to tell if a resource is confirmed."""
|
||||
return False
|
||||
|
||||
def send_request(self):
|
||||
if self.is_confirmed(self.resource):
|
||||
return
|
||||
self.get_email().send({
|
||||
"token": default_token_generator.make_token(self.user),
|
||||
"uid": encode_pk(self.user),
|
||||
"rtid": encode_pk(ContentType.objects.get_for_model(self.resource)),
|
||||
"rid": encode_pk(self.resource),
|
||||
})
|
||||
|
||||
|
||||
class ConfirmationSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer class for confirmation requests.
|
||||
"""
|
||||
token = serializers.CharField()
|
||||
uid = serializers.CharField()
|
||||
rtid = serializers.CharField()
|
||||
rid = serializers.CharField()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.user = None
|
||||
self.resource_type = None
|
||||
self.resource = None
|
||||
|
||||
def validate_uid(self, value):
|
||||
self.user = self.get_model_object(get_user_model(), value)
|
||||
return value
|
||||
|
||||
def validate_rtid(self, value):
|
||||
self.resource_type = self.get_model_object(ContentType, value)
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
# we need to be sure that the rtid was already decoded
|
||||
self.resource = self.get_model_object(self.resource_type.model_class(), data["rid"])
|
||||
return data
|
||||
|
||||
def get_model_object(self, model, oid):
|
||||
try:
|
||||
# urlsafe_base64_decode() decodes to bytestring
|
||||
oid = urlsafe_base64_decode(oid).decode()
|
||||
return model._default_manager.get(id=oid)
|
||||
except (TypeError, ValueError, OverflowError, model.DoesNotExist):
|
||||
raise ValidationError("Error while decoding object id")
|
||||
|
||||
|
||||
class ConfirmationView(APIView):
|
||||
"""
|
||||
View for creating a confirmation API endpoint.
|
||||
"""
|
||||
confirmation_class = Confirmation
|
||||
serializer_class = ConfirmationSerializer
|
||||
|
||||
def post(self, request, format=None):
|
||||
try:
|
||||
self.check_confirmation(request.data)
|
||||
return Response({}, status=status.HTTP_204_NO_CONTENT)
|
||||
except ConfirmationError as e:
|
||||
return Response(e.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def check_confirmation(self, data):
|
||||
serializer = self.serializer_class(data=data)
|
||||
if not serializer.is_valid():
|
||||
raise ConfirmationError(errors=serializer.errors)
|
||||
token = serializer.validated_data["token"]
|
||||
if default_token_generator.check_token(serializer.user, token):
|
||||
# confirm resource or raise an exception otherwise
|
||||
self.confirmation_class(serializer.user, serializer.resource).check()
|
||||
else:
|
||||
raise ConfirmationError("Invalid token")
|
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ['py39']
|
||||
exclude = '''
|
||||
(
|
||||
/(
|
||||
\.eggs
|
||||
| \.git
|
||||
| \.tox
|
||||
| migrations
|
||||
)
|
||||
)
|
||||
'''
|
|
@ -1,3 +0,0 @@
|
|||
setuptools~=40.8.0
|
||||
django~=2.2.13
|
||||
djangorestframework~=3.9.0
|
7
setup.cfg
Normal file
7
setup.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[flake8]
|
||||
max-line-length = 120
|
||||
select = C,E,F,I,W,B,B950
|
||||
ignore = E203, E501, W503
|
||||
exclude = .tox, node_modules, src, **/migrations/*.py
|
||||
import-order-style = google
|
||||
application-import-names = userausfall
|
44
setup.py
44
setup.py
|
@ -1,19 +1,45 @@
|
|||
from setuptools import setup, find_packages
|
||||
import os
|
||||
|
||||
from setuptools import find_namespace_packages, setup
|
||||
|
||||
from userausfall import __version__
|
||||
|
||||
__dir__ = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
try:
|
||||
with open(os.path.join(__dir__, "README.md")) as f:
|
||||
long_description = "\n" + f.read()
|
||||
except FileNotFoundError:
|
||||
long_description = ""
|
||||
|
||||
setup(
|
||||
name="userausfall",
|
||||
version=__version__,
|
||||
description="account management for systemausfall.org",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://git.systemausfall.org/systemausfall.org/userausfall",
|
||||
author="Robert Waltemath",
|
||||
author_email="rw@roko.li",
|
||||
packages=find_packages(),
|
||||
install_requires=(
|
||||
"django>=2.2<3.0",
|
||||
# "djangorestframework>=3.12<4.0",
|
||||
# "djoser>=2.1<3.0",
|
||||
),
|
||||
author="userausfall developers",
|
||||
author_email="hallo@roko.li",
|
||||
license="AGPL-3.0-or-later",
|
||||
packages=find_namespace_packages(include=["userausfall"]),
|
||||
install_requires=[
|
||||
"django~=3.2.8",
|
||||
"djangorestframework~=3.12.1",
|
||||
"djangorestframework-camel-case~=1.2.0",
|
||||
"django-filter~=2.4.0",
|
||||
"django-rest-registration~=0.6.4",
|
||||
"djeveric@https://git.hack-hro.de/memoorje/djeveric/-/archive/main/djeveric-main.tar.gz",
|
||||
"drf-spectacular~=0.18.2",
|
||||
"ldap3~=2.8.1",
|
||||
],
|
||||
include_package_data=True,
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Framework :: Django :: 3.2",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
],
|
||||
)
|
||||
|
|
17
src/App.vue
17
src/App.vue
|
@ -1,16 +1,25 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<Navbar />
|
||||
<navbar />
|
||||
<section class="container">
|
||||
<div class="columns is-centered">
|
||||
<router-view />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import Navbar from "@/components/Navbar.vue";
|
||||
import { user } from "@/api";
|
||||
|
||||
@Component({ components: { Navbar } })
|
||||
export default class App extends Vue {}
|
||||
export default class App extends Vue {
|
||||
public async created(): Promise<void> {
|
||||
if (!user.isAuthenticated && this.$route.name !== "login") {
|
||||
this.$router.push({ name: "login" });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Cookies from "js-cookie";
|
||||
import Vue from "vue";
|
||||
|
||||
type HTTPMethod = "GET" | "POST" | "PUT" | "PATCH";
|
||||
|
||||
|
@ -95,3 +96,5 @@ export class User {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const user = Vue.observable(new User());
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
<template>
|
||||
<div style="display: flex">
|
||||
<b-input v-if="isEditing" v-model="editorValue" />
|
||||
<span v-else>{{ value }}</span>
|
||||
<b-button @click="toggleEditor" style="margin-left: auto">{{
|
||||
isEditing ? "Speichern" : "Bearbeiten"
|
||||
}}</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class InlineEditor extends Vue {
|
||||
@Prop() private value: string | undefined;
|
||||
|
||||
private editorValue = "";
|
||||
private isEditing = false;
|
||||
|
||||
private created() {
|
||||
this.editorValue = this.value || "";
|
||||
}
|
||||
|
||||
private toggleEditor() {
|
||||
this.isEditing = !this.isEditing;
|
||||
this.$emit("input", this.editorValue);
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,74 +0,0 @@
|
|||
<template>
|
||||
<form class="box" @submit.prevent="doAction">
|
||||
<div v-if="errorMessage" class="notification is-danger">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<b-field label="Benutzername">
|
||||
<b-input type="text" v-model="user.username" />
|
||||
</b-field>
|
||||
<b-field label="Kennwort">
|
||||
<b-input type="password" v-model="user.password" />
|
||||
</b-field>
|
||||
<b-field v-if="mode === 'signup'" label="Kennwort wiederholen">
|
||||
<b-input type="password" v-model="password2" />
|
||||
</b-field>
|
||||
|
||||
<div class="buttons">
|
||||
<b-button native-type="submit" type="is-primary">
|
||||
{{ mode === "login" ? "Anmelden" : "Konto anlegen" }}
|
||||
</b-button>
|
||||
<router-link v-if="mode === 'login'" to="/signup"
|
||||
>Konto anlegen</router-link
|
||||
>
|
||||
<router-link v-else to="/login">Anmelden</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||
import { mixins } from "vue-class-component";
|
||||
import { User } from "@/api";
|
||||
import { Route } from "vue-router";
|
||||
import { NotifyMixin } from "@/mixins";
|
||||
|
||||
@Component
|
||||
export default class LoginForm extends mixins(NotifyMixin) {
|
||||
@Prop() private user!: User;
|
||||
|
||||
private mode: "login" | "signup" = "login";
|
||||
private password2 = "";
|
||||
private errorMessage = "";
|
||||
|
||||
public created(): void {
|
||||
if (this.$route.name === "signup") this.mode = "signup";
|
||||
}
|
||||
|
||||
@Watch("$route")
|
||||
public routeChanged(to: Route): void {
|
||||
if (to.name === "signup") {
|
||||
this.mode = "signup";
|
||||
} else {
|
||||
this.mode = "login";
|
||||
}
|
||||
}
|
||||
|
||||
private async doAction() {
|
||||
try {
|
||||
if (this.mode === "login") {
|
||||
await this.user.login();
|
||||
if (this.$route.name !== "home") this.$router.push({ name: "home" });
|
||||
} else {
|
||||
await this.user.signup();
|
||||
this.$router.push({ name: "login" });
|
||||
this.showSuccess("Du kannst dich nun anmelden.");
|
||||
}
|
||||
} catch {
|
||||
this.errorMessage = "Fehler";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
|
@ -1,29 +1,25 @@
|
|||
import Vue from "vue";
|
||||
import VueRouter, { RouteConfig } from "vue-router";
|
||||
import MainPage from "../views/MainPage.vue";
|
||||
import LoginView from "../views/Login.vue";
|
||||
import UserView from "../views/User.vue";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const routes: Array<RouteConfig> = [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: MainPage,
|
||||
name: "index",
|
||||
component: UserView,
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: MainPage,
|
||||
component: LoginView,
|
||||
},
|
||||
{
|
||||
path: "/signup",
|
||||
name: "signup",
|
||||
component: MainPage,
|
||||
},
|
||||
{
|
||||
path: "/confirm/:uid/:token",
|
||||
name: "confirm",
|
||||
component: MainPage,
|
||||
component: LoginView,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
73
src/views/Login.vue
Normal file
73
src/views/Login.vue
Normal file
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div class="column is-3-widescreen is-4-desktop is-5-tablet">
|
||||
<form class="box" @submit.prevent="doAction">
|
||||
<div v-if="errorMessage" class="notification is-danger">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<b-field label="Benutzername">
|
||||
<b-input type="text" v-model="user.username" />
|
||||
</b-field>
|
||||
<b-field label="Kennwort">
|
||||
<b-input type="password" v-model="user.password" />
|
||||
</b-field>
|
||||
<b-field v-if="mode === 'signup'" label="Kennwort wiederholen">
|
||||
<b-input type="password" v-model="password2" />
|
||||
</b-field>
|
||||
|
||||
<div class="buttons">
|
||||
<b-button native-type="submit" type="is-primary">
|
||||
{{ mode === "login" ? "Anmelden" : "Konto anlegen" }}
|
||||
</b-button>
|
||||
<router-link v-if="mode === 'login'" to="/signup"
|
||||
>Konto anlegen</router-link
|
||||
>
|
||||
<router-link v-else to="/login">Anmelden</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Watch } from "vue-property-decorator";
|
||||
import { user } from "@/api";
|
||||
import { NotifyMixin } from "@/mixins";
|
||||
import { mixins } from "vue-class-component";
|
||||
import { Route } from "vue-router";
|
||||
|
||||
@Component
|
||||
export default class Login extends mixins(NotifyMixin) {
|
||||
user = user;
|
||||
private mode: "login" | "signup" = "login";
|
||||
private password2 = "";
|
||||
private errorMessage = "";
|
||||
|
||||
public created(): void {
|
||||
if (this.$route.name === "signup") this.mode = "signup";
|
||||
}
|
||||
|
||||
@Watch("$route")
|
||||
public routeChanged(to: Route): void {
|
||||
if (to.name === "signup") {
|
||||
this.mode = "signup";
|
||||
} else {
|
||||
this.mode = "login";
|
||||
}
|
||||
}
|
||||
|
||||
private async doAction() {
|
||||
try {
|
||||
if (this.mode === "login") {
|
||||
await this.user.login();
|
||||
if (this.$route.name !== "home") this.$router.push({ name: "home" });
|
||||
} else {
|
||||
await this.user.signup();
|
||||
this.$router.push({ name: "login" });
|
||||
this.showSuccess("Du kannst dich nun anmelden.");
|
||||
}
|
||||
} catch {
|
||||
this.errorMessage = "Fehler";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,35 +0,0 @@
|
|||
<template>
|
||||
<section class="container">
|
||||
<div class="columns is-centered">
|
||||
<div
|
||||
v-if="user.isAuthenticated"
|
||||
class="column is-5-fullhd is-6-widescreen is-7-desktop is-7-tablet"
|
||||
>
|
||||
<UserTable :user="user" />
|
||||
</div>
|
||||
<div v-else class="column is-3-widescreen is-4-desktop is-5-tablet">
|
||||
<LoginForm :user="user" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from "vue-property-decorator";
|
||||
import LoginForm from "@/components/LoginForm.vue";
|
||||
import { mixins } from "vue-class-component";
|
||||
import { User } from "@/api";
|
||||
import UserTable from "@/components/UserTable.vue";
|
||||
import { NotifyMixin } from "@/mixins";
|
||||
|
||||
@Component({ components: { UserTable, LoginForm } })
|
||||
export default class Home extends mixins(NotifyMixin) {
|
||||
private user = new User();
|
||||
|
||||
public async created(): Promise<void> {
|
||||
if (!this.user.isAuthenticated && this.$route.name !== "login") {
|
||||
this.$router.push({ name: "login" });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="column is-5-fullhd is-6-widescreen is-7-desktop is-7-tablet">
|
||||
<b-notification type="is-info" aria-close-label="Close notification">
|
||||
Dein Konto ist noch nicht aktiv.
|
||||
<a v-if="user.isTrusted" @click="activate()">Jetzt aktivieren</a>
|
||||
|
@ -12,15 +12,11 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td>Benutzername</td>
|
||||
<td>
|
||||
<InlineEditor v-model="user.username" @input="user.save()" />
|
||||
</td>
|
||||
<td>{{ user.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vertrauensperson</td>
|
||||
<td>
|
||||
<inline-editor v-model="user.confidantEmail" @input="user.save()" />
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -28,15 +24,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { User } from "@/api";
|
||||
import InlineEditor from "@/components/InlineEditor.vue";
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { user } from "@/api";
|
||||
|
||||
@Component({
|
||||
components: { InlineEditor },
|
||||
})
|
||||
export default class UserTable extends Vue {
|
||||
@Prop() private user!: User;
|
||||
@Component
|
||||
export default class Home extends Vue {
|
||||
user = user;
|
||||
|
||||
async activate(): Promise<void> {
|
||||
await this.user.activate();
|
25
tox.ini
Normal file
25
tox.ini
Normal file
|
@ -0,0 +1,25 @@
|
|||
[tox]
|
||||
envlist = lint, test-py3-app
|
||||
skip_missing_interpreters = true
|
||||
minversion = 3.21.0
|
||||
|
||||
[testenv:lint]
|
||||
# no need to install package with deps to lint sources
|
||||
skip_install = true
|
||||
deps =
|
||||
black
|
||||
flake8
|
||||
flake8-import-order
|
||||
setenv =
|
||||
FORMAT_PATHS = userausfall{/} setup.py
|
||||
commands =
|
||||
python3 -m flake8 {env:FORMAT_PATHS}
|
||||
python3 -m black --check {env:FORMAT_PATHS}
|
||||
|
||||
[testenv:test-py3-app]
|
||||
sitepackages = true
|
||||
deps =
|
||||
coverage
|
||||
setenv = DJANGO_SETTINGS_MODULE=userausfall.settings
|
||||
commands =
|
||||
python3 -m coverage run --append --source='userausfall' -m django test --verbosity=2
|
|
@ -1,3 +1 @@
|
|||
__version__ = "0.1.0"
|
||||
|
||||
default_app_config = 'userausfall.apps.UserausfallConfig'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from userausfall.models import User, TrustBridge
|
||||
from userausfall.models import TrustBridge, User
|
||||
|
||||
admin.site.register(TrustBridge)
|
||||
admin.site.register(User)
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UserausfallConfig(AppConfig):
|
||||
name = 'userausfall'
|
||||
|
||||
def ready(self):
|
||||
from . import signals
|
|
@ -1,17 +0,0 @@
|
|||
from djeveric import Confirmation
|
||||
from userausfall.emails import ConfidantConfirmationEmail
|
||||
from userausfall.models import User
|
||||
|
||||
|
||||
class ConfidantConfirmation(Confirmation):
|
||||
email_class = ConfidantConfirmationEmail
|
||||
|
||||
def has_permission(self, user: User, resource: User):
|
||||
return user == resource.confidant_unconfirmed
|
||||
|
||||
def is_confirmed(self, resource: User):
|
||||
return resource.confidant_unconfirmed == resource.confidant
|
||||
|
||||
def confirm(self, resource: User):
|
||||
resource.confidant = resource.confidant_unconfirmed
|
||||
resource.save()
|
|
@ -1,8 +1,8 @@
|
|||
from djeveric import Email
|
||||
from djeveric.emails import ConfirmationEmail
|
||||
|
||||
|
||||
class ConfidantConfirmationEmail(Email):
|
||||
class TrustBridgeConfirmationEmail(ConfirmationEmail):
|
||||
subject = "TODO"
|
||||
|
||||
def get_message(self, context):
|
||||
return '"token": "{token}", "uid": "{uid}", "rtid": "{rtid}", "rid": "{rid}"'.format(**context)
|
||||
def get_body(self, context: dict[str]) -> str:
|
||||
return "{token}".format(**context)
|
||||
|
|
|
@ -1,21 +1,62 @@
|
|||
from django.conf import settings
|
||||
from ldap3 import Server, Connection, SYNC
|
||||
from ldap3 import Connection, MOCK_SYNC, SAFE_SYNC, Server
|
||||
|
||||
_test_connection = None
|
||||
|
||||
|
||||
def create_account(username, raw_password):
|
||||
server = Server("localhost")
|
||||
# The SAFE_SYNC client strategy doesn't seem to be present in Buster version of ldap3. We might want to use it as
|
||||
# soon as it is available (multithreading).
|
||||
connection = Connection(
|
||||
server,
|
||||
settings.USERAUSFALL_LDAP["ADMIN_USER_DN"],
|
||||
settings.USERAUSFALL_LDAP["ADMIN_USER_PASSWORD"],
|
||||
client_strategy=SYNC,
|
||||
auto_bind=True,
|
||||
)
|
||||
is_success = connection.add(
|
||||
class LDAPManager:
|
||||
def __init__(self):
|
||||
if not getattr(settings, "USERAUSFALL_LDAP_IS_TEST", False):
|
||||
self.connection = self._get_connection()
|
||||
else:
|
||||
self.connection = self._get_test_connection()
|
||||
|
||||
def create_account(self, username, raw_password):
|
||||
is_success = self.connection.add(
|
||||
f"cn={username},dc=local",
|
||||
["simpleSecurityObject", "organizationalRole"],
|
||||
{"userPassword": raw_password},
|
||||
)
|
||||
return is_success
|
||||
|
||||
def has_account(self, username):
|
||||
exists = self.connection.search(f"cn={username},dc=local", "(objectclass=simpleSecurityObject)")
|
||||
return exists
|
||||
|
||||
def is_valid_account_data(self, username, raw_password):
|
||||
is_valid = self.connection.search(
|
||||
f"cn={username},dc=local",
|
||||
"(objectclass=simpleSecurityObject)",
|
||||
attributes=["userPassword"],
|
||||
)
|
||||
if is_valid:
|
||||
is_valid = self.connection.entries[0]["userPassword"].value == raw_password
|
||||
return is_valid
|
||||
|
||||
def drop_test_connection(self):
|
||||
global _test_connection
|
||||
self.connection.unbind()
|
||||
self.connection = None
|
||||
_test_connection = None
|
||||
|
||||
def _get_connection(self):
|
||||
server = Server("localhost")
|
||||
connection = Connection(
|
||||
server,
|
||||
settings.USERAUSFALL_LDAP["ADMIN_USER_DN"],
|
||||
settings.USERAUSFALL_LDAP["ADMIN_USER_PASSWORD"],
|
||||
client_strategy=SAFE_SYNC,
|
||||
auto_bind=True,
|
||||
)
|
||||
return connection
|
||||
|
||||
def _get_test_connection(self):
|
||||
global _test_connection
|
||||
if _test_connection is None:
|
||||
server = Server("testserver")
|
||||
_test_connection = Connection(
|
||||
server, user="cn=admin,dc=local", password="admin_secret", client_strategy=MOCK_SYNC
|
||||
)
|
||||
_test_connection.strategy.add_entry("cn=admin,dc=local", {"userPassword": "admin_secret"})
|
||||
_test_connection.bind()
|
||||
return _test_connection
|
||||
|
|
28
userausfall/migrations/0012_auto_20211021_0901.py
Normal file
28
userausfall/migrations/0012_auto_20211021_0901.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.2.8 on 2021-10-21 09:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('userausfall', '0011_trustbridge'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='trustbridge',
|
||||
old_name='user',
|
||||
new_name='trust_taker',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trustbridge',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
20
userausfall/migrations/0013_trustbridge_trust_giver.py
Normal file
20
userausfall/migrations/0013_trustbridge_trust_giver.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.2.8 on 2021-10-21 09:04
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('userausfall', '0012_auto_20211021_0901'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trustbridge',
|
||||
name='trust_giver',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
|
@ -1,18 +1,17 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
|
||||
from django.contrib.auth.models import PermissionsMixin
|
||||
from django.contrib.auth.validators import UnicodeUsernameValidator
|
||||
from django.core.mail import send_mail
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from djeveric.fields import ConfirmationField
|
||||
from djeveric.models import ConfirmableModelMixin
|
||||
|
||||
from userausfall import ldap
|
||||
|
||||
|
||||
class MissingUserAttribute(Exception):
|
||||
"""The user object is missing a required attribute."""
|
||||
|
||||
pass
|
||||
from userausfall.emails import TrustBridgeConfirmationEmail
|
||||
from userausfall.ldap import LDAPManager
|
||||
|
||||
|
||||
class PasswordMismatch(Exception):
|
||||
|
@ -52,15 +51,13 @@ class UserManager(BaseUserManager):
|
|||
return self._create_user(email, password, **extra_fields)
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
class User(PermissionsMixin, AbstractBaseUser):
|
||||
username_validator = UnicodeUsernameValidator()
|
||||
|
||||
username = models.CharField(
|
||||
_("username"),
|
||||
max_length=150,
|
||||
help_text=_(
|
||||
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||
),
|
||||
help_text=_("Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."),
|
||||
validators=[username_validator],
|
||||
error_messages={"unique": _("A user with that username already exists.")},
|
||||
unique=True,
|
||||
|
@ -87,21 +84,36 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||
super().clean()
|
||||
self.email = self.__class__.objects.normalize_email(self.email)
|
||||
|
||||
def create_ldap_account(self, raw_password):
|
||||
"""Create the LDAP account which corresponds to this user."""
|
||||
if not self.check_password(raw_password):
|
||||
raise PasswordMismatch("The given password does not match the user's password.")
|
||||
return self._ldap.create_account(self.username, raw_password)
|
||||
|
||||
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||
"""Send an email to this user."""
|
||||
send_mail(subject, message, from_email, [self.email], **kwargs)
|
||||
|
||||
def create_ldap_account(self, raw_password):
|
||||
"""Create the LDAP account which corresponds to this user."""
|
||||
if not self.username:
|
||||
raise MissingUserAttribute("User is missing a username.")
|
||||
if not self.check_password(raw_password):
|
||||
raise PasswordMismatch(
|
||||
"The given password does not match the user's password."
|
||||
)
|
||||
return ldap.create_account(self.username, raw_password)
|
||||
def get_primary_email(self):
|
||||
"""Returns the primary email address for this user."""
|
||||
return f"{self.username}@{settings.USERAUSFALL['PRIMARY_EMAIL_DOMAIN']}"
|
||||
|
||||
def has_ldap_account(self):
|
||||
"""Returns True if an ldap account exists for the user's username."""
|
||||
return self._ldap.has_account(self.username)
|
||||
|
||||
@cached_property
|
||||
def _ldap(self):
|
||||
return LDAPManager()
|
||||
|
||||
|
||||
class TrustBridge(models.Model):
|
||||
user = models.OneToOneField("User", on_delete=models.CASCADE, related_name="trust_bridge")
|
||||
is_trusted = models.BooleanField(default=False)
|
||||
class TrustBridge(ConfirmableModelMixin, models.Model):
|
||||
is_trusted = ConfirmationField(email_class=TrustBridgeConfirmationEmail)
|
||||
trust_giver = models.ForeignKey("User", on_delete=models.SET_NULL, null=True)
|
||||
trust_taker = models.OneToOneField("User", on_delete=models.CASCADE, related_name="trust_bridge")
|
||||
|
||||
def get_confirmation_email_recipient(self) -> str:
|
||||
return self.trust_giver.get_primary_email()
|
||||
|
||||
def _has_confirmation_recipient(self):
|
||||
return self.trust_giver is not None
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
from rest_framework import permissions
|
||||
|
||||
|
||||
class UserPermission(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
result = False
|
||||
if view.action == "activate":
|
||||
result = True
|
||||
elif view.action == "create":
|
||||
result = True
|
||||
elif view.action == "retrieve_authenticated":
|
||||
result = request.user.is_authenticated
|
||||
return result
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
result = False
|
||||
if view.action == "activate":
|
||||
result = request.user == obj
|
||||
return result
|
|
@ -1,30 +1,48 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from userausfall.models import User, TrustBridge
|
||||
from userausfall.models import TrustBridge, User
|
||||
from userausfall.views import get_authenticated_user
|
||||
|
||||
|
||||
class TrustBridgeSerializer(serializers.ModelSerializer):
|
||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "trust_bridge", "url"]
|
||||
|
||||
|
||||
class ActivateUserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "password", "url"]
|
||||
|
||||
def validate(self, data):
|
||||
if not hasattr(self.instance, "trust_bridge") or not self.instance.trust_bridge.is_trusted:
|
||||
raise serializers.ValidationError("User has no trusted trust bridge")
|
||||
return data
|
||||
|
||||
def validate_password(self, value):
|
||||
if not self.instance.check_password(value):
|
||||
raise serializers.ValidationError("Password does not match the user's password")
|
||||
return value
|
||||
|
||||
|
||||
class TrustBridgeUserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "url", "username"]
|
||||
# the UniqueValidator for username prevents successful validation for existing users
|
||||
extra_kwargs = {"username": {"validators": []}}
|
||||
|
||||
|
||||
class TrustBridgeSerializer(serializers.HyperlinkedModelSerializer):
|
||||
trust_giver = TrustBridgeUserSerializer()
|
||||
|
||||
class Meta:
|
||||
model = TrustBridge
|
||||
fields = ["is_trusted"]
|
||||
|
||||
|
||||
class ActivateUserSerializer(serializers.Serializer):
|
||||
password = serializers.CharField()
|
||||
|
||||
|
||||
class RetrieveUserSerializer(serializers.ModelSerializer):
|
||||
trust_bridge = TrustBridgeSerializer(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["pk", "username", "trust_bridge"]
|
||||
|
||||
|
||||
class CreateUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("username", "password")
|
||||
fields = ["id", "is_trusted", "trust_giver", "url"]
|
||||
read_only_fields = ["is_trusted"]
|
||||
|
||||
def create(self, validated_data):
|
||||
return User.objects.create_user(**validated_data)
|
||||
user = get_authenticated_user(self.context["request"])
|
||||
trust_giver, _ = User.objects.get_or_create(username=validated_data["trust_giver"]["username"])
|
||||
return TrustBridge.objects.create(trust_taker=user, trust_giver=trust_giver)
|
||||
|
|
3
userausfall/rest_api/tests/__init__.py
Normal file
3
userausfall/rest_api/tests/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .auth import AuthenticationTestCase # noqa: F401, F403
|
||||
from .trust_bridges import TrustBridgeTestCase # noqa: F401, F403
|
||||
from .users import UserTestCase # noqa: F401, F403
|
90
userausfall/rest_api/tests/auth.py
Normal file
90
userausfall/rest_api/tests/auth.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
from rest_framework import status
|
||||
|
||||
from userausfall.models import User
|
||||
from userausfall.rest_api.tests.userausfall import UserausfallAPITestCase
|
||||
|
||||
|
||||
class UserMixin:
|
||||
user: User
|
||||
password: str
|
||||
username: str
|
||||
|
||||
def create_user(self):
|
||||
self.username = f"test{User.objects.count()}"
|
||||
self.password = "test12345"
|
||||
self.user = User.objects.create_user(self.username, self.password)
|
||||
return self.user
|
||||
|
||||
def ensure_user_exists(self):
|
||||
if not hasattr(self, "user"):
|
||||
self.create_user()
|
||||
|
||||
def authenticate_user(self):
|
||||
self.ensure_user_exists()
|
||||
if hasattr(self.client, "force_authentication"):
|
||||
self.client.force_authenticate(user=self.user)
|
||||
else:
|
||||
self.client.force_login(user=self.user)
|
||||
|
||||
|
||||
class AuthenticationTestCase(UserMixin, UserausfallAPITestCase):
|
||||
base_url = "/api/auth"
|
||||
|
||||
def test_signup(self):
|
||||
"""
|
||||
Create a new user account (signup)
|
||||
"""
|
||||
url = "/register/"
|
||||
username = "test"
|
||||
password = "test12345"
|
||||
data = {"username": username, "password": password, "passwordConfirm": password}
|
||||
response = self.client.post(self.get_api_url(url), data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(User.objects.count(), 1)
|
||||
self.assertEqual(User.objects.get().username, username)
|
||||
|
||||
def test_login(self) -> None:
|
||||
"""
|
||||
Create a session cookie (login)
|
||||
"""
|
||||
url = "/login/"
|
||||
self.create_user()
|
||||
data = {"login": self.username, "password": self.password}
|
||||
response = self.client.post(self.get_api_url(url), data)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.wsgi_request.user.is_authenticated)
|
||||
|
||||
def test_logout(self) -> None:
|
||||
"""
|
||||
Remove the session (logout)
|
||||
"""
|
||||
url = "/logout/"
|
||||
self.authenticate_user()
|
||||
response = self.client.post(self.get_api_url(url))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertFalse(response.wsgi_request.user.is_authenticated)
|
||||
|
||||
def test_retrieve_user(self) -> None:
|
||||
"""
|
||||
Retrieve data of authenticated user
|
||||
"""
|
||||
url = "/profile/"
|
||||
self.create_user()
|
||||
self.authenticate_user()
|
||||
response = self.client.get(self.get_api_url(url))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(
|
||||
response.data,
|
||||
{
|
||||
"username": self.username,
|
||||
"id": self.user.id,
|
||||
},
|
||||
)
|
||||
|
||||
def test_create_two_users(self) -> None:
|
||||
"""
|
||||
Create more than one user
|
||||
"""
|
||||
self.create_user()
|
||||
self.create_user()
|
||||
self.assertEqual(User.objects.count(), 2)
|
77
userausfall/rest_api/tests/trust_bridges.py
Normal file
77
userausfall/rest_api/tests/trust_bridges.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from django.core import mail
|
||||
from rest_framework import status
|
||||
|
||||
from userausfall.models import TrustBridge, User
|
||||
from userausfall.rest_api.tests.auth import UserMixin
|
||||
from userausfall.rest_api.tests.userausfall import get_url, UserausfallAPITestCase
|
||||
|
||||
|
||||
class TrustBridgeMixin(UserMixin):
|
||||
trust_bridge: TrustBridge
|
||||
trust_giver: User
|
||||
|
||||
def create_trust_bridge(self, is_trusted=False):
|
||||
self.trust_giver = self.create_user()
|
||||
self.create_user()
|
||||
self.trust_bridge = TrustBridge.objects.create(
|
||||
trust_taker=self.user, trust_giver=self.trust_giver, is_trusted=is_trusted
|
||||
)
|
||||
return self.trust_bridge
|
||||
|
||||
|
||||
class TrustBridgeTestCase(TrustBridgeMixin, UserausfallAPITestCase):
|
||||
def test_create_trust_bridge(self):
|
||||
"""Create a trust bridge for the current user."""
|
||||
url = "/trust-bridges/"
|
||||
trust_giver = self.create_user()
|
||||
self.create_user()
|
||||
self.authenticate_user()
|
||||
response = self.client.post(
|
||||
self.get_api_url(url),
|
||||
{
|
||||
"trust_giver": {
|
||||
"username": trust_giver.username,
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(TrustBridge.objects.count(), 1)
|
||||
self.assertEqual(TrustBridge.objects.get().trust_giver, trust_giver)
|
||||
|
||||
def test_retrieve_trust_bridge(self):
|
||||
"""Retrieve the trust bridge information of a user without an ldap account."""
|
||||
url = "/trust-bridges/{pk}/"
|
||||
self.create_trust_bridge()
|
||||
self.authenticate_user()
|
||||
response = self.client.get(self.get_api_url(url, pk=self.trust_bridge.pk))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(
|
||||
response.data,
|
||||
{
|
||||
"id": self.trust_bridge.id,
|
||||
"is_trusted": False,
|
||||
"trust_giver": {
|
||||
"id": self.trust_giver.id,
|
||||
"username": self.trust_giver.username,
|
||||
"url": get_url(response, "user", self.trust_giver),
|
||||
},
|
||||
"url": get_url(response, "trustbridge", self.trust_bridge),
|
||||
},
|
||||
)
|
||||
|
||||
def test_send_confirmation_email_on_creation(self):
|
||||
"""When setting a trust giver a confirmation email is sent."""
|
||||
self.create_trust_bridge()
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn(self.user.trust_bridge.get_confirmation_token(), mail.outbox[0].body)
|
||||
|
||||
def test_confirm_trust_bridge(self):
|
||||
"""The trust giver may confirm the trust bridge."""
|
||||
url = "/trust-bridges/{pk}/confirm/"
|
||||
self.create_trust_bridge()
|
||||
response = self.client.post(
|
||||
self.get_api_url(url, pk=self.trust_bridge.pk), {"token": self.trust_bridge.get_confirmation_token()}
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.trust_bridge.refresh_from_db()
|
||||
self.assertTrue(self.trust_bridge.is_trusted)
|
18
userausfall/rest_api/tests/userausfall.py
Normal file
18
userausfall/rest_api/tests/userausfall.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from rest_framework.reverse import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
def get_url(response, basename, instance):
|
||||
return reverse(f"{basename}-detail", [instance.pk], request=response.wsgi_request)
|
||||
|
||||
|
||||
class UserausfallAPITestCase(APITestCase):
|
||||
base_url = "/api"
|
||||
|
||||
def get_api_url(self, url, **kwargs):
|
||||
"""
|
||||
Prepend the path to the full url for this test class.
|
||||
:param url: an url fragment
|
||||
:return: the url fragment prepended with the base url
|
||||
"""
|
||||
return f"{self.base_url}{url.format(**kwargs)}"
|
72
userausfall/rest_api/tests/users.py
Normal file
72
userausfall/rest_api/tests/users.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
from django.test import override_settings
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import ErrorDetail
|
||||
|
||||
from userausfall.ldap import LDAPManager
|
||||
from userausfall.rest_api.tests.trust_bridges import TrustBridgeMixin
|
||||
from userausfall.rest_api.tests.userausfall import get_url, UserausfallAPITestCase
|
||||
|
||||
|
||||
@override_settings(USERAUSFALL_LDAP_IS_TEST=True)
|
||||
class UserTestCase(TrustBridgeMixin, UserausfallAPITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.ldap = LDAPManager()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.ldap.drop_test_connection()
|
||||
|
||||
def test_retrieve_user(self):
|
||||
"""Retrieve the details of the current user."""
|
||||
url = "/users/{pk}/"
|
||||
self.authenticate_user()
|
||||
response = self.client.get(self.get_api_url(url, pk=self.user.pk))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(
|
||||
response.data,
|
||||
{
|
||||
"id": self.user.id,
|
||||
"trust_bridge": None,
|
||||
"url": get_url(response, "user", self.user),
|
||||
},
|
||||
)
|
||||
|
||||
def test_activate_user(self):
|
||||
"""Create the ldap account for the current user."""
|
||||
url = "/users/{pk}/activate/"
|
||||
self.create_trust_bridge(is_trusted=True)
|
||||
self.authenticate_user()
|
||||
response = self.client.post(self.get_api_url(url, pk=self.user.pk), {"password": self.password})
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertTrue(self.user.has_ldap_account())
|
||||
|
||||
def test_activate_user_with_invalid_password(self):
|
||||
"""Create the ldap account for the current user with an invalid password."""
|
||||
url = "/users/{pk}/activate/"
|
||||
self.create_trust_bridge(is_trusted=True)
|
||||
self.authenticate_user()
|
||||
response = self.client.post(self.get_api_url(url, pk=self.user.pk), {"password": "invalid"})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertDictEqual(
|
||||
response.data, {"password": [ErrorDetail("Password does not match the user's password", code="invalid")]}
|
||||
)
|
||||
|
||||
def test_activate_user_without_trust_bridge(self):
|
||||
"""Create the ldap account for the current user without a trust bridge."""
|
||||
url = "/users/{pk}/activate/"
|
||||
self.authenticate_user()
|
||||
response = self.client.post(self.get_api_url(url, pk=self.user.pk), {"password": self.password})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertDictEqual(
|
||||
response.data, {"non_field_errors": [ErrorDetail("User has no trusted trust bridge", code="invalid")]}
|
||||
)
|
||||
|
||||
def test_activate_user_with_untrusted_trust_bridge(self):
|
||||
"""Create the ldap account for the current user with an untrusted trust bridge."""
|
||||
url = "/users/{pk}/activate/"
|
||||
self.create_trust_bridge(is_trusted=False)
|
||||
self.authenticate_user()
|
||||
response = self.client.post(self.get_api_url(url, pk=self.user.pk), {"password": self.password})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertDictEqual(
|
||||
response.data, {"non_field_errors": [ErrorDetail("User has no trusted trust bridge", code="invalid")]}
|
||||
)
|
|
@ -1,13 +1,17 @@
|
|||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
from rest_framework import routers
|
||||
|
||||
from userausfall.rest_api.views import UserViewSet
|
||||
from userausfall.rest_api.views import TrustBridgeViewSet, UserViewSet
|
||||
|
||||
router = routers.DefaultRouter(trailing_slash=True)
|
||||
router.register(r'users', UserViewSet, basename="user")
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r"trust-bridges", TrustBridgeViewSet, "trustbridge")
|
||||
router.register(r"users", UserViewSet, "user")
|
||||
|
||||
urlpatterns = [
|
||||
# path("confirm/confidant/", ConfidantConfirmationView.as_view())
|
||||
path("", include(router.urls)),
|
||||
path("auth/", include("rest_registration.api.urls")),
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
|
||||
path("schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
|
|
@ -1,48 +1,45 @@
|
|||
from rest_framework import viewsets, status
|
||||
from djeveric.views import ConfirmModelMixin
|
||||
from rest_framework import mixins, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from userausfall.models import User, MissingUserAttribute, PasswordMismatch
|
||||
from userausfall.rest_api.permissions import UserPermission
|
||||
from userausfall.rest_api.serializers import (
|
||||
ActivateUserSerializer,
|
||||
CreateUserSerializer,
|
||||
RetrieveUserSerializer,
|
||||
)
|
||||
from userausfall.models import TrustBridge, User
|
||||
from userausfall.rest_api.serializers import ActivateUserSerializer, TrustBridgeSerializer, UserSerializer
|
||||
from userausfall.views import get_authenticated_user
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [UserPermission]
|
||||
queryset = User.objects.all()
|
||||
class TrustBridgeViewSet(
|
||||
ConfirmModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
):
|
||||
queryset = TrustBridge.objects
|
||||
serializer_class = TrustBridgeSerializer
|
||||
|
||||
@action(detail=False, url_path="me")
|
||||
def retrieve_authenticated(self, request):
|
||||
"""Retrieve user data for logged in user."""
|
||||
serializer = self.get_serializer(request.user)
|
||||
return Response(serializer.data)
|
||||
def get_basic_queryset(self):
|
||||
return self.queryset.filter(trust_taker=get_authenticated_user(self.request))
|
||||
|
||||
|
||||
class ActivateUserMixin:
|
||||
@action(detail=True, methods=["post"])
|
||||
def activate(self, request, pk=None):
|
||||
"""Create the corresponding LDAP account."""
|
||||
user: User = self.get_object()
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
try:
|
||||
# We prevent untrusted user accounts from being activated via API.
|
||||
# They might be activated via Admin or programmatically.
|
||||
if not user.trust_bridge.is_trusted:
|
||||
raise MissingUserAttribute("User has no trusted trust bridge.")
|
||||
user.create_ldap_account(serializer.validated_data["password"])
|
||||
except (MissingUserAttribute, PasswordMismatch) as e:
|
||||
return Response({"message": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_activate(instance, serializer)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
else:
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def perform_activate(self, instance: User, serializer):
|
||||
instance.create_ldap_account(serializer.validated_data["password"])
|
||||
|
||||
|
||||
class UserViewSet(ActivateUserMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return User.objects.filter(pk=get_authenticated_user(self.request).pk)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "activate":
|
||||
return ActivateUserSerializer
|
||||
elif self.action == "create":
|
||||
return CreateUserSerializer
|
||||
elif self.action == "retrieve_authenticated":
|
||||
return RetrieveUserSerializer
|
||||
else:
|
||||
return super().get_serializer_class()
|
||||
|
|
|
@ -2,24 +2,25 @@
|
|||
Django settings for userausfall.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/topics/settings/
|
||||
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DATA_DIR = os.environ.get("USERAUSFALL_DATA_DIR", BASE_DIR)
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = Path(os.environ.get("USERAUSFALL_DATA_DIR", BASE_DIR))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get('LOCAL_DJANGO_SECRET_KEY')
|
||||
SECRET_KEY = "django-insecure-ngy2599=i5c*5(*bw%gbs&jzb(^p-4zk&6!a8a76tevv$tb9xq"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
@ -30,80 +31,78 @@ ALLOWED_HOSTS = []
|
|||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.staticfiles',
|
||||
'userausfall',
|
||||
'rest_framework',
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.staticfiles",
|
||||
"rest_framework",
|
||||
"rest_registration",
|
||||
"django_filters",
|
||||
"drf_spectacular",
|
||||
"userausfall",
|
||||
"userausfall.rest_api",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'userausfall.urls'
|
||||
ROOT_URLCONF = "userausfall.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'userausfall.wsgi.application'
|
||||
WSGI_APPLICATION = "userausfall.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(DATA_DIR, 'db.sqlite3'),
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": DATA_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# User model
|
||||
#
|
||||
|
||||
AUTH_USER_MODEL = 'userausfall.User'
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -111,9 +110,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'de-de'
|
||||
LANGUAGE_CODE = "de-de"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
@ -125,30 +124,69 @@ USE_TZ = True
|
|||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
|
||||
# Media files
|
||||
# https://docs.djangoproject.com/en/2.2/topics/files/
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
MEDIA_ROOT = DATA_DIR / "media"
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_URL = "/media/"
|
||||
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
|
||||
# User model
|
||||
#
|
||||
|
||||
AUTH_USER_MODEL = "userausfall.User"
|
||||
|
||||
|
||||
# Sending email
|
||||
# https://docs.djangoproject.com/en/3.2/topics/email/
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
|
||||
# Django Rest Framework
|
||||
# https://www.django-rest-framework.org/api-guide/settings/
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
),
|
||||
"DEFAULT_FILTER_BACKENDS": [
|
||||
"django_filters.rest_framework.DjangoFilterBackend",
|
||||
],
|
||||
"DEFAULT_PARSER_CLASSES": [
|
||||
"djangorestframework_camel_case.parser.CamelCaseFormParser",
|
||||
"djangorestframework_camel_case.parser.CamelCaseMultiPartParser",
|
||||
"djangorestframework_camel_case.parser.CamelCaseJSONParser",
|
||||
],
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
"rest_framework.permissions.IsAuthenticated",
|
||||
],
|
||||
"DEFAULT_RENDERER_CLASSES": [
|
||||
"djangorestframework_camel_case.render.CamelCaseJSONRenderer",
|
||||
"djangorestframework_camel_case.render.CamelCaseBrowsableAPIRenderer",
|
||||
],
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"TEST_REQUEST_DEFAULT_FORMAT": "json",
|
||||
}
|
||||
|
||||
REST_REGISTRATION = {
|
||||
"REGISTER_VERIFICATION_ENABLED": False,
|
||||
"REGISTER_EMAIL_VERIFICATION_ENABLED": False,
|
||||
"RESET_PASSWORD_VERIFICATION_ENABLED": False,
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "Userausfall API",
|
||||
"DESCRIPTION": "Account management for systemausfall.org",
|
||||
"VERSION": "0.0.1",
|
||||
"POSTPROCESSING_HOOKS": ["drf_spectacular.contrib.djangorestframework_camel_case.camelize_serializer_fields"],
|
||||
}
|
||||
|
||||
|
||||
|
@ -159,6 +197,10 @@ SITE_ID = 1
|
|||
|
||||
|
||||
USERAUSFALL_LDAP = {
|
||||
'ADMIN_USER_DN': 'cn=admin,dc=local',
|
||||
'ADMIN_USER_PASSWORD': os.environ.get('USERAUSFALL_LDAP_PASSWORD'),
|
||||
"ADMIN_USER_DN": "cn=admin,dc=local",
|
||||
"ADMIN_USER_PASSWORD": os.environ.get("USERAUSFALL_LDAP_PASSWORD"),
|
||||
}
|
||||
|
||||
USERAUSFALL = {
|
||||
"PRIMARY_EMAIL_DOMAIN": "systemausfall.org",
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from userausfall.models import User
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def user_saved(sender, instance: User, **kwargs):
|
||||
# if instance.confidant_unconfirmed is not None:
|
||||
# ConfidantConfirmation(instance.confidant_unconfirmed, instance).send_request()
|
||||
pass
|
30
userausfall/tests.py
Normal file
30
userausfall/tests.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from django.test import override_settings, TestCase
|
||||
|
||||
from userausfall.ldap import LDAPManager
|
||||
|
||||
|
||||
@override_settings(USERAUSFALL_LDAP_IS_TEST=True)
|
||||
class LDAPTestCase(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.username = "test"
|
||||
self.password = "test12345"
|
||||
self.ldap = LDAPManager()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.ldap.drop_test_connection()
|
||||
|
||||
def test_create_has_account(self):
|
||||
exists = self.ldap.has_account(self.username)
|
||||
self.assertFalse(exists)
|
||||
is_created = self.ldap.create_account(self.username, self.password)
|
||||
self.assertTrue(is_created)
|
||||
exists = self.ldap.has_account(self.username)
|
||||
self.assertTrue(exists)
|
||||
|
||||
def test_create_account_data(self):
|
||||
is_valid = self.ldap.is_valid_account_data(self.username, self.password)
|
||||
self.assertFalse(is_valid)
|
||||
is_created = self.ldap.create_account(self.username, self.password)
|
||||
self.assertTrue(is_created)
|
||||
is_valid = self.ldap.is_valid_account_data(self.username, self.password)
|
||||
self.assertTrue(is_valid)
|
|
@ -1,10 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
from userausfall.rest_api import urls as rest_api_urls
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include("userausfall.rest_api.urls")),
|
||||
path("api-auth/", include("rest_framework.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/", include("userausfall.rest_api.urls")),
|
||||
]
|
||||
|
|
7
userausfall/views.py
Normal file
7
userausfall/views.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from userausfall.models import User
|
||||
|
||||
|
||||
def get_authenticated_user(request) -> User:
|
||||
if request is not None and request.user.is_authenticated:
|
||||
return request.user
|
||||
return None
|
|
@ -1,16 +1,16 @@
|
|||
"""
|
||||
WSGI config for userausfall project.
|
||||
WSGI config for memoorje project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'userausfall.settings')
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "userausfall.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
|
Reference in a new issue