Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

194 changed files with 159 additions and 7242 deletions

View File

@ -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

View File

@ -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=""

3
.gitignore vendored
View File

@ -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/

View File

@ -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

View File

@ -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" ]

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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}")

View File

@ -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,
),
),
],
),
]

View File

@ -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,))

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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">

View File

@ -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">

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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"
>&nbsp; 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>

View File

@ -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>

View File

@ -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.