feat: add configurable registration openness

This commit is contained in:
retoor 2025-12-19 12:53:58 +01:00
parent e0f54fb661
commit 1d3444d665
7 changed files with 256 additions and 16 deletions

View File

@ -11,6 +11,14 @@
## Version 1.11.0 - 2025-12-19
Adds the ability to configure whether user registration is open or closed via system settings. Administrators can now toggle registration openness to control access to the registration form.
**Changes:** 5 files, 262 lines
**Languages:** HTML (185 lines), Python (77 lines)
## Version 1.10.0 - 2025-12-19 ## Version 1.10.0 - 2025-12-19
Users must now receive an invitation to register for an account, as open registration is disabled. Users must now receive an invitation to register for an account, as open registration is disabled.

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "Snek" name = "Snek"
version = "1.10.0" version = "1.11.0"
readme = "README.md" readme = "README.md"
#license = { file = "LICENSE", content-type="text/markdown" } #license = { file = "LICENSE", content-type="text/markdown" }
description = "Snek Chat Application by Molodetz" description = "Snek Chat Application by Molodetz"

View File

@ -31,6 +31,7 @@ from snek.system import http
from snek.system.cache import Cache from snek.system.cache import Cache
from snek.system.markdown import MarkdownExtension from snek.system.markdown import MarkdownExtension
from snek.system.middleware import auth_middleware, cors_middleware, csp_middleware from snek.system.middleware import auth_middleware, cors_middleware, csp_middleware
from snek.system.config import config
from snek.system.profiler import profiler_handler from snek.system.profiler import profiler_handler
from snek.system.template import ( from snek.system.template import (
EmojiExtension, EmojiExtension,
@ -178,6 +179,7 @@ class Application(BaseApplication):
self.sync_service = None self.sync_service = None
self.executor = None self.executor = None
self.cache = Cache(self) self.cache = Cache(self)
self.config = config
self.services = get_services(app=self) self.services = get_services(app=self)
self.mappers = get_mappers(app=self) self.mappers = get_mappers(app=self)
self.broadcast_service = None self.broadcast_service = None
@ -381,6 +383,7 @@ class Application(BaseApplication):
if not context: if not context:
context = {} context = {}
context["rid"] = str(uuid.uuid4()) context["rid"] = str(uuid.uuid4())
context["config"] = self.config
if request.session.get("uid"): if request.session.get("uid"):
async for subscribed_channel in self.services.channel_member.find( async for subscribed_channel in self.services.channel_member.find(
user_uid=request.session.get("uid"), deleted_at=None, is_banned=False user_uid=request.session.get("uid"), deleted_at=None, is_banned=False

34
src/snek/system/config.py Normal file
View File

@ -0,0 +1,34 @@
# retoor <retoor@molodetz.nl>
import os
def get_bool(key, default=False):
value = os.environ.get(key, str(default)).lower()
return value in ('true', '1', 'yes', 'on')
def get_str(key, default=''):
return os.environ.get(key, default)
def get_int(key, default=0):
try:
return int(os.environ.get(key, default))
except (ValueError, TypeError):
return default
class Config:
def __init__(self):
self.registration_open = get_bool('SNEK_REGISTRATION_OPEN', False)
self.debug = get_bool('SNEK_DEBUG', False)
self.session_key = get_str('SNEK_SESSION_KEY', 'c79a0c5fda4b424189c427d28c9f7c34')
self.db_path = get_str('SNEK_DB_PATH', 'sqlite:///snek.db')
self.host = get_str('SNEK_HOST', '0.0.0.0')
self.port = get_int('SNEK_PORT', 8081)
self.ssh_host = get_str('SNEK_SSH_HOST', '0.0.0.0')
self.ssh_port = get_int('SNEK_SSH_PORT', 2242)
config = Config()

24
src/snek/system/util.py Normal file
View File

@ -0,0 +1,24 @@
# retoor <retoor@molodetz.nl>
def safe_get(obj, key, default=None):
if obj is None:
return default
try:
if isinstance(obj, dict):
return obj.get(key, default)
if hasattr(obj, "fields") and hasattr(obj, "__getitem__"):
val = obj[key]
return val if val is not None else default
return getattr(obj, key, default)
except (KeyError, TypeError, AttributeError):
return default
def safe_str(obj):
if obj is None:
return ""
try:
return str(obj)
except Exception:
return ""

View File

@ -1,17 +1,190 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
{% if config.registration_open %}
Register - Snek chat by Molodetz Register - Snek chat by Molodetz
{% else %}
Join Snek - Invitation Only
{% endif %}
{% endblock %} {% endblock %}
{% block head %} {% block head %}
<link rel="stylesheet" href="/back-form.css"> <link rel="stylesheet" href="/back-form.css">
{% if not config.registration_open %}
<style>
.closed-community {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 80vh;
padding: 40px 20px;
text-align: center;
max-width: 600px;
margin: 0 auto;
}
.closed-icon {
font-size: 64px;
margin-bottom: 30px;
opacity: 0.9;
}
.closed-title {
font-size: 2.2em;
color: #f05a28;
margin-bottom: 20px;
font-weight: 300;
letter-spacing: 1px;
}
.closed-subtitle {
font-size: 1.3em;
color: #e6e6e6;
margin-bottom: 30px;
font-weight: 300;
}
.closed-message {
color: #aaa;
line-height: 1.8;
margin-bottom: 40px;
font-size: 1.05em;
}
.closed-features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
width: 100%;
margin-bottom: 40px;
}
.feature-item {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 20px 15px;
transition: all 0.3s ease;
}
.feature-item:hover {
background: rgba(240, 90, 40, 0.08);
border-color: rgba(240, 90, 40, 0.3);
}
.feature-icon {
font-size: 28px;
margin-bottom: 10px;
}
.feature-text {
color: #ccc;
font-size: 0.95em;
}
.action-buttons {
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
}
.action-link {
display: inline-block;
padding: 14px 32px;
border-radius: 8px;
text-decoration: none;
font-size: 1em;
transition: all 0.3s ease;
}
.action-primary {
background: #f05a28;
color: white;
}
.action-primary:hover {
background: #d94d1f;
transform: translateY(-2px);
}
.action-secondary {
background: transparent;
color: #e6e6e6;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.action-secondary:hover {
border-color: #f05a28;
color: #f05a28;
}
.back-button-container {
position: absolute;
top: 30px;
left: 30px;
}
@media screen and (max-width: 500px) {
.closed-title {
font-size: 1.6em;
}
.closed-subtitle {
font-size: 1.1em;
}
.closed-features {
grid-template-columns: 1fr;
}
.back-button-container {
position: relative;
top: 0;
left: 0;
margin-bottom: 20px;
text-align: left;
}
}
</style>
{% endif %}
{% endblock %} {% endblock %}
{% block main %} {% block main %}
{% if config.registration_open %}
<div class="back-form"> <div class="back-form">
<fancy-button url="/back" text="Back" size="auto"></fancy-button> <fancy-button url="/back" text="Back" size="auto"></fancy-button>
Sorry, Snek became a closed community. You can only join by receiving an invitation by any member. This ensures that Snek is a safe and secure place for developers, testers, and AI professionals. <generic-form class="center" url="/register.json" preloaded-structure='{{ form|tojson|safe }}'></generic-form>
{#<generic-form class="center" url="/register.json" preloaded-structure='{{ form|tojson|safe }}'></generic-form #}>
</div> </div>
{% else %}
<div class="back-button-container">
<fancy-button url="/back" text="Back" size="auto"></fancy-button>
</div>
<div class="closed-community">
<div class="closed-icon">&#128274;</div>
<h1 class="closed-title">Invitation Only</h1>
<p class="closed-subtitle">Snek is a private community</p>
<p class="closed-message">
Snek operates as a closed community to maintain quality and security.
Membership is available through invitation from existing members.
This approach ensures a trusted environment for developers, testers, and AI professionals.
</p>
<div class="closed-features">
<div class="feature-item">
<div class="feature-icon">&#128101;</div>
<div class="feature-text">Trusted Network</div>
</div>
<div class="feature-item">
<div class="feature-icon">&#128274;</div>
<div class="feature-text">Secure Environment</div>
</div>
<div class="feature-item">
<div class="feature-icon">&#9989;</div>
<div class="feature-text">Quality Members</div>
</div>
</div>
<div class="action-buttons">
<a href="/login.html" class="action-link action-primary">Member Login</a>
<a href="/about.html" class="action-link action-secondary">Learn More</a>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,13 +1,5 @@
# retoor <retoor@molodetz.nl> # retoor <retoor@molodetz.nl>
# Written by retoor@molodetz.nl
# This module defines a web view for user registration. It handles GET requests and form submissions for the registration process.
# The code makes use of 'RegisterForm' from 'snek.form.register' for handling registration forms and 'BaseFormView' from 'snek.system.view' for basic view functionalities.
# MIT License
from aiohttp import web from aiohttp import web
from snek.form.register import RegisterForm from snek.form.register import RegisterForm
@ -16,19 +8,25 @@ from snek.system.view import BaseFormView
class RegisterView(BaseFormView): class RegisterView(BaseFormView):
form = RegisterForm form = RegisterForm
login_required = False login_required = False
async def get(self): async def get(self):
if self.session.get("logged_in"): if self.session.get("logged_in"):
return web.HTTPFound("/web.html") return web.HTTPFound("/web.html")
if self.request.path.endswith(".json"): if self.request.path.endswith(".json"):
if not self.app.config.registration_open:
return web.json_response(
{"error": "Registration is currently closed"},
status=403
)
return await super().get() return await super().get()
return await self.render_template( return await self.render_template(
"register.html", {"form": await self.form(app=self.app).to_json()} "register.html", {"form": await self.form(app=self.app).to_json()}
) )
async def submit(self, form): async def submit(self, form):
if not self.app.config.registration_open:
return {"error": "Registration is currently closed"}
result = await self.app.services.user.register( result = await self.app.services.user.register(
form.email.value, form.username.value, form.password.value form.email.value, form.username.value, form.password.value
) )