Compare commits
75 Commits
add-key-fo
...
master
Author | SHA1 | Date |
---|---|---|
vilmibm | afc5d30ed8 | |
vilmibm | 8d04787277 | |
vilmibm | be2975ad62 | |
vilmibm | 0f078f86fd | |
vilmibm | 90dca0274a | |
vilmibm | 158bdcff5f | |
vilmibm | 77d728a153 | |
vilmibm | 2385446e74 | |
vilmibm | 5220d9bfad | |
vilmibm | 4e3977edd2 | |
vilmibm | 2354695401 | |
vilmibm | de07b42082 | |
Nate Smith | 3d12a5bfd4 | |
Nate Smith | 32e640b875 | |
Nate Smith | 02a50e1d11 | |
Nate Smith | dfd843064d | |
Nate Smith | 31adb3cc3a | |
Nate Smith | e59bd893fd | |
Nate Smith | a2af7c5c96 | |
Nate Smith | 9373589c52 | |
Nate Smith | 2c71afebc2 | |
Nate Smith | e9d1fea15e | |
vilmibm | ffeb999290 | |
vilmibm | 0881982614 | |
Nate Smith | 4eb72edf42 | |
vilmibm | 3cb39206c1 | |
Nate Smith | 1757ec85c8 | |
vilmibm | 627ccf6e55 | |
vilmibm | b33c244d7b | |
vilmibm | f56e12a053 | |
vilmibm | a8917a05c3 | |
Nate Smith | 57e18fb337 | |
vilmibm | 677f5ff2ee | |
vilmibm | 92177e7a66 | |
vilmibm | d316fef6b6 | |
vilmibm | 5271fdc8f5 | |
vilmibm | 330ab81d63 | |
vilmibm | 08ff82094d | |
Nate Smith | 3c4eabd42e | |
Mallory Hancock | 9f9fbf949b | |
Mallory Hancock | 3879017dd1 | |
vilmibm | 31e0ef4add | |
vilmibm | 1425e48c02 | |
vilmibm | 165b79d0b6 | |
vilmibm | 372e1bf84d | |
vilmibm | 3aa777f253 | |
vilmibm | eca5242e37 | |
vilmibm | 99d6b1a1c4 | |
vilmibm | 7d4b64b111 | |
vilmibm | 70a5fc31de | |
vilmibm | 9e54e59628 | |
vilmibm | ca1500d265 | |
vilmibm | f095c5ec7f | |
vilmibm | 14639d78f8 | |
vilmibm | 8be382da87 | |
vilmibm | 7fc039559e | |
Nate Smith | a908079ba9 | |
nate | 1bdaede8c2 | |
nate | 169df3e84d | |
nate | b25ad2de8e | |
nate | 13edcad576 | |
Nate Smith | 747f1e6f17 | |
nate | 48bebb7386 | |
nate | 3d58870f34 | |
nate | 6ec813a328 | |
nate | ae4ce741ba | |
nate | 0a5dae85a3 | |
nate | f7bbdf3f0f | |
nate | 7da85faeed | |
nate | 0d8b370b5d | |
nate | 33fee98309 | |
nate | e150e07001 | |
nate | ae69c79a51 | |
nate | 76c476f258 | |
nate | 61e5e3b4fc |
|
@ -3,3 +3,11 @@
|
|||
__pycache__
|
||||
*.pyc
|
||||
*.egg-info
|
||||
.bash_history
|
||||
.viminfo
|
||||
ttadmin/settings_live.py
|
||||
ttadmin/static
|
||||
venv/
|
||||
build/
|
||||
dist/
|
||||
|
||||
|
|
|
@ -10,13 +10,14 @@ _Being an adminstrative tool written in Django for <https://tilde.town>_.
|
|||
* [x] Helpdesk
|
||||
* [x] User account management
|
||||
* [ ] Start/stop services
|
||||
* [ ] Cost reporting from AWS
|
||||
* [ ] Cost reporting from DO
|
||||
* [ ] Status monitoring
|
||||
|
||||
## Requirements
|
||||
|
||||
* Python 3.5+
|
||||
* PostgreSQL 9+
|
||||
* Ubuntu or Debian
|
||||
|
||||
## Installation / setup
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env python3
|
||||
"""This script wraps the usermod command to allow user account renames via
|
||||
sudoers."""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
|
||||
def rename_user(old_username, new_username):
|
||||
"""Given an old and a new username, renames user on disk with usermod.
|
||||
Raises if the usermod call fails."""
|
||||
|
||||
args = [
|
||||
'pkill',
|
||||
'-u',
|
||||
old_username]
|
||||
subprocess.run(args, check=False)
|
||||
|
||||
# Rename user
|
||||
args = [
|
||||
'usermod',
|
||||
'-l',
|
||||
new_username,
|
||||
'-m',
|
||||
'-d',
|
||||
os.path.join('/home', new_username),
|
||||
old_username
|
||||
]
|
||||
subprocess.run(args, check=True)
|
||||
|
||||
# Rename their group
|
||||
args = [
|
||||
'groupmod',
|
||||
'-n',
|
||||
new_username,
|
||||
old_username
|
||||
]
|
||||
subprocess.run(args, check=True)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 3:
|
||||
print('[rename_user] Too few arguments passed.', file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
rename_user(argv[1], argv[2])
|
||||
except subprocess.CalledProcessError as e:
|
||||
print('[rename_user] {}'.format(e), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main(sys.argv))
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
PORT=8888
|
||||
INSTALL_ROOT=/town/src/tildetown-admin
|
||||
APP_ROOT=$INSTALL_ROOT/ttadmin
|
||||
|
||||
VENV=/town/venvs/ttadmin
|
||||
|
||||
source $VENV/bin/activate
|
||||
export DJANGO_SETTINGS_MODULE=settings_live
|
||||
cd $APP_ROOT
|
||||
gunicorn -t120 -b0.0.0.0:$PORT ttadmin.wsgi
|
|
@ -14,11 +14,14 @@
|
|||
|
||||
* create ttadmin user
|
||||
* ttadmin db user (or just rely on ident..?) / database created
|
||||
* copy `create_keyfile.py` from `scripts/` and put it in `/opt/bin/`.
|
||||
* `chmod o+x /opt/bin/create_keyfile.py``
|
||||
* copy `create_keyfile.py` from `scripts/` and put it in `/opt/bin/`.
|
||||
* copy `rename_user.py` from `scripts/` and put it in `/tilde/bin/`.
|
||||
* `chmod u+x /opt/bin/create_keyfile.py``
|
||||
* add to sudoers:
|
||||
|
||||
ttadmin ALL=(ALL)NOPASSWD:/usr/sbin/adduser,/bin/mkdir,/opt/bin/create_keyfile.py
|
||||
```
|
||||
ttadmin ALL=(ALL)NOPASSWD:/usr/sbin/adduser,/bin/mkdir,/opt/bin/create_keyfile.py,/tilde/bin/rename_user.py
|
||||
```
|
||||
|
||||
* have virtualenv with python 3.5+ ready, install tildetown-admin package into it
|
||||
* run django app as wsgi container through gunicorn as the ttadmin user with venv active
|
||||
|
|
9
setup.py
9
setup.py
|
@ -15,12 +15,11 @@ setup(
|
|||
'License :: OSI Approved :: Affero GNU General Public License v3 (AGPLv3)',
|
||||
],
|
||||
packages=['ttadmin'],
|
||||
install_requires = ['Django==1.10.2',
|
||||
install_requires = ['Django==1.11.29',
|
||||
'sshpubkeys==2.2.0',
|
||||
'psycopg2-binary==2.7.4',
|
||||
'requests==2.12.5',
|
||||
'psycopg2-binary==2.8.5',
|
||||
'gunicorn==19.6.0',
|
||||
'Mastodon.py==1.1.1',
|
||||
'tweepy==3.5.0'],
|
||||
'Mastodon.py==1.4.5',
|
||||
'tweepy==3.7.0'],
|
||||
include_package_data = True,
|
||||
)
|
||||
|
|
|
@ -4,15 +4,15 @@ from random import shuffle
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.forms import ChoiceField
|
||||
|
||||
CAPTCHA_CHOICES = [('two', 'zorp borp'),
|
||||
('three', 'quop bop'),
|
||||
('four', 'NO, I AM NOT A ROBOT'),
|
||||
('five', 'crackle zop'),
|
||||
('six', '*rusty screech*'),
|
||||
('seven', 'mother, give me legs')]
|
||||
CAPTCHA_CHOICES = [('number', 'zorp borp'),
|
||||
('hey', 'quop bop'),
|
||||
('never', 'NO, I AM NOT A ROBOT'),
|
||||
('eleven', 'crackle zop'),
|
||||
('twelve', '*rusty screech*'),
|
||||
('eighty', 'mother, give me legs')]
|
||||
shuffle(CAPTCHA_CHOICES)
|
||||
CAPTCHA_CHOICES.insert(0, ('one', 'beep boop'),)
|
||||
NOT_A_ROBOT = 'four'
|
||||
NOT_A_ROBOT = 'never'
|
||||
|
||||
def validate_captcha(captcha):
|
||||
if captcha != NOT_A_ROBOT:
|
||||
|
|
|
@ -1,32 +1,26 @@
|
|||
import logging
|
||||
|
||||
import requests
|
||||
from smtplib import SMTP_SSL, SMTPException
|
||||
from email.message import EmailMessage
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
FROM='root@tilde.town'
|
||||
|
||||
def send_email(to, body, subject='a message from tilde.town', frum=FROM,):
|
||||
"""Sends an email using mailgun. Logs on failure."""
|
||||
response = requests.post(
|
||||
settings.MAILGUN_URL,
|
||||
auth=('api', settings.MAILGUN_KEY),
|
||||
data={
|
||||
'from': frum,
|
||||
'to': to,
|
||||
'subject': subject,
|
||||
'text': body
|
||||
}
|
||||
)
|
||||
def send_email(to, body, subject='a message from tilde.town'):
|
||||
"""Sends an email using external SMTP. Logs on failure."""
|
||||
em = EmailMessage()
|
||||
em['Subject'] = subject
|
||||
em['From'] = 'root@tilde.town'
|
||||
em['To'] = to
|
||||
em.set_content(body)
|
||||
try:
|
||||
with SMTP_SSL(port=settings.SMTP_PORT, host=settings.SMTP_HOST) as smtp:
|
||||
smtp.login('root@tilde.town', settings.SMTP_PASSWORD)
|
||||
smtp.send_message(em)
|
||||
smtp.quit()
|
||||
except SMTPException as e:
|
||||
logger.error(f'failed to send email "{subject}" to {to}: {e}')
|
||||
return False
|
||||
|
||||
success = response.status_code == 200
|
||||
|
||||
if not success:
|
||||
logger.error('{}: failed to send email "{}" to {}'.format(
|
||||
response.status_code,
|
||||
subject,
|
||||
to))
|
||||
|
||||
return success
|
||||
return True
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>tilde.town guestbook</h1>
|
||||
<p><em>don't try to post urls. it won't work.</em></p>
|
||||
<marquee>~*~*~*~*say hello*~*~*~*~</marquee>
|
||||
<form class="tilde" action="{% url 'guestbook:guestbook' %}" method="post">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import re
|
||||
|
||||
from django.shortcuts import redirect
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic.edit import FormView
|
||||
|
@ -5,6 +7,8 @@ from django.views.generic.edit import FormView
|
|||
from .forms import GuestbookForm
|
||||
from .models import GuestbookMessage
|
||||
|
||||
SUSPICIOUS_RE = re.compile(r'https?://')
|
||||
|
||||
|
||||
class GuestbookView(FormView):
|
||||
form_class = GuestbookForm
|
||||
|
@ -17,5 +21,7 @@ class GuestbookView(FormView):
|
|||
|
||||
def form_valid(self, form):
|
||||
del form.cleaned_data['captcha']
|
||||
if SUSPICIOUS_RE.search(form.cleaned_data['msg']) != None:
|
||||
return redirect('guestbook:guestbook')
|
||||
t = GuestbookMessage.objects.create(**form.cleaned_data)
|
||||
return redirect('guestbook:guestbook')
|
||||
|
|
|
@ -1,7 +1,44 @@
|
|||
from django.contrib import admin
|
||||
from .models import Ticket
|
||||
from django.forms import ModelForm
|
||||
from .models import Ticket, Note
|
||||
|
||||
|
||||
class ImmutableNoteInline(admin.TabularInline):
|
||||
model = Note
|
||||
extra = 1
|
||||
max_num = 0
|
||||
fields = ('author', 'created', 'body')
|
||||
readonly_fields = ('author', 'created', 'body')
|
||||
can_delete = False
|
||||
ordering = ('created',)
|
||||
|
||||
|
||||
class NewNoteInline(admin.StackedInline):
|
||||
model = Note
|
||||
extra = 0
|
||||
fields = ('body',)
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super().get_queryset(request)
|
||||
return queryset.none()
|
||||
|
||||
|
||||
@admin.register(Ticket)
|
||||
class TicketAdmin(admin.ModelAdmin):
|
||||
list_display = ('issue_status', 'issue_type', 'name', 'email')
|
||||
fields = ('name', 'email', 'issue_status', 'issue_type', 'issue_text')
|
||||
inlines = [ImmutableNoteInline, NewNoteInline]
|
||||
readonly_fields = ('submitted', 'issue_type')
|
||||
list_display = ('submitted', 'issue_status', 'assigned', 'issue_type', 'name', 'email',)
|
||||
list_filter = ('issue_status', 'issue_type', 'assigned')
|
||||
fields = ('submitted', 'name', 'email', 'assigned', 'issue_status', 'issue_type', 'issue_text')
|
||||
|
||||
def save_related(self, request, form, formsets, change):
|
||||
# THIS IS EXTREMELY BOOTLEG AND MAY BREAK IF MORE INLINES ARE ADDED TO THIS ADMIN.
|
||||
for formset in formsets:
|
||||
if len(formset.forms) == 1:
|
||||
# It's probably the add new note form (i hope).
|
||||
note_form = formset.forms[0]
|
||||
note_form.instance.author = request.user
|
||||
note_form.instance.save()
|
||||
note_form.save(commit=False)
|
||||
note_form.save_m2m()
|
||||
return super().save_related(request, form, formsets, change)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-01-19 18:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('help', '0003_auto_20171110_2323'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='submitted',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-07-16 02:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0015_remove_townie_reviewed'),
|
||||
('help', '0004_ticket_submitted'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Note',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('body', models.TextField()),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Townie')),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='help.Ticket')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-07-16 02:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('help', '0005_note'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='note',
|
||||
name='author',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-07-18 22:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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),
|
||||
('help', '0006_auto_20190716_0258'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='assigned',
|
||||
field=models.ForeignKey(help_text='Assign this ticket to an admin or unassign it.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-07-18 22:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('help', '0007_ticket_assigned'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
name='assigned',
|
||||
field=models.ForeignKey(blank=True, help_text='Assign this ticket to an admin or unassign it.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
|
@ -1,4 +1,5 @@
|
|||
from django.db.models import Model, TextField, EmailField, CharField
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Model, TextField, EmailField, CharField, DateTimeField, ForeignKey
|
||||
|
||||
ISSUE_TYPE_CHOICES = (
|
||||
('logging_in', 'help logging in'),
|
||||
|
@ -18,6 +19,7 @@ ISSUE_STATUS_CHOICES = (
|
|||
|
||||
|
||||
class Ticket(Model):
|
||||
submitted = DateTimeField(auto_now_add=True)
|
||||
name = CharField(blank=False, null=False, max_length=100)
|
||||
email = EmailField(blank=False, null=False)
|
||||
issue_type = CharField(choices=ISSUE_TYPE_CHOICES,
|
||||
|
@ -30,6 +32,17 @@ class Ticket(Model):
|
|||
null=False,
|
||||
max_length=50,
|
||||
default=ISSUE_STATUS_CHOICES[0][0])
|
||||
assigned = ForeignKey(User, blank=True, null=True, help_text="Assign this ticket to an admin or unassign it.")
|
||||
|
||||
def __str__(self):
|
||||
return '{} from {}'.format(self.issue_type, self.name)
|
||||
|
||||
|
||||
class Note(Model):
|
||||
created = DateTimeField(auto_now_add=True)
|
||||
body = TextField(blank=False, null=False)
|
||||
author = ForeignKey(User)
|
||||
ticket = ForeignKey(Ticket)
|
||||
|
||||
def __str__(self):
|
||||
return "admin note"
|
||||
|
|
|
@ -6,7 +6,7 @@ To run this For Real, you'll want to:
|
|||
* set a different SECRET_KEY
|
||||
* change the password for the database or delete the password and use ident
|
||||
* change DEBUG to False
|
||||
* set mailgun api info
|
||||
* set smtp password
|
||||
"""
|
||||
import os
|
||||
|
||||
|
@ -101,8 +101,9 @@ STATIC_URL = '/static/'
|
|||
# Not used during local development, but used in staging+live environments
|
||||
STATIC_ROOT = 'static'
|
||||
|
||||
MAILGUN_URL = "OVERWRITE THIS"
|
||||
MAILGUN_KEY = "OVERWRITE THIS"
|
||||
SMTP_PORT=465
|
||||
SMTP_HOST="smtp.zoho.com"
|
||||
SMTP_PASSWORD="OVERWRITE THIS"
|
||||
|
||||
# Mastodon credentials
|
||||
MASTO_CLIENT_ID = "OVERWRITE THIS"
|
||||
|
|
|
@ -2,8 +2,8 @@ from django.conf.urls import url, include
|
|||
from django.contrib import admin
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^help/', include('help.urls')),
|
||||
# url(r'^help/', include('help.urls')),
|
||||
url(r'^users/', include('users.urls')),
|
||||
url(r'^guestbook/', include('guestbook.urls')),
|
||||
# url(r'^guestbook/', include('guestbook.urls')),
|
||||
url(r'^admin/', admin.site.urls),
|
||||
]
|
||||
|
|
|
@ -9,19 +9,27 @@ class PubkeyInline(admin.TabularInline):
|
|||
model = Pubkey
|
||||
extra = 1
|
||||
|
||||
def bulk_review(madmin, req, qs):
|
||||
def bulk_accept(madmin, req, qs):
|
||||
for townie in qs:
|
||||
townie.reviewed = True
|
||||
townie.state = Townie.ACCEPTED
|
||||
townie.save()
|
||||
post_users_to_social(qs)
|
||||
|
||||
bulk_review.short_description = 'mark selected townies as reviewed'
|
||||
bulk_accept.short_description = 'mark selected townies as accepted'
|
||||
|
||||
def bulk_reject(madmin, req, qs):
|
||||
for townie in qs:
|
||||
townie.state = Townie.REJECTED
|
||||
townie.save()
|
||||
|
||||
bulk_reject.short_description = 'mark selected townies as rejected'
|
||||
|
||||
@admin.register(Townie)
|
||||
class TownieAdmin(admin.ModelAdmin):
|
||||
inlines = [PubkeyInline]
|
||||
list_display = ('username', 'reviewed', 'email')
|
||||
ordering = ('reviewed',)
|
||||
list_display = ('username', 'state', 'email')
|
||||
readonly_fields = ('reasons', 'plans', 'socials')
|
||||
ordering = ('state',)
|
||||
exclude = ('first_name', 'last_name', 'password', 'groups', 'user_permissions', 'last_login', 'is_staff', 'is_active', 'is_superuser')
|
||||
actions = (bulk_review,)
|
||||
actions = (bulk_accept, bulk_reject,)
|
||||
search_fields = ('username', 'email', 'displayname')
|
||||
|
|
|
@ -12,9 +12,9 @@ submission_throttle = {}
|
|||
throttle_submission = throttler(submission_throttle)
|
||||
|
||||
|
||||
USERNAME_RE = re.compile(r'[a-z][a-z0-9_]+')
|
||||
USERNAME_MIN_LENGTH = 3
|
||||
DISPLAY_NAME_RE = re.compile(r"[a-zA-Z0-9_\-']+")
|
||||
USERNAME_RE = re.compile(r'^[a-z][a-z0-9_]+$')
|
||||
USERNAME_MIN_LENGTH = 2
|
||||
DISPLAY_NAME_RE = re.compile(r"^[a-zA-Z0-9_\-']+$")
|
||||
DISPLAY_MIN_LENGTH = 2
|
||||
|
||||
|
||||
|
@ -23,7 +23,7 @@ def validate_username(username):
|
|||
raise ValidationError('Username too short.')
|
||||
if not USERNAME_RE.match(username):
|
||||
raise ValidationError('Username must be all lowercase, start with a letter, and only use the _ special character')
|
||||
duplicate = Townie.objects.filter(username=username).count()
|
||||
duplicate = Townie.objects.filter(username=username).exclude(state=Townie.REJECTED).count()
|
||||
if duplicate > 0:
|
||||
raise ValidationError('Username already in use :(')
|
||||
|
||||
|
@ -34,6 +34,7 @@ def validate_displayname(display_name):
|
|||
if not DISPLAY_NAME_RE.match(display_name):
|
||||
raise ValidationError("Valid characters: a-z, A-Z, 0-9, -, _, and '.")
|
||||
|
||||
|
||||
def validate_pubkey(pubkey):
|
||||
# TODO see if I can get the type out
|
||||
key = ssh.SSHKey(pubkey, strict_mode=False, skip_option_parsing=True)
|
||||
|
@ -52,32 +53,63 @@ class TownieForm(Form):
|
|||
validators=(validate_username,),
|
||||
help_text='lowercase and no spaces. underscore ok',
|
||||
label='username')
|
||||
|
||||
email = EmailField(
|
||||
help_text='only used to message you about your account and nothing else.',
|
||||
label='e-mail')
|
||||
|
||||
displayname = CharField(
|
||||
validators=(validate_displayname,),
|
||||
help_text='100% optional. pseudonyms welcome.',
|
||||
label='display name',
|
||||
required=False)
|
||||
|
||||
referral = CharField(
|
||||
required=False,
|
||||
label='did a townie refer you? put their handle here.',
|
||||
help_text="this is optional and just helps us when reviewing your application.")
|
||||
|
||||
reasons = CharField(
|
||||
widget=Textarea,
|
||||
required=False,
|
||||
required=True,
|
||||
label='what interests you about tilde.town?',
|
||||
help_text='This is a totally optional place for you to tell us what excites you about getting a ~ account. This is mainly just so we can all feel warm fuzzies.')
|
||||
help_text="""
|
||||
What about this intentional community intrigues you and makes you want to be a part of it?
|
||||
""".strip())
|
||||
|
||||
plans = CharField(
|
||||
widget=Textarea,
|
||||
required=True,
|
||||
label='what sort of things do you want to do on tilde.town?',
|
||||
help_text="""
|
||||
Do you want to socialize? Make something? Learn stuff?
|
||||
""".strip())
|
||||
|
||||
socials = CharField(
|
||||
widget=Textarea,
|
||||
required=False,
|
||||
label='where else are you online?',
|
||||
help_text="""Optional, but if you're comfortable sharing with us some links to other online
|
||||
spaces you're in (like twitter, mastodon, neocities, or whatever) we'd love to get to know
|
||||
you when reviewing your application.
|
||||
""".strip())
|
||||
|
||||
captcha = CaptchaField()
|
||||
|
||||
pubkey = CharField(
|
||||
widget=Textarea,
|
||||
validators=(validate_pubkey,),
|
||||
label='SSH public key',
|
||||
help_text='if this is not a thing you are familiar with, that\'s okay! you can make one <a href="/users/keymachine">here</a> or read <a href="https://tilde.town/~wiki/ssh.html">our guide</a> to learn more.')
|
||||
help_text='if this is not a thing you are familiar with, that\'s okay! you can make one <a href="/users/keymachine">here</a> or read <a href="https://tilde.town/wiki/getting-started/ssh.html">our guide</a> to learn more.')
|
||||
|
||||
pubkey_type = ChoiceField(
|
||||
choices=SSH_TYPE_CHOICES,
|
||||
label='SSH public key type',
|
||||
help_text="unless you know what you're doing you can leave this be.")
|
||||
|
||||
aup = BooleanField(
|
||||
label='i super agree to our acceptable use policy',
|
||||
help_text='please read our <a href="https://tilde.town/~wiki/conduct.html">code of conduct</a> and click this box if you agree.')
|
||||
label="i agree to the town's acceptable use policy",
|
||||
help_text='please read our <a href="https://tilde.town/wiki/conduct.html">code of conduct</a> and click this box if you agree.')
|
||||
|
||||
def clean(self):
|
||||
result = super().clean()
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-01-19 18:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0009_auto_20170114_0757'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pubkey',
|
||||
name='key_type',
|
||||
field=models.CharField(choices=[('ssh-rsa', 'ssh-rsa'), ('ssh-dss', 'ssh-dss'), ('ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256')], max_length=50),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-04-16 01:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0010_auto_20190119_1828'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='townie',
|
||||
name='plans',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='townie',
|
||||
name='referral',
|
||||
field=models.CharField(max_length=100, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='townie',
|
||||
name='socials',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-07-10 16:22
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0011_auto_20190416_0126'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='townie',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='townie',
|
||||
name='referral',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-07-16 01:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0012_auto_20190710_1622'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='townie',
|
||||
name='state',
|
||||
field=models.CharField(choices=[('3_rejected', 'Rejected'), ('2_accepted', 'Accepted'), ('0_unreviewed', 'Unreviewed'), ('4_permaban', 'Permanently Banned'), ('1_tempban', 'Temporarily Banned')], default='0_unreviewed', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='townie',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, help_text='Use this field to share information about this user (reviewed or not) for other admins to see', null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-07-16 01:51
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def set_state(apps, _):
|
||||
Townie = apps.get_model('users', 'Townie')
|
||||
for townie in Townie.objects.all():
|
||||
if townie.reviewed:
|
||||
townie.state = '2_accepted'
|
||||
|
||||
townie.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0013_auto_20190716_0148'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_state)
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2019-07-16 02:10
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0014_auto_20190716_0151'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='townie',
|
||||
name='reviewed',
|
||||
),
|
||||
]
|
|
@ -4,7 +4,7 @@ from subprocess import run, CalledProcessError
|
|||
from tempfile import TemporaryFile
|
||||
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import pre_save
|
||||
from django.db.models.signals import pre_save, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import TextField, BooleanField, CharField, ForeignKey
|
||||
|
@ -19,6 +19,7 @@ SSH_TYPE_CHOICES = (
|
|||
('ssh-rsa', 'ssh-rsa',),
|
||||
('ssh-dss', 'ssh-dss',),
|
||||
('ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256'),
|
||||
('ssh-ed25519', 'ssh-ed25519'),
|
||||
)
|
||||
|
||||
DEFAULT_INDEX_PATH = '/etc/skel/public_html/index.html'
|
||||
|
@ -32,7 +33,7 @@ else:
|
|||
|
||||
KEYFILE_HEADER = """########## GREETINGS! ##########
|
||||
# Hi! This file is automatically managed by tilde.town. You
|
||||
# probably shouldn't change it. If you want to add more public keys that's
|
||||
# seriously shouldn't change it. If you want to add more public keys that's
|
||||
# totally fine: you can put them in ~/.ssh/authorized_keys"""
|
||||
|
||||
|
||||
|
@ -42,25 +43,57 @@ class Townie(User):
|
|||
class Meta:
|
||||
verbose_name = 'Townie'
|
||||
verbose_name_plural = 'Townies'
|
||||
|
||||
# the actual values here have a leading int for sorting :(
|
||||
UNREVIEWED = '0_unreviewed'
|
||||
TEMPBAN = '1_tempban'
|
||||
ACCEPTED = '2_accepted'
|
||||
REJECTED = '3_rejected'
|
||||
PERMABAN = '4_permaban'
|
||||
STATE_CHOICES = (
|
||||
(REJECTED, 'Rejected'),
|
||||
(ACCEPTED, 'Accepted'),
|
||||
(UNREVIEWED, 'Unreviewed'),
|
||||
(PERMABAN, 'Permanently Banned'),
|
||||
(TEMPBAN, 'Temporarily Banned'),
|
||||
)
|
||||
shell = CharField(max_length=50, default="/bin/bash")
|
||||
reviewed = BooleanField(default=False)
|
||||
state = CharField(max_length=20, choices=STATE_CHOICES, default=UNREVIEWED)
|
||||
reasons = TextField(blank=True, null=False, default='')
|
||||
plans = TextField(blank=True, null=False, default='')
|
||||
socials = TextField(blank=True, null=False, default='')
|
||||
referral = CharField(max_length=100, null=True, blank=True)
|
||||
displayname = CharField(max_length=100, blank=False, null=False)
|
||||
notes = TextField(blank=True, null=True,
|
||||
help_text='Use this field to share information about this user (reviewed or not) for other admins to see')
|
||||
|
||||
@property
|
||||
def accepted(self):
|
||||
return self.ACCEPTED == self.state
|
||||
|
||||
@property
|
||||
def unreviewed(self):
|
||||
return self.UNREVIEWED == self.state
|
||||
|
||||
@property
|
||||
def home(self):
|
||||
return os.path.join('/home', self.username)
|
||||
|
||||
def send_welcome_email(self, admin_name='vilmibm'):
|
||||
def generate_gift(self):
|
||||
command = '/town/bin/generate_welcome_present.sh'
|
||||
error = _guarded_run(['sudo', command, self.username])
|
||||
if error:
|
||||
logger.error(error)
|
||||
return
|
||||
|
||||
def send_welcome_email(self):
|
||||
welcome_tmpl = get_template('users/welcome_email.txt')
|
||||
context = {
|
||||
'username': self.username,
|
||||
'admin_name': admin_name,
|
||||
'admin_name': 'vilmibm',
|
||||
}
|
||||
text = welcome_tmpl.render(context)
|
||||
from_address = '{}@tilde.town'.format(admin_name)
|
||||
success = send_email(self.email, text, subject='tilde.town!',
|
||||
frum=from_address)
|
||||
success = send_email(self.email, text, subject='tilde.town!')
|
||||
if not success:
|
||||
Ticket.objects.create(name='system',
|
||||
email='root@tilde.town',
|
||||
|
@ -84,7 +117,7 @@ class Townie(User):
|
|||
"""A VERY NOT IDEMPOTENT create function. Originally, I had ambitions
|
||||
to have this be idempotent and able to incrementally update a user as
|
||||
needed, but decided that was overkill for now."""
|
||||
assert(self.reviewed)
|
||||
assert(self.accepted)
|
||||
dot_ssh_path = '/home/{}/.ssh'.format(self.username)
|
||||
|
||||
error = _guarded_run(['sudo',
|
||||
|
@ -95,7 +128,7 @@ class Townie(User):
|
|||
'--disabled-password',
|
||||
self.username])
|
||||
if error:
|
||||
logging.error(error)
|
||||
logger.error(error)
|
||||
return
|
||||
|
||||
error = _guarded_run(['sudo',
|
||||
|
@ -105,7 +138,7 @@ class Townie(User):
|
|||
self.username])
|
||||
|
||||
if error:
|
||||
logging.error(error)
|
||||
logger.error(error)
|
||||
return
|
||||
|
||||
# Create .ssh
|
||||
|
@ -114,7 +147,7 @@ class Townie(User):
|
|||
'mkdir',
|
||||
dot_ssh_path])
|
||||
if error:
|
||||
logging.error(error)
|
||||
logger.error(error)
|
||||
return
|
||||
|
||||
def write_authorized_keys(self):
|
||||
|
@ -134,54 +167,114 @@ class Townie(User):
|
|||
fp.seek(0)
|
||||
error = _guarded_run(['sudo',
|
||||
'--user={}'.format(self.username),
|
||||
'/opt/bin/create_keyfile.py',
|
||||
'/town/src/tildetown-admin/scripts/create_keyfile.py',
|
||||
self.username],
|
||||
stdin=fp)
|
||||
if error:
|
||||
logging.error(error)
|
||||
logger.error(error)
|
||||
|
||||
def generate_authorized_keys(self):
|
||||
"""returns a string suitable for writing out to an authorized_keys
|
||||
file"""
|
||||
content = KEYFILE_HEADER
|
||||
for pubkey in self.pubkey_set.all():
|
||||
if pubkey.key.startswith('ssh-'):
|
||||
prefix = pubkey.key.split(' ')
|
||||
prefix = prefix[0] if len(prefix) > 0 else None
|
||||
if prefix in [p[0] for p in SSH_TYPE_CHOICES]:
|
||||
content += '\n{}'.format(pubkey.key)
|
||||
else:
|
||||
content += '\n{} {}'.format(key.key_type, pubkey.key)
|
||||
content += '\n{} {}'.format(pubkey.key_type, pubkey.key)
|
||||
|
||||
return content
|
||||
|
||||
def rename_on_disk(self, old_username):
|
||||
"""Assuming that this instance has a new name set, renames this user on
|
||||
disk with self.username."""
|
||||
# TODO use systemd thing to end their session
|
||||
error = _guarded_run([
|
||||
'sudo',
|
||||
'/town/src/tildetown-admin/scripts/rename_user.py',
|
||||
old_username,
|
||||
self.username])
|
||||
if error:
|
||||
logger.error(error)
|
||||
return
|
||||
logger.info('Renamed {} to {}'.format(old_username, self.username))
|
||||
|
||||
# send user an email
|
||||
|
||||
rename_tmpl = get_template('users/rename_email.txt')
|
||||
context = {
|
||||
'old_username': old_username,
|
||||
'new_username': self.username
|
||||
}
|
||||
text = rename_tmpl.render(context)
|
||||
success = send_email(self.email, text, subject='Your tilde.town user has been renamed!')
|
||||
if not success:
|
||||
Ticket.objects.create(name='system',
|
||||
email='root@tilde.town',
|
||||
issue_type='other',
|
||||
issue_text='was not able to send rename email to {} ({})'.format(
|
||||
self.username,
|
||||
self.email))
|
||||
|
||||
|
||||
class Pubkey(Model):
|
||||
key_type = CharField(max_length=50,
|
||||
blank=False,
|
||||
null=False,
|
||||
choices=SSH_TYPE_CHOICES,
|
||||
)
|
||||
choices=SSH_TYPE_CHOICES)
|
||||
key = TextField(blank=False, null=False)
|
||||
townie = ForeignKey(Townie)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Townie)
|
||||
def on_townie_pre_save(sender, instance, **kwargs):
|
||||
existing = Townie.objects.filter(username=instance.username)
|
||||
if not existing:
|
||||
# we're making a new Townie; this means someone just signed up. We
|
||||
# don't care at all about their state on disk.
|
||||
@receiver(post_save, sender=Pubkey)
|
||||
def on_pubkey_post_save(sender, instance, **kwargs):
|
||||
# Ensure we're checking the townie as it exists at the point of pubkey
|
||||
# save. If a user is being reviewed, we'll write their key file in the
|
||||
# townie pre save.
|
||||
townie = Townie.objects.filter(username=instance.townie.username)
|
||||
if not townie:
|
||||
return
|
||||
|
||||
existing = existing[0]
|
||||
townie = townie[0]
|
||||
|
||||
needs_creation = not existing.reviewed and instance.reviewed == True
|
||||
regen_keyfile = needs_creation or set(existing.pubkey_set.all()) != set(instance.pubkey_set.all())
|
||||
if townie.accepted:
|
||||
townie.write_authorized_keys()
|
||||
|
||||
if needs_creation:
|
||||
instance.create_on_disk()
|
||||
instance.send_welcome_email()
|
||||
|
||||
if regen_keyfile:
|
||||
instance.write_authorized_keys()
|
||||
@receiver(pre_save, sender=Townie)
|
||||
def on_townie_pre_save(sender, instance, **kwargs):
|
||||
if instance.id is None:
|
||||
logger.info('Signup from {}'.format(instance.username))
|
||||
return
|
||||
|
||||
existing = Townie.objects.get(id=instance.id)
|
||||
|
||||
# See if we need to create the user on disk.
|
||||
if existing.unreviewed and instance.accepted:
|
||||
logger.info('Creating user {} on disk.'.format(instance.username))
|
||||
try:
|
||||
instance.create_on_disk()
|
||||
instance.write_authorized_keys()
|
||||
except Exception as e:
|
||||
logger.error('Failed syncing user {} to disk: {}'.format(instance.username, e))
|
||||
else:
|
||||
instance.send_welcome_email()
|
||||
instance.generate_gift()
|
||||
|
||||
return
|
||||
else:
|
||||
# This user state transition is currently undefined. In the future, we can check for things
|
||||
# like bans/unbans and then take the appropriate action.
|
||||
return
|
||||
|
||||
# See if this user needs a rename on disk
|
||||
logger.info('checking for rename {} vs {}'.format(
|
||||
existing.username, instance.username))
|
||||
if existing.username != instance.username:
|
||||
logger.info('username do not match, going to rename')
|
||||
instance.rename_on_disk(existing.username)
|
||||
|
||||
|
||||
def _guarded_run(cmd_args, **run_args):
|
||||
|
@ -200,22 +293,3 @@ def _guarded_run(cmd_args, **run_args):
|
|||
issue_text='error while running {}: {}'.format(
|
||||
cmd_args, e))
|
||||
return e
|
||||
|
||||
|
||||
# things to consider:
|
||||
# * what happens when a user wants their name changed?
|
||||
# * it looks like usermod -l and a mv of the home dir can change a user's username.
|
||||
# * would hook this into the pre_save signal to note a username change
|
||||
# * what happens when a user is marked as not reviewed?
|
||||
# * does this signal user deletion? Or does literal Townie deletion signal
|
||||
# "needs to be removed from disk"? I think it makes the most sense for the
|
||||
# latter to imply full user deletion.
|
||||
# * I honestly can't even think of a reason to revert a user to "not reviewed"
|
||||
# and perhaps it's best to just not make that possible. for now, though, I
|
||||
# think I can ignore it.
|
||||
# * what happens when a user needs to be banned?
|
||||
# * the Townie should be deleted via post_delete signal
|
||||
# * what are things about a user that might change in django and require changes on disk?
|
||||
# * username
|
||||
# * displayname (only if i start using this?)
|
||||
# * ssh key
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
For more information about where to save keys, how to
|
||||
use them, and how to use terminals (on all platforms),
|
||||
check out the <a
|
||||
href="https://tilde.town/~wiki/ssh.html">tilde.town ssh
|
||||
href="https://tilde.town/wiki/ssh.html">tilde.town ssh
|
||||
primer</a>.
|
||||
</p>
|
||||
</td>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
hi!
|
||||
|
||||
you requested a new username on tilde.town. This process required logging you
|
||||
out and killing any active processes you had running. sorry if this caused any
|
||||
confusion or inconvenience.
|
||||
|
||||
old username: {{old_username}}
|
||||
new username: {{new_username}}
|
||||
|
||||
you'll use this when ssh'ing into the town.
|
||||
|
||||
best,
|
||||
~vilmibm
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
|
||||
|
||||
{% if signups_enabled %}
|
||||
<form class="tilde" action="{% url 'users:signup' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<table id="signup">
|
||||
|
@ -40,5 +41,11 @@
|
|||
<input type="submit" value="sign up <3" />
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>
|
||||
Town signups are currently disabled, but will resume in the future. If you can't wait for an account, you
|
||||
can check out some <a href="https://tildeverse.org/">similar servers</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -12,23 +12,22 @@
|
|||
<h1>thanks for signing up for <a href="https://tilde.town">tilde.town!</a>!</h1>
|
||||
<p>you'll get an email when your account is live. a human reads over
|
||||
each new account and manually activates it, so it might take 1-3
|
||||
days. if you think your account has been overlooked, tweet
|
||||
<a href="https://twitter.com/tildetown">@tildetown</a> or file a
|
||||
<a href="https://tilde.town/helpdesk">ticket</a>.
|
||||
days. if you think your account has been overlooked, file a
|
||||
<a href="https://cgi.tilde.town/help/tickets">ticket</a>.
|
||||
</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<h2>in the meantime, check out some of our projects...</h2>
|
||||
<ul>
|
||||
<li><a href="https://tilde.town/wiki/">our wiki</a></li>
|
||||
<li><a href="https://tilde.town/~bear/where.html">tilde map</a></li>
|
||||
<li><a href="https://github.com/tildetown/zine/raw/master/issue_1/zine.pdf">zine</a></li>
|
||||
<li><a href="http://tilde.town/~wiki/">our wiki</a></li>
|
||||
<li><a href="https://tilde.town/~karlen/">no one will ever read this but</a></li>
|
||||
<li><a href="https://tilde.town/~kc/blackout/">black out</a></li>
|
||||
<li><a href="https://tilde.town/~subtransience/machinecode/index.html">the machine room</a></li>
|
||||
</ul>
|
||||
<h2>or a <a href="https://tilde.town/cgi/random">random page</a></h2>
|
||||
<h2>or a <a href="https://cgi.tilde.town/users/random">random page</a></h2>
|
||||
</td>
|
||||
<td style="padding:1em">
|
||||
<a href="http://giphy.com/gifs/cyndipop-golden-girls-bea-arthur-ToMjGpK80QLT7KLWPLO">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Welcome to tilde.town, ~{{username}}!
|
||||
|
||||
Please take a moment to review our code of conduct: https://tilde.town/~wiki/conduct.html
|
||||
Please take a moment to review our code of conduct: https://tilde.town/wiki/conduct.html
|
||||
|
||||
and login with:
|
||||
|
||||
|
|
|
@ -12,10 +12,18 @@ from django.views.generic.edit import FormView
|
|||
from .forms import TownieForm
|
||||
from .models import Townie, Pubkey
|
||||
|
||||
SIGNUPS_ENABLED = True
|
||||
|
||||
class SignupView(FormView):
|
||||
form_class = TownieForm
|
||||
template_name = 'users/signup.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['signups_enabled'] = SIGNUPS_ENABLED
|
||||
return ctx
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
del form.cleaned_data['captcha']
|
||||
|
|
Loading…
Reference in New Issue