first commit

This commit is contained in:
meewan 2020-10-17 22:04:19 +02:00
parent d19659c4f8
commit d5e61053f6
10 changed files with 614 additions and 1 deletions

View File

@ -1,3 +1,15 @@
# nextcloudregister
Nextcloud register form without using a nextclooud app.
Nextcloud register form without using a nextcloud app.
# installation
To install you need to:
* install flask and requests
* clone the repo in the /site directory
* copy the etc content in /etc
* link /etc/nextcloudregister/nextcloudregister.service to /etc/systemd/system/nextcloudregister.service
* configure nextcloudregister fot your nextcloud instance in the file /etc/nextcloudregister/config.ini
By default it will serve on port 9000

0
__init__.py Normal file
View File

View File

@ -0,0 +1,49 @@
[api]
# api configuration
# username to authenticate on the api
username = admin
# password to use on the api
password = secret
# domain where is the nexcloud instance
domain = cloud.example.com
# whether or not we should use https
use_https = yes
# should we require a valid ssl certificate ?
# note: this option is ignored if use_https if false
check_certificate = yes
# nexcloud api base uri
# the part after the domain but before the enpoint itself
# you should not need to change it
base_uri = /ocs/v1.php/cloud
[web]
# configuration for the web part and the user interaction
# path to display the form
base_uri =
# color to match the nextcloud theme
color = #0082c9
# link to the nexcloud instance landing page
instance_link = https://cloud.example.com/index.php/login
# link to the host organisation
org_link = https://example.com
# link to the favicon to use. Fo not use a small one as it is displayed
# in the form too
favicon = favicon.ico
# link to the EULA for this service
eula = https://example.com/eula
[rules]
# all business rules
# maximum account authorized on the nextcloud instance.
# 0 means unlimited
max_accounts = 50
# whether or not the user must give an email to subscribe
# at least one of email and password is mandatory
mandatory_email = yes
# whether or not the user must give an password to subscribe
# at least one of email and password is mandatory
mandatory_password = yes

View File

@ -0,0 +1,13 @@
[Unit]
Description=Nextcloudregister a service to register in nextcloud
After=local-fs.target
Wants=network-online.target
[Service]
Environment=PYTHONPATH=/usr/lib/python3/dist-packages/:/sites/
ExecStart=/usr/bin/uwsgi-core --ini /etc/nextcloudregister/uwsgi.ini
User=nextcloudregister
Group=nextcloudregister
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,5 @@
[uwsgi]
socket = :9090
protocol = http
wsgi-file = /sites/nextcloudregister/webapp.py
plugin = python3

194
lib.py Normal file
View File

@ -0,0 +1,194 @@
from configparser import ConfigParser
import requests
CONFIG_PATH = "/etc/nextcloudregister/config.ini"
class NextcloudApiException(Exception):
# base exception for unknown or undocumented errors
pass
class NextcloudApiInvalidInputData(NextcloudApiException):
# data geven to the api were not valid
pass
class NextcloudApiUsernamealreadyExists(NextcloudApiException):
# try to create a user with an already existing username
pass
class NextcloudApiPermissionDenied(NextcloudApiException):
# Not enough right to perform that action
pass
class NextcloudApiUnknownError(NextcloudApiException):
# generic error with an hint
def __init__(self, hint):
self.hint = hint
class NextcloudApiNoEmailPassword(NextcloudApiException):
# At least one of email or password is mandatory
pass
class NextcloudApiCannotSendEmail(NextcloudApiException):
# invitation email not sent
pass
class NextcloudApi:
"""Class that abstract the Nexcloud api to handle the user creation. This
class only abstract a small part of the api.
:param str config_path: path to the configfile for the application
"""
def __init__(self, config_path=CONFIG_PATH):
self.config_path = config_path
@property
def config(self):
"""lazy loading for the configuration to make the class free to
instanciate
"""
if not hasattr(self, "_config"):
self._config = ConfigParser()
self._config.read(self.config_path)
return self._config
def count_accounts(self):
"""Returns the number of accounts already existing on this nextcloud
instance
:rtype: int
:returns the number of accounts
:raise: NextcloudApiException if the api was not available
"""
raw_dict = self._request(endpoint="users")
users = raw_dict['ocs']['data'].get('users', [])
return len(users)
def create_account(self, username, password=None, email=None):
"""Try to create a new user account using the provided data.
:param str username: the username to use for this account
:param str password: the password for the new user. optional is an
email is given
:param str email: the email for the new user. optional if a password is
given
:raise:
ValueError: if the email and the password are empty
NextcloudApiException: if the api was not available or an unknow
error ocured
NextcloudApiInvalidInputData: the given data are somwhat invalid
NextcloudApiUsernamealreadyExists: the given username is already
used
NextcloudApiPermissionDenied: the given user in the options has no
right to create a user
NextcloudApiUnknownError: un anknow error acured but with an hint
NextcloudApiCannotSendEmail: could not send the invitation email
"""
data = {
"userid": username,
}
if password:
data["password"] = password
if email:
data["email"] = email
result = self._request(
endpoint="users",
data=data,
verb="POST",
)
status = result['ocs']['meta']['statuscode']
if status == 101:
raise NextcloudApiInvalidInputData()
elif status == 102:
raise NextcloudApiUsernamealreadyExists()
elif status == 103:
raise NextcloudApiException("103")
elif status == 105:
raise NextcloudApiPermissionDenied()
elif status == 107:
raise NextcloudApiUnknownError(
hint=result['ocs']['meta']['message']
)
elif status == 108:
raise NextcloudApiNoEmailPassword()
elif status == 109:
NextcloudApiCannotSendEmail()
elif status != 100:
raise NextcloudApiException(str(status))
def _request(self, endpoint, *, data=None, verb="GET"):
"""Abstract most of the boilerplate work to make a restapi call to
nextcloud.
:rtype: dict
:returns: the api answer to the request
:raise: NextcloudApiException if the api was not available
"""
if verb.upper() not in ("GET", "POST", "PUT", "DELETE"):
raise ValueError("Unknown http verb")
url = self._get_url(endpoint)
headers = {
"OCS-APIRequest": "true",
"Accept": "application/json",
}
if verb.upper() == "POST":
headers["Content-Type"] = "application/x-www-form-urlencoded"
url = f"{url}?format=json"
method = getattr(requests, verb.lower())
kwargs = {
"headers": headers,
"auth": (
self.config.get("api", "username"),
self.config.get("api", "password")
),
"data": data,
}
if self.config.getboolean("api", "use_https"):
if not self.config.getboolean("api", "check_certificate"):
kwargs["verify"] = False
result = method(url, **kwargs)
if result.status_code not in (200, 204):
raise NextcloudApiException(
"Nextcloud api unavailable code: %s" % result.status_code
)
return result.json()
def _get_url(self, endpoint):
"""Builds the url to call for the iven endpoint of api
:rtype: str
:returns: the full url with scheme for the given endpoint
"""
url = ""
# build protocol scheme
url += "http"
if self.config.getboolean("api", "use_https"):
url += "s"
url += "://"
# add domain
url += self.config.get("api", "domain")
# add fullpath to endpoint
url = "/".join(
(
url,
self.config.get("api", "base_uri"),
endpoint,
)
)
return url

2
requirement/webapp.txt Normal file
View File

@ -0,0 +1,2 @@
flask
requests

54
templates/form.html Normal file
View File

@ -0,0 +1,54 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<link rel="icon" href="{{ favicon }}">
<title>Nextcloud registration form</title>
<link rel="stylesheet" href="{{base_uri}}style.css">
{% if success and instance_link %}
<meta http-equiv="refresh" content="10;URL={{ instance_link }}">
{% endif %}
</head>
<body>
<nav class="navbar">
<div>
</div>
</nav>
<div id="main">
{% if favicon %}
<img src={{favicon}} class="favicon"/>
{% endif %}
{% if max_accounts %}
<div class="count">
Il reste {{ count_accounts }} / {{ max_accounts }} comptes.
</div>
{% endif %}
{% if error %}
<div class="error message">
{{ error }}
</div>
{% endif %}
{% if info %}
<div class="info message">
{{ info }}
</div>
{% endif %}
<form method="POST">
<input type="text" name="username" placeholder="Nom d'utilisateur" value="{{ data.username }}" {% if disable %} disabled="disabled" {% endif %}/>
{% if mandatory_email %}
<input type="text" name="email" placeholder="Adresse email" value="{{ data.email }}" {% if disable %} disabled="disabled" {% endif %}/>
{% endif %}
{% if mandatory_password %}
<input type="password" name="password1" placeholder="Mot de passe" {% if disable %} disabled="disabled" {% endif %}/>
<input type="password" name="password2" placeholder="Confirmez le mot de passe" {% if disable %} disabled="disabled" {% endif %}/>
{% endif %}
{% if eula %}
<div class="eula">
En m'inscrivant à ce service j'accepte ses <a href="{{ eula }}">Conditions Générales d'utilisation</a>
</div>
{% endif %}
<input type="submit" name="validate" value="S'inscrire" {% if disable %} disabled="disabled" {% endif %}/>
</form>
</div>
</body>
</html>

78
templates/style.css Normal file
View File

@ -0,0 +1,78 @@
body
{
margin: 0;
}
input
{
display: block;
height:25px;
border-radius: 12.5px;
width:440px;
margin:20px;
border-width: 1px;
border-style: solid;
padding-left: 10px;
padding-right: 10px;
border-color: {{ color }};
}
input[type=submit]
{
color:white;
width:460px;
background-color: {{ color }};
}
.favicon
{
display:block;
margin: 0 auto;
max-width: 100px;
}
.navbar
{
color:white;
height:50px;
width:100%;
background-color: {{ color }};
}
#main
{
padding-top:20px;
margin:auto;
width:500px;
}
.eula
{
margin-left: 50px;
}
.error
{
background-color: #fff0f0;
border-color: #ff0000;
}
.info
{
background-color: #f0f0ff;
border-color: #0000ff;
}
.message
{
border-style: solid;
border-width: 1px;
border-radius: 12.5px;
width:440px;
margin:20px;
padding-left: 10px;
padding-right: 10px;
padding-top: 3px;
padding-bottom: 3px;
}
.count
{
text-align: center;
}

206
webapp.py Normal file
View File

@ -0,0 +1,206 @@
from configparser import ConfigParser
from flask import (
Flask,
request,
render_template,
Response,
)
from nextcloudregister.lib import (
CONFIG_PATH,
NextcloudApi,
NextcloudApiCannotSendEmail,
NextcloudApiException,
NextcloudApiInvalidInputData,
NextcloudApiNoEmailPassword,
NextcloudApiPermissionDenied,
NextcloudApiUsernamealreadyExists,
NextcloudApiUnknownError,
)
config = ConfigParser()
config.read(CONFIG_PATH)
app = Flask(__name__)
base_uri = config.get("web", "base_uri", fallback="")
base_uri = base_uri + ("" if base_uri.endswith("/") else "/")
@app.route(base_uri, methods=["GET", "POST"])
def form_manager():
if request.method == "POST":
return form_post()
else:
return form_get()
def form_get(data=None, error=None, info=None, success=False):
context = {
"base_uri": base_uri,
"data": data or {},
"disable": success,
"error": error,
"eula": config.get("web", "eula", fallback=""),
"favicon": config.get("web", "favicon", fallback=""),
"info": info,
"instance_link": config.get("web", "instance_link", fallback=""),
"max_accounts": config.getint("rules", "max_accounts", fallback=0),
"org_link": config.get("web", "org_link", fallback=""),
"success": success,
}
context["mandatory_password"] = config.getboolean(
"rules",
"mandatory_password",
fallback=True,
)
context["mandatory_email"] = config.getboolean(
"rules",
"mandatory_email",
fallback=False,
)
count_accounts = 0
api = NextcloudApi()
try:
count_accounts = api.count_accounts()
except NextcloudApiException:
context["max_accounts"] = 0
if count_accounts >= context["max_accounts"] and not success:
context["disable"] = True
context["error"] = (
"Tout les comptes disponibles sur cette instance ont deja été "
"distribués."
)
else:
context["count_accounts"] = context["max_accounts"] - count_accounts
return render_template("form.html", **context)
def form_post():
mandatory_email = config.getboolean(
"rules",
"mandatory_email",
fallback=False,
)
mandatory_password = config.getboolean(
"rules",
"mandatory_password",
fallback=False,
)
# validate mandatory fields
if not request.form.get("username"):
return form_get(
data=request.form,
error="Un nom d'utilisateur est obligatoire pour vous inscrire."
)
if mandatory_email and not request.form.get("email"):
return form_get(
data=request.form,
error="Une adresse mail est obligatoire pour vous inscrire."
)
if mandatory_password and not request.form.get("password1"):
return form_get(
data=request.form,
error="Un mot de passe est obligatoire pour vous inscrire."
)
# validate password
if request.form.get("password1"):
password1 = request.form.get("password1")
password2 = request.form.get("password2")
if not password1 == password2:
return form_get(
data=request.form,
error="Les mots de passe ne correspondent pas."
)
api = NextcloudApi()
try:
api.create_account(
username=request.form.get("username"),
password=request.form.get("password1"),
email=request.form.get("email")
)
except NextcloudApiCannotSendEmail:
return form_get(
data=request.form,
error="Impossible d'envoyer un email pour la creation du compte."
)
except NextcloudApiInvalidInputData:
return form_get(
data=request.form,
error=(
"Erreur interne. Merci de contacter l'administrateur de "
"l'instance Nextcloud."
)
)
except NextcloudApiNoEmailPassword:
return form_get(
data=request.form,
error=(
"Une addresse email ou un mot de passe sont obligatoires pour "
"créer un compte Nextcloud."
)
)
except NextcloudApiPermissionDenied:
return form_get(
data=request.form,
error=(
"Erreur interne. Merci de contacter l'administrateur de "
"l'instance Nextcloud. (Permission denied)"
)
)
except NextcloudApiUsernamealreadyExists:
return form_get(
data=request.form,
error=(
"Le nom d'utilisateur que vous avez choisi est déjà utilisé "
"sur cette instance Nextcloud."
)
)
except NextcloudApiUnknownError as err:
return form_get(
data=request.form,
error=err.hint
)
except NextcloudApiException as err:
return form_get(
data=request.form,
error=(
"Erreur interne. Merci de contacter l'administrateur de "
"l'instance Nextcloud. (%s)" % err
)
)
except Exception:
return form_get(
data=request.form,
error=(
"Erreur interne. Merci de contacter l'administrateur de "
"l'instance Nextcloud."
)
)
if config.get("web", "instance_link", fallback=""):
return form_get(
info=(
"L'inscription est un succes, vous allez maintenat être "
"redirigé vers votre instance Nextcloud."
),
success=True,
)
else:
return form_get(
info="Vous êtes maintenat inscrit sur cette instance Nextcloud",
success=True,
)
@app.route(base_uri + "style.css")
def style():
context = {
"color": config.get("web", "color")
}
css_content = render_template("style.css", **context)
return Response(css_content, mimetype="text/css")
application = app