Compare commits
No commits in common. "master" and "master" have entirely different histories.
148
.dockerignore
148
.dockerignore
|
@ -1,148 +0,0 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
databse.db
|
||||
.env
|
||||
|
||||
|
||||
node_modules/
|
||||
dist/
|
||||
tmp/
|
||||
northstar.db
|
||||
instance
|
||||
northstar/static/docs/openapi
|
|
@ -1,7 +1,3 @@
|
|||
export DATABASE_URL=""
|
||||
export db=""
|
||||
export STRIPE_SECRET_KEY=""
|
||||
export STRIPE_PUBLIC_KEY=""
|
||||
export HOSTEA_INFRA_HOSTEA_REPO_REMOTE="ssh://git@localhost:22/hostea/fleet.git"
|
||||
export HOSTEA_META_FORGEJO_INSTANCE="http://localhost:3000"
|
||||
export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
|
||||
export OIDC_RSA_PRIVATE_KEY=""
|
||||
|
|
|
@ -152,6 +152,3 @@ cython_debug/
|
|||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
keys
|
||||
htmlcov/
|
||||
tmp/
|
||||
static/
|
||||
|
|
|
@ -3,17 +3,12 @@ pipeline:
|
|||
image: python
|
||||
environment:
|
||||
- DATABSE_URL=postgres://postgres:password@database:5432/postgres
|
||||
- EMAIL_URL=smtp://admin:password@smtp:10025
|
||||
- HOSTEA_INFRA_HOSTEA_REPO_REMOTE=ssh://git@forgejo:22/hostea/
|
||||
- HOSTEA_META_FORGEJO_INSTANCE=http://forgejo:3000
|
||||
- EMAIL_URL=smtp://admin:password@localhost:10025
|
||||
commands:
|
||||
- export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
|
||||
- pip install virtualenv
|
||||
- make env
|
||||
- make lint
|
||||
- make coverage
|
||||
# - make integration-test
|
||||
secrets: [ STRIPE_PUBLIC_KEY, STRIPE_SECRET_KEY ]
|
||||
- make test
|
||||
|
||||
services:
|
||||
database:
|
||||
|
@ -21,15 +16,9 @@ services:
|
|||
environment:
|
||||
- POSTGRES_PASSWORD=password
|
||||
|
||||
forgejo:
|
||||
image: codeberg.org/forgejo/forgejo:1.18.0-1
|
||||
container_name: hostea-dash-forgejo
|
||||
|
||||
smtp:
|
||||
image: maildev/maildev:latest
|
||||
container_name: hostea-dash-maildev
|
||||
image: maildev/maildev
|
||||
environment:
|
||||
- MAILDEV_SMTP_PORT=10025
|
||||
- MAILDEV_WEB_PORT=1080
|
||||
- MAILDEV_INCOMING_USER=admin
|
||||
- MAILDEV_INCOMING_PASS=password
|
||||
|
|
19
Dockerfile
19
Dockerfile
|
@ -1,19 +0,0 @@
|
|||
FROM python
|
||||
|
||||
LABEL org.opencontainers.image.source https://forgejo.hostea.org/Hostea/dashboard
|
||||
|
||||
RUN useradd -ms /bin/bash -u 1001 hostea
|
||||
RUN apt-get update && apt-get install -y ca-certificates git
|
||||
USER hostea
|
||||
|
||||
WORKDIR /home/hostea
|
||||
run mkdir app/
|
||||
WORKDIR /home/hostea/app/
|
||||
RUN pip3 install virtualenv
|
||||
RUN python3 -m virtualenv venv
|
||||
COPY requirements.txt .
|
||||
# See https://github.com/pypa/pip/issues/9819
|
||||
RUN ./venv/bin/pip install --use-feature=in-tree-build -r requirements.txt
|
||||
COPY . .
|
||||
#ENV . ./venv/bin/activate && make env
|
||||
CMD [ "./venv/bin/python3", "manage.py", "runserver", "0.0.0.0:8000" ]
|
13
Makefile
13
Makefile
|
@ -9,13 +9,10 @@ endef
|
|||
|
||||
default: ## Run app
|
||||
$(call run_migrations)
|
||||
. ./venv/bin/activate && yes yes | python manage.py collectstatic
|
||||
. ./venv/bin/activate && python manage.py runserver
|
||||
|
||||
coverage: ## Generate test coverage report
|
||||
. ./venv/bin/activate && coverage run manage.py test
|
||||
. ./venv/bin/activate && coverage report -m
|
||||
. ./venv/bin/activate && coverage html
|
||||
$(call unimplemented)
|
||||
|
||||
doc: ## Generates documentation
|
||||
$(call unimplemented)
|
||||
|
@ -26,8 +23,6 @@ docker: ## Build Docker image from source
|
|||
env: ## Install all dependencies
|
||||
@-virtualenv venv
|
||||
. ./venv/bin/activate && pip install -r requirements.txt
|
||||
. ./venv/bin/activate && ./integration/ci.sh init
|
||||
. ./venv/bin/activate && yes yes | python manage.py collectstatic
|
||||
|
||||
freeze: ## Freeze python dependencies
|
||||
@. ./venv/bin/activate && pip freeze > requirements.txt
|
||||
|
@ -35,11 +30,9 @@ freeze: ## Freeze python dependencies
|
|||
help: ## Prints help for targets with comments
|
||||
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
integration-test: ## run integration tests
|
||||
. ./venv/bin/activate && integration/tests.sh
|
||||
|
||||
lint: ## Run linter
|
||||
@./venv/bin/black dashboard accounts dash support billing infrastructure integration
|
||||
@./venv/bin/black ./dashboard/*
|
||||
@./venv/bin/black ./accounts/*
|
||||
|
||||
migrate: ## Run migrations
|
||||
$(call run_migrations)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Hostea dashboard
|
||||
|
||||
[![status-badge](https://woodpecker.gna.org/api/badges/Hostea/dashboard/status.svg)](https://woodpecker.gna.org/Hostea/dashboard)
|
||||
[![status-badge](https://woodpecker.hostea.org/api/badges/Hostea/dashboard/status.svg)](https://woodpecker.hostea.org/Hostea/dashboard)
|
||||
[![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg?style=flat-square)](http://www.gnu.org/licenses/agpl-3.0)
|
||||
[![Chat](https://img.shields.io/badge/matrix-hostea:matrix.batsense.net-purple?style=flat-square)](https://matrix.to/#/#hostea:matrix.batsense.net)
|
||||
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
from .utils import ConfirmAccess
|
||||
|
||||
|
||||
def confirm_access(function):
|
||||
def wrap(request, *args, **kwargs):
|
||||
return ConfirmAccess.validate_decorator(
|
||||
request=request, fn=function, *args, **kwargs
|
||||
)
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
def redirect_if_authenticated(fn):
|
||||
"""
|
||||
Redirect authenticated users visiting sign in/sign up views
|
||||
"""
|
||||
|
||||
def wrap(request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
data = request.GET if request.method == "GET" else request.POST
|
||||
if "next" in data:
|
||||
return redirect(data["next"])
|
||||
return redirect(reverse("accounts.home"))
|
||||
return fn(request, *args, **kwargs)
|
||||
|
||||
return wrap
|
|
@ -1,110 +0,0 @@
|
|||
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from oauth2_provider.models import get_application_model
|
||||
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
||||
|
||||
from accounts.utils import gen_secret
|
||||
|
||||
Application = get_application_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Get user ID from username"
|
||||
app_name_key = "app_name"
|
||||
username_key = "username"
|
||||
redirect_uri_key = "redirect_uri"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(self.app_name_key, type=str, help="The application name")
|
||||
|
||||
parser.add_argument(
|
||||
self.username_key,
|
||||
type=str,
|
||||
help="The username of user who will own this app",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
self.redirect_uri_key,
|
||||
type=str,
|
||||
help="The username of user who will own this app",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if self.username_key not in options:
|
||||
self.stdout.write(self.style.ERROR("Please provide username"))
|
||||
return
|
||||
if self.app_name_key not in options:
|
||||
self.stdout.write(self.style.ERROR("Please provide application name"))
|
||||
return
|
||||
|
||||
if self.redirect_uri_key not in options:
|
||||
self.stdout.write(self.style.ERROR("Please provide redirect uri"))
|
||||
return
|
||||
|
||||
username = options[self.username_key]
|
||||
application_name = options[self.app_name_key]
|
||||
redirect_uri = options[self.redirect_uri_key]
|
||||
|
||||
User = get_user_model()
|
||||
if not User.objects.filter(username=username).exists():
|
||||
self.stderr.write(self.style.ERROR(f"user {username} not found"))
|
||||
return
|
||||
|
||||
user = User.objects.get(username=username)
|
||||
# python manage.py createapplication --name demo-oidc-app --user 1 --client-id 22500acb0bcfcba137d6b8ae96d3f2 --client-secret 296055337620b0e443ad24a32cb675 --algorithm HS256 --skip-authorization --redirect-uri http://example.org/uri1 confidential code -v
|
||||
|
||||
client_id = generate_client_id()
|
||||
client_secret = generate_client_secret()
|
||||
config = {
|
||||
"name": application_name,
|
||||
"user_id": user.id,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"algorithm": "HS256",
|
||||
"skip_authorization": True,
|
||||
"redirect_uris": redirect_uri,
|
||||
"authorization_grant_type": "authorization-code",
|
||||
"client_type": "confidential",
|
||||
}
|
||||
|
||||
app = Application(**config)
|
||||
|
||||
try:
|
||||
app.full_clean()
|
||||
except ValidationError as exc:
|
||||
errors = "\n ".join(
|
||||
[
|
||||
"- " + err_key + ": " + str(err_value)
|
||||
for err_key, err_value in exc.message_dict.items()
|
||||
]
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.ERROR("Please correct the following errors:\n %s" % errors)
|
||||
)
|
||||
else:
|
||||
app.save()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"New application {application_name} created successfully."
|
||||
)
|
||||
)
|
||||
self.stdout.write(f"client_id: {client_id}")
|
||||
self.stdout.write(f"client_secret: {client_secret}")
|
|
@ -1,52 +0,0 @@
|
|||
# Generated by Django 4.0.3 on 2022-07-10 06:14
|
||||
|
||||
import accounts.utils
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("accounts", "0005_accountconfirmchallenge_created_at"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PasswordResetChallenge",
|
||||
fields=[
|
||||
(
|
||||
"public_ref",
|
||||
models.CharField(
|
||||
default=accounts.utils.gen_secret,
|
||||
editable=False,
|
||||
max_length=32,
|
||||
unique=True,
|
||||
verbose_name="Public referece to challenge text",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"challenge_text",
|
||||
models.CharField(
|
||||
default=accounts.utils.gen_secret,
|
||||
editable=False,
|
||||
max_length=32,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
verbose_name="Challenge text",
|
||||
),
|
||||
),
|
||||
(
|
||||
"owned_by",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -52,36 +52,3 @@ class AccountConfirmChallenge(models.Model):
|
|||
|
||||
def pending_url(self):
|
||||
return reverse("accounts.verify.pending", args=(self.public_ref,))
|
||||
|
||||
|
||||
class PasswordResetChallenge(models.Model):
|
||||
owned_by = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
public_ref = models.CharField(
|
||||
"Public referece to challenge text",
|
||||
unique=True,
|
||||
max_length=32,
|
||||
default=gen_secret,
|
||||
editable=False,
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, blank=True)
|
||||
|
||||
challenge_text = models.CharField(
|
||||
"Challenge text",
|
||||
unique=True,
|
||||
max_length=32,
|
||||
default=gen_secret,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.challenge_text}"
|
||||
|
||||
def verification_link(self):
|
||||
"""
|
||||
Get verification link
|
||||
"""
|
||||
return reverse("accounts.password.reset", args=(self.challenge_text,))
|
||||
|
||||
def pending_url(self):
|
||||
return reverse("accounts.password.reset.resend", args=(self.public_ref,))
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
{% extends "common/components/base.html" %}
|
||||
{% block title %}{% block title_name %} {% endblock %} | Gna! Dashboard{% endblock %}
|
||||
{% block title %}{% block title_name %} {% endblock %} | Hostea Dashbaord{% endblock %}
|
||||
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<main class="auth__main">
|
||||
<section class="main">
|
||||
<div class="title">
|
||||
<h1><a href="https://forgejo.org">Forgejo</a> hosting and <a href="/forgejo-clinic/">service</a></h1>
|
||||
<h1>Free Forge Ecosystem for Free Developers</h1>
|
||||
<p class="welcome">
|
||||
A free forge ecosystem for free developers.
|
||||
Hostea is a self-hostable libre software development suite comprising Gitea, Woodpecker CI, Librepages and GitPad with payments integration.
|
||||
</p>
|
||||
<ul class="index-banner__features-list">
|
||||
<li class="index-banner__features">Dedicated <a href="https://hosteadashboard.gna.org/register/">Forgejo hosting</a> and <a href="https://woodpecker-ci.org/">Woodpecker CI</a> from 10€/month</li>
|
||||
<li class="index-banner__features">Clinic to <a href="https://gna.org/forgejo-clinic/">heal sick Forgejo</a> instances</li>
|
||||
<li class="index-banner__features">100% <a href="https://www.gnu.org/philosophy/free-sw.html">Free Software</a></li>
|
||||
<li class="index-banner__features">Radically <a href="https://forum.gna.org/t/about-governance-and-decisions-in-hostea/55">Transparent</a></li>
|
||||
<li class="index-banner__features">Run by a <a href="https://forum.gna.org/t/about-governance-and-decisions-in-hostea/55">horizontal collective</a></li>
|
||||
<li class="index-banner__features">25% of the income <a href="https://forum.gna.org/t/decision-revenue-sharing-model/92">dedicated to sustain Free Software dependencies</a></li>
|
||||
<li class="index-banner__features">Committed to <a href="https://forgefriends.org/blog/2022/06/30/2022-06-state-forge-federation/">further forge federation</a></li>
|
||||
<li class="index-banner__features">Fully managed</li>
|
||||
<li class="index-banner__features">100% Free Software</li>
|
||||
<li class="index-banner__features">Fully Self-Hostable</li>
|
||||
<li class="index-banner__features">Observable and reliable</li>
|
||||
<li class="index-banner__features">Federation when available</li>
|
||||
<li class="index-banner__features">Radically transparent</li>
|
||||
<li class="index-banner__features">Horizontal community</li>
|
||||
<li class="index-banner__features">Run Hostea and become a service provider!</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{% extends 'accounts/auth/base.html' %}
|
||||
|
||||
{% block title_name %} {{ title }} {% endblock %}
|
||||
{% block login %}
|
||||
<h2>{{ title }}</h2>
|
||||
<h2>Login</h2>
|
||||
<form action="{% url 'accounts.login' %}" method="POST" class="form" accept-charset="utf-8">
|
||||
{% include "common/components/error.html" %}
|
||||
{% csrf_token %}
|
||||
|
@ -21,8 +19,6 @@
|
|||
/>
|
||||
</label>
|
||||
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
|
||||
<label class="form__label" for="password">
|
||||
Password
|
||||
<input
|
||||
|
@ -34,12 +30,12 @@
|
|||
/>
|
||||
</label>
|
||||
<div class="form__action-container">
|
||||
<a href="{% url 'accounts.password.reset.new' %}">Forgot password?</a>
|
||||
<a href="/forgot-password">Forgot password?</a>
|
||||
<button class="form__submit" type="submit">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="form__alt-action">
|
||||
New to Gna!?
|
||||
New to Hostea?
|
||||
<a href="{% url 'accounts.register' %}">Create an account</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
{% extends "common/components/base.html" %}
|
||||
{% block title %} Reset Password| Gna! Dashboard{% endblock %}
|
||||
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||
{% block main %}
|
||||
<div class="dialogue-box__container">
|
||||
<h2>Reset password</h2>
|
||||
<form
|
||||
action="{% url 'accounts.password.reset.new' %}"
|
||||
method="POST"
|
||||
class="form"
|
||||
accept-charset="utf-8"
|
||||
>
|
||||
{% include "common/components/error.html" %} {% csrf_token %}
|
||||
<label class="form__label" for="email">
|
||||
Email
|
||||
<input
|
||||
class="form__input"
|
||||
name="email"
|
||||
id="email"
|
||||
type="email"
|
||||
/>
|
||||
</label>
|
||||
<div class="form__action-container">
|
||||
<button class="form__submit" type="submit">Send Password Reset Link</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,20 +0,0 @@
|
|||
{% extends "common/components/base.html" %}
|
||||
{% block title %} Reset Password | Gna! Dashboard{% endblock %}
|
||||
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||
{% block main %}
|
||||
<div class="dialogue-box__container">
|
||||
<h2>Reset password</h2>
|
||||
<p>Verification link is sent to email address: {{email}}</p>
|
||||
<form
|
||||
action="{% url 'accounts.password.reset.resend' public_ref=public_ref %}"
|
||||
method="POST"
|
||||
class="form"
|
||||
accept-charset="utf-8"
|
||||
>
|
||||
{% include "common/components/error.html" %} {% csrf_token %}
|
||||
<div class="form__action-container">
|
||||
<button class="form__submit" type="submit">Click here to resend email</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,40 +0,0 @@
|
|||
{% extends "common/components/base.html" %}
|
||||
{% block title %} Reset Password | Gna! Dashboard{% endblock %}
|
||||
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||
{% block main %}
|
||||
<div class="dialogue-box__container">
|
||||
<h2>Reset Password</h2>
|
||||
<form
|
||||
action="{% url 'accounts.password.reset' challenge=challenge %}"
|
||||
method="POST"
|
||||
class="form"
|
||||
accept-charset="utf-8"
|
||||
>
|
||||
{% include "common/components/error.html" %} {% csrf_token %}
|
||||
<label class="form__label" for="password">
|
||||
password
|
||||
<input
|
||||
class="form__input"
|
||||
name="password"
|
||||
required
|
||||
id="password"
|
||||
type="password"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="form__label" for="confirm_password">
|
||||
Re-enter Password
|
||||
<input
|
||||
class="form__input"
|
||||
name="confirm_password"
|
||||
required
|
||||
id="confirm_password"
|
||||
type="password"
|
||||
/>
|
||||
</label>
|
||||
<div class="form__action-container">
|
||||
<button class="form__submit" type="submit">Reset Password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,7 +1,6 @@
|
|||
{% extends 'accounts/auth/base.html' %}
|
||||
{% block title_name %} {{ title }} {% endblock %}
|
||||
{% block login %}
|
||||
<h2>{{ title }}</h2>
|
||||
<h2>Sign Up</h2>
|
||||
<form action="{% url 'accounts.register' %}" method="POST" class="form" accept-charset="utf-8">
|
||||
{% include "common/components/error.html" %}
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
{% extends "common/components/base.html" %}
|
||||
{% block title %} Confirm Access | Gna! Dashboard{% endblock %}
|
||||
{% block nav %} {% include "dash/common/components/primary-nav.html" %} {% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="dialogue-box__container">
|
||||
<h1>{{ Title }}</h1>
|
||||
<form
|
||||
action="{% url 'accounts.sudo' %}"
|
||||
method="POST"
|
||||
class="form"
|
||||
accept-charset="utf-8"
|
||||
>
|
||||
{% include "common/components/error.html" %} {% csrf_token %}
|
||||
<p>Please login to confirm access</p>
|
||||
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<label class="form__label" for="password">
|
||||
Password
|
||||
<input
|
||||
class="form__input"
|
||||
name="password"
|
||||
required
|
||||
id="password"
|
||||
type="password"
|
||||
/>
|
||||
</label>
|
||||
<div class="form__action-container">
|
||||
<button class="form__submit" type="submit">Confirm access</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "common/components/base.html" %}
|
||||
{% block title %} Confirm Account | Gna! Dashboard{% endblock %}
|
||||
{% block title %} Confirm Account | Hostea Dashbaord{% endblock %}
|
||||
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||
{% block main %}
|
||||
<div class="dialogue-box__container">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "common/components/base.html" %}
|
||||
{% block title %} Confirm Account | Gna! Dashboard{% endblock %}
|
||||
{% block title %} Confirm Account | Hostea Dashbaord{% endblock %}
|
||||
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||
{% block main %}
|
||||
<div class="dialogue-box__container">
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
Hello {{ username }},
|
||||
|
||||
You have a new password!
|
||||
|
||||
Your password for signing in to Gna! was recently changed. If you made this change, then we're all set.
|
||||
|
||||
If you did not make this change, please reset your password to secure your account.
|
||||
|
||||
{% url 'accounts.password.reset.new' %}
|
||||
|
||||
Either way, feel free to reach out with any questions you might have. We're here to help.
|
||||
|
||||
Cheers,
|
||||
Gna! team
|
|
@ -1,9 +0,0 @@
|
|||
Hello {{ email }},
|
||||
|
||||
Please click on the link below to reset your password:
|
||||
{{ link }}
|
||||
|
||||
If you don't recognise this activity, please delete this mail.
|
||||
|
||||
Cheers,
|
||||
Gna! team
|
|
@ -1,9 +0,0 @@
|
|||
Hello {{ username }},
|
||||
|
||||
Please click on the link below to verify your email.
|
||||
{{ link }}
|
||||
|
||||
If you don't recognise this activity, please delete this mail.
|
||||
|
||||
Cheers,
|
||||
Gna! team
|
|
@ -9,7 +9,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<header>{% block nav %} {% endblock %}</header>
|
||||
{% block main %} {% endblock %}
|
||||
<main>{% block main %} {% endblock %}</main>
|
||||
{% include "common/components/footer.html" %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,6 +2,20 @@
|
|||
<div class="footer__container">
|
||||
<div class="footer__column">
|
||||
<span class="license__conatiner">
|
||||
<a class="license__link" rel="noreferrer" href="/docs" target="_blank"
|
||||
>Docs</a
|
||||
>
|
||||
<span class="footer__column-divider--mobile-visible">|</span>
|
||||
<a
|
||||
class="license__link"
|
||||
rel="noreferrer"
|
||||
href="https://www.eff.org/issues/do-not-track/amp/"
|
||||
target="_blank"
|
||||
>No AMP</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="footer__column">
|
||||
<a
|
||||
href="/"
|
||||
class="footer__link"
|
||||
|
@ -9,24 +23,19 @@
|
|||
rel="noopener"
|
||||
title="RSS"
|
||||
>Home</a>
|
||||
<span class="footer__column-divider--mobile-visible">|</span>
|
||||
<a class="license__link" rel="noreferrer" href="https://gna.org/about" target="_blank"
|
||||
> About</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="footer__column-divider">|</div>
|
||||
<a href="mailto:{{ footer.admin_email }}" class="footer__link"
|
||||
>Contact Instance Maintainer</a
|
||||
>
|
||||
<div class="footer__column-divider">|</div>
|
||||
<a
|
||||
class="footer__link"
|
||||
href="{{ footer.source_code.link }}"
|
||||
href="{{ footer.source_code }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="Source Code"
|
||||
>
|
||||
{{ footer.source_code.text }}
|
||||
v{{ footer.version }}-{{ footer.git_hash }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
||||
<div class="nav__header">
|
||||
<a class="nav__logo-container" href="/">
|
||||
<img class="nav__logo-img" src="{% static 'img/logo.png' %}"
|
||||
alt="Gna! logo"/>
|
||||
<img src="{% static 'img/android-icon-48x48.png' %}"
|
||||
alt="Hostea temporary logo"/>
|
||||
<p class="nav__home-btn">
|
||||
ostea
|
||||
</p>
|
||||
</a>
|
||||
<label class="nav__hamburger-menu" for="nav__toggle">
|
||||
<span class="nav__hamburger-inner"></span>
|
||||
|
@ -18,8 +21,9 @@
|
|||
<a class="nav__link" rel="noreferrer" href="{% url 'accounts.login' %}">Login</a>
|
||||
</div>
|
||||
<div class="nav__link-container">
|
||||
<a class="nav__link" rel="noreferrer" href="{% url 'accounts.register' %}">Register</a>
|
||||
<a class="nav__link" rel="noreferrer" href="page.auth.register">Register</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
@ -15,52 +15,17 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import time
|
||||
import os
|
||||
from io import StringIO
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
import requests
|
||||
|
||||
from django.core import mail
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management import call_command
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase, Client, override_settings
|
||||
from django.utils.http import urlencode
|
||||
from django.contrib.auth import authenticate
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
from oauth2_provider.models import get_application_model
|
||||
|
||||
from .models import AccountConfirmChallenge, PasswordResetChallenge
|
||||
from .models import AccountConfirmChallenge
|
||||
from .management.commands.rm_unverified_users import (
|
||||
Command as CleanUnverifiedUsersCommand,
|
||||
)
|
||||
from .utils import ConfirmAccess
|
||||
from .decorators import confirm_access
|
||||
|
||||
|
||||
def register_util(t: TestCase, username: str):
|
||||
t.password = "asdklfja;ldkfja;df"
|
||||
t.username = username
|
||||
t.email = f"{t.username}@example.org"
|
||||
t.user = get_user_model().objects.create(
|
||||
username=t.username,
|
||||
email=t.email,
|
||||
)
|
||||
t.user.set_password(t.password)
|
||||
t.user.save()
|
||||
|
||||
|
||||
def login_util(t: TestCase, c: Client, redirect_to: str):
|
||||
payload = {
|
||||
"login": t.username,
|
||||
"password": t.password,
|
||||
}
|
||||
resp = c.post(reverse("accounts.login"), payload)
|
||||
t.assertEqual(resp.status_code, 302)
|
||||
t.assertEqual(resp.headers["location"], reverse(redirect_to))
|
||||
|
||||
|
||||
class LoginTest(TestCase):
|
||||
|
@ -69,17 +34,22 @@ class LoginTest(TestCase):
|
|||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.password = "password121231"
|
||||
self.username = "create_new_app_tests"
|
||||
register_util(t=self, username=self.username)
|
||||
self.email = f"{self.username}@example.org"
|
||||
self.user = get_user_model().objects.create(
|
||||
username=self.username,
|
||||
email=self.email,
|
||||
)
|
||||
self.user.set_password(self.password)
|
||||
self.user.save()
|
||||
|
||||
def test_login_template_works(self):
|
||||
"""
|
||||
Tests if login template renders
|
||||
"""
|
||||
resp = self.client.get(reverse("accounts.login"))
|
||||
self.assertEqual(
|
||||
b"A free forge ecosystem for free developers" in resp.content, True
|
||||
)
|
||||
self.assertEqual(b"Free Forge Ecosystem" in resp.content, True)
|
||||
|
||||
def test_login_works(self):
|
||||
"""
|
||||
|
@ -88,10 +58,16 @@ class LoginTest(TestCase):
|
|||
c = Client()
|
||||
|
||||
# username login works
|
||||
login_util(t=self, c=c, redirect_to="accounts.home")
|
||||
payload = {
|
||||
"login": self.username,
|
||||
"password": self.password,
|
||||
}
|
||||
resp = c.post(reverse("accounts.login"), payload)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("accounts.home"))
|
||||
|
||||
# email login works
|
||||
payload = {
|
||||
paylaod = {
|
||||
"login": self.email,
|
||||
"password": self.password,
|
||||
}
|
||||
|
@ -105,140 +81,18 @@ class LoginTest(TestCase):
|
|||
"password": self.user.email,
|
||||
}
|
||||
|
||||
resp = self.client.post(reverse("accounts.login"), payload)
|
||||
resp = self.client.post(reverse("accounts.login"), paylaod)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
self.assertEqual(b"Login Failed" in resp.content, True)
|
||||
|
||||
# protected view works
|
||||
resp = c.get(reverse("accounts.home"))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("dash.home"))
|
||||
|
||||
def test_logout_works(self):
|
||||
"""
|
||||
Logout view tests
|
||||
"""
|
||||
c = Client()
|
||||
login_util(t=self, c=c, redirect_to="accounts.home")
|
||||
resp = c.get(reverse("accounts.logout"))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("accounts.login"))
|
||||
|
||||
def test_default_login_uri_works(self):
|
||||
"""
|
||||
/accounts/login should redirect_to /login
|
||||
"""
|
||||
c = Client()
|
||||
resp = c.get(reverse("accounts.default_login_url"))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("accounts.login"))
|
||||
|
||||
def test_login_view_redirects_if_user_is_loggedin(self):
|
||||
"""
|
||||
Automatically redirect authenticated users that are visiting login view
|
||||
"""
|
||||
c = Client()
|
||||
login_util(t=self, c=c, redirect_to="accounts.home")
|
||||
|
||||
resp = c.get(reverse("accounts.login"))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("accounts.home"))
|
||||
|
||||
resp = c.post(reverse("accounts.login"))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("accounts.home"))
|
||||
|
||||
resp = c.get(
|
||||
f"{reverse('accounts.login')}?next={reverse('dash.instances.list')}"
|
||||
)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("dash.instances.list"))
|
||||
|
||||
ctx = {"next": reverse("dash.instances.list")}
|
||||
resp = c.post(reverse("accounts.login"), ctx)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("dash.instances.list"))
|
||||
|
||||
|
||||
class ResetPasswordTest(TestCase):
|
||||
def setUp(self):
|
||||
self.username = "reset_password_user"
|
||||
register_util(t=self, username=self.username)
|
||||
|
||||
def reset_password(self):
|
||||
c = Client()
|
||||
payload = {
|
||||
"email": self.email,
|
||||
}
|
||||
resp = c.get(reverse("accounts.password.reset.new"))
|
||||
self.assertEqual(resp.status_code == 200)
|
||||
|
||||
resp = c.post(reverse("accounts.password.reset.new"), payload)
|
||||
self.assertEqual(resp.status_code == 302)
|
||||
challenge = PasswordResetChallenge.objects.filter(owned_by=self.user)
|
||||
self.assertEqual(resp.headers["location"] == challenge.pending_url(), True)
|
||||
|
||||
password_reset_mail = mail.outbox.pop()
|
||||
self.assertEqual("reset your password" in password_reset_mail, True)
|
||||
self.assertEqual(challenge.verification_link() in password_reset_mail, True)
|
||||
|
||||
resp = c.get(self.challenge.verification_link())
|
||||
self.assertEqual(resp.status_code == 200)
|
||||
|
||||
new_password = "newpasdasdf234234"
|
||||
|
||||
# passwords don't match
|
||||
payload = {
|
||||
"password": new_password,
|
||||
"confirm_password": self.password,
|
||||
}
|
||||
resp = c.post(self.challenge.verification_link(), payload)
|
||||
self.assertEqual(resp.status_code == 400)
|
||||
|
||||
# change password
|
||||
payload["confirm_password"] = new_password
|
||||
resp = c.post(self.challenge.verification_link(), payload)
|
||||
self.assertEqual(resp.status_code == 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("accounts.login"))
|
||||
|
||||
# verify password changed notification email was sent
|
||||
password_updated_email = mail.outbox.pop()
|
||||
self.assertEqual(
|
||||
"Your password for signing in to Hostea was recently changed. If you made this change, then we're all set."
|
||||
in password_updated_email,
|
||||
True,
|
||||
)
|
||||
self.assertEqual(reverse("accounts.reset.new") in password_updated_email, True)
|
||||
|
||||
# trying to login with old password
|
||||
payload = {
|
||||
"login": self.username,
|
||||
"password": self.password,
|
||||
}
|
||||
resp = self.client.post(reverse("accounts.login"), payload)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
self.assertEqual(b"Login Failed" in resp.content, True)
|
||||
|
||||
payload["password"] = new_password
|
||||
resp = c.post(reverse("accounts.login"), payload)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("accounts.home"))
|
||||
|
||||
|
||||
class RegistrationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.username = "register_user"
|
||||
self.password = "2i3j4;1qlk2asdf"
|
||||
self.email = "register_user@example.com"
|
||||
|
||||
def test_register_template_works(self):
|
||||
"""
|
||||
Tests if register template renders
|
||||
"""
|
||||
resp = self.client.get(reverse("accounts.register"))
|
||||
self.assertEqual(
|
||||
b"A free forge ecosystem for free developers." in resp.content, True
|
||||
)
|
||||
self.assertEqual(b"Free Forge Ecosystem" in resp.content, True)
|
||||
|
||||
def test_register_works(self):
|
||||
"""
|
||||
|
@ -248,54 +102,31 @@ class RegistrationTest(TestCase):
|
|||
|
||||
# passwords don't match
|
||||
msg = {
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"email": self.email,
|
||||
"confirm_password": self.email,
|
||||
"username": "register_user",
|
||||
"password": "password",
|
||||
"email": "register_user@example.com",
|
||||
"confirm_password": "foo@example.com",
|
||||
}
|
||||
resp = c.post(reverse("accounts.register"), msg)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# register user
|
||||
msg["confirm_password"] = self.password
|
||||
msg["confirm_password"] = msg["password"]
|
||||
resp = c.post(reverse("accounts.register"), msg)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
user = get_user_model().objects.get(username=self.username)
|
||||
user = get_user_model().objects.get(username=msg["username"])
|
||||
self.assertEqual(user.is_active, False)
|
||||
challenge = AccountConfirmChallenge.objects.get(owned_by=user)
|
||||
|
||||
# verify is email is sent
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(challenge.verification_link() in mail.outbox[0].body, True)
|
||||
self.assertEqual(mail.outbox[0].to, [self.email])
|
||||
|
||||
pending_url = challenge.pending_url()
|
||||
self.assertEqual(resp.headers["location"], pending_url)
|
||||
resend_url = reverse("accounts.verify.resend", args=(challenge.public_ref,))
|
||||
|
||||
# visit pending URL
|
||||
resp = c.get(pending_url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(str.encode(self.email) in resp.content, True)
|
||||
self.assertEqual(str.encode(resend_url) in resp.content, True)
|
||||
|
||||
resp = c.post(resend_url)
|
||||
resp = c.post(reverse("accounts.verify.resend", args=(challenge.public_ref,)))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], pending_url)
|
||||
|
||||
# check resend
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
|
||||
resp = c.get(challenge.verification_link())
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(
|
||||
str.encode(challenge.verification_link()) in resp.content, True
|
||||
)
|
||||
|
||||
# check verification link in email
|
||||
resp = c.post(challenge.verification_link())
|
||||
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("accounts.login"))
|
||||
user.refresh_from_db()
|
||||
|
@ -335,9 +166,9 @@ class UnverifiedAccountCleanupTets(TestCase):
|
|||
# passwords don't match
|
||||
msg = {
|
||||
"username": username1,
|
||||
"password": "asdklfja;ldkfja;df",
|
||||
"password": "password",
|
||||
"email": f"{username1}@example.com",
|
||||
"confirm_password": "asdklfja;ldkfja;df",
|
||||
"confirm_password": "password",
|
||||
}
|
||||
|
||||
# register user
|
||||
|
@ -368,179 +199,3 @@ class UnverifiedAccountCleanupTets(TestCase):
|
|||
self.assertEqual(
|
||||
get_user_model().objects.filter(username=username2).exists(), False
|
||||
)
|
||||
|
||||
|
||||
class SudoWorks(TestCase):
|
||||
def setUp(self):
|
||||
self.username = "sudo_useworks"
|
||||
register_util(t=self, username=self.username)
|
||||
|
||||
def test_sudo_renders(self):
|
||||
c = Client()
|
||||
|
||||
resp = c.get(reverse("accounts.sudo"))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(
|
||||
resp.headers["location"],
|
||||
f"/accounts/login/?next={reverse('accounts.sudo')}",
|
||||
)
|
||||
|
||||
login_util(t=self, c=c, redirect_to="accounts.home")
|
||||
|
||||
# GET sudo page
|
||||
ctx = {"next": reverse("accounts.home")}
|
||||
sudo_path = f"{reverse('accounts.sudo')}?{urlencode(ctx)}"
|
||||
resp = c.get(sudo_path)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(b"Please login to confirm access" in resp.content, True)
|
||||
|
||||
# Success sudo validation
|
||||
payload = {"password": self.password, "next": ctx["next"]}
|
||||
resp = c.post(reverse("accounts.sudo"), payload)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], ctx["next"])
|
||||
|
||||
# Fail sudo validation
|
||||
payload["password"] = self.username
|
||||
resp = c.post(reverse("accounts.sudo"), payload)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
self.assertEqual(b"Wrong Password" in resp.content, True)
|
||||
|
||||
|
||||
max_sudo_ttl = 5
|
||||
|
||||
|
||||
class MockRequest:
|
||||
def __init__(self, path, session={}):
|
||||
self.path = path
|
||||
self.session = session
|
||||
|
||||
|
||||
class ConfirmAccessDecorator(TestCase):
|
||||
|
||||
# TODO: override to test TTL
|
||||
def test_redirect_to_sudo(self):
|
||||
request = MockRequest(path="/")
|
||||
resp = ConfirmAccess.redirect_to_sudo(request)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
ctx = {"next": request.path}
|
||||
self.assertEqual(
|
||||
resp.headers["location"], f"{reverse('accounts.sudo')}?{urlencode(ctx)}"
|
||||
)
|
||||
|
||||
@override_settings(HOSTEA={"ACCOUNTS": {"SUDO_TTL": max_sudo_ttl}})
|
||||
def test_is_valid(self):
|
||||
request = MockRequest(path="/")
|
||||
|
||||
# request doesn't have sudo authorization data
|
||||
self.assertEqual(ConfirmAccess.is_valid(request), False)
|
||||
|
||||
# authorize sudo
|
||||
ConfirmAccess.set(request)
|
||||
|
||||
# request has sudo authorization data and is valid for this time duration
|
||||
self.assertEqual(ConfirmAccess.is_valid(request), True)
|
||||
|
||||
time.sleep(settings.HOSTEA["ACCOUNTS"]["SUDO_TTL"] + 2)
|
||||
|
||||
# request has sudo authorization data and is not valid for this time duration
|
||||
self.assertEqual(ConfirmAccess.is_valid(request), False)
|
||||
|
||||
def test_validate_decorator_cls_method(self):
|
||||
req = MockRequest(path="/")
|
||||
req.session = {}
|
||||
|
||||
def fn(req, *args, **kwargs):
|
||||
return True
|
||||
|
||||
args = {}
|
||||
kwargs = {}
|
||||
|
||||
# request doesn't have sudo authorization data and is not valid for this time duration
|
||||
resp = ConfirmAccess.validate_decorator(req, fn, *args, **kwargs)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
ctx = {"next": req.path}
|
||||
self.assertEqual(
|
||||
resp.headers["location"], f"{reverse('accounts.sudo')}?{urlencode(ctx)}"
|
||||
)
|
||||
|
||||
# authorize sudo
|
||||
ConfirmAccess.set(req)
|
||||
# request has sudo authorization data and is valid for this time duration
|
||||
self.assertEqual(
|
||||
ConfirmAccess.validate_decorator(req, fn, *args, **kwargs), True
|
||||
)
|
||||
|
||||
def test_validate_decorator(self):
|
||||
req = MockRequest(path="/")
|
||||
req.session = {}
|
||||
|
||||
@confirm_access
|
||||
def fn(req):
|
||||
return True
|
||||
|
||||
args = {}
|
||||
kwargs = {}
|
||||
|
||||
# request doesn't have sudo authorization data and is not valid for this time duration
|
||||
resp = fn(req)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
ctx = {"next": req.path}
|
||||
self.assertEqual(
|
||||
resp.headers["location"], f"{reverse('accounts.sudo')}?{urlencode(ctx)}"
|
||||
)
|
||||
|
||||
# authorize sudo
|
||||
ConfirmAccess.set(req)
|
||||
# request has sudo authorization data and is valid for this time duration
|
||||
self.assertEqual(fn(req), True)
|
||||
|
||||
|
||||
class CreateOidCApplicaiton(TestCase):
|
||||
"""
|
||||
Test command: manage.py create_oidc
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.username = "oidcadmin"
|
||||
register_util(t=self, username=self.username)
|
||||
|
||||
def test_cmd(self):
|
||||
|
||||
Application = get_application_model()
|
||||
|
||||
stdout = StringIO()
|
||||
stderr = StringIO()
|
||||
|
||||
redirect_uri = "http://example.org"
|
||||
app_name = "test_cmd_oidc"
|
||||
|
||||
# username exists
|
||||
call_command(
|
||||
"create_oidc",
|
||||
app_name,
|
||||
self.username,
|
||||
redirect_uri,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
out = stdout.getvalue()
|
||||
|
||||
self.assertIn(f"New application {app_name} created successfully", out)
|
||||
|
||||
client_id = out.split("\n")[1].split(" ")[1]
|
||||
|
||||
self. |