Compare commits

..

75 Commits

Author SHA1 Message Date
vilmibm afc5d30ed8 disable help and guestbook; minor fixes 2021-03-08 01:14:36 +00:00
vilmibm 8d04787277 fix validation REs, fix key thing 2021-03-07 21:52:26 +00:00
vilmibm be2975ad62 upgrade django 2020-06-03 02:03:51 +00:00
vilmibm 0f078f86fd upgrade Django 2019-12-18 14:07:40 +00:00
vilmibm 90dca0274a return to generating gifts 2019-12-16 22:25:20 +00:00
vilmibm 158bdcff5f guestbook spam stuff 2019-12-16 22:25:03 +00:00
vilmibm 77d728a153 bootleg anti-spam measure 2019-12-16 22:22:01 +00:00
vilmibm 2385446e74 increase timeout to help with bulk add of users 2019-12-16 22:21:44 +00:00
vilmibm 5220d9bfad atomic user creation/notification 2019-10-11 22:53:39 +00:00
vilmibm 4e3977edd2 enum sigh 2019-08-21 15:39:49 +00:00
vilmibm 2354695401 fix signups toggle 2019-08-20 21:39:38 +00:00
vilmibm de07b42082 gross hack 2019-07-20 01:25:52 +00:00
Nate Smith 3d12a5bfd4
Merge pull request #48 from tildetown/admin-improvements
Admin improvements
2019-07-19 20:09:52 -05:00
Nate Smith 32e640b875 support assigning of help tickets 2019-07-18 17:25:28 -05:00
Nate Smith 02a50e1d11 working support for tickete notes 2019-07-18 17:05:08 -05:00
Nate Smith dfd843064d messy wip for Note 2019-07-15 21:44:13 -05:00
Nate Smith 31adb3cc3a rejected users do not occupy a username 2019-07-15 21:15:33 -05:00
Nate Smith e59bd893fd actually remove reviewed field plus required migrations 2019-07-15 21:10:48 -05:00
Nate Smith a2af7c5c96 restore this field to make migrations easier 2019-07-15 20:48:05 -05:00
Nate Smith 9373589c52 code for adding a state field to users 2019-07-15 20:38:11 -05:00
Nate Smith 2c71afebc2 mark readonly user signup fields 2019-07-10 11:27:29 -05:00
Nate Smith e9d1fea15e add notes field to Townie 2019-07-10 11:27:20 -05:00
vilmibm ffeb999290 Merge branch 'master' of github.com:tildetown/tildetown-admin 2019-07-09 17:28:53 +00:00
vilmibm 0881982614 update some reqs for py3 compat 2019-07-09 17:28:27 +00:00
Nate Smith 4eb72edf42
Update README.md 2019-07-05 13:52:05 -05:00
vilmibm 3cb39206c1 oops 2019-07-03 21:18:08 +00:00
Nate Smith 1757ec85c8
Merge pull request #37 from tildetown/zoho
migrate to zoho
2019-07-03 16:11:22 -05:00
vilmibm 627ccf6e55 no longer need requests 2019-07-03 21:02:50 +00:00
vilmibm b33c244d7b allow referral to be blank 2019-07-03 20:41:50 +00:00
vilmibm f56e12a053 switch away from mailgun and to external SMTP 2019-07-03 20:41:40 +00:00
vilmibm a8917a05c3 support for pausing signups 2019-07-02 04:46:29 +00:00
Nate Smith 57e18fb337
Merge pull request #35 from tildetown/new-signup-questions
add some new signup questions
2019-04-15 15:50:04 -10:00
vilmibm 677f5ff2ee some migrations 2019-04-16 01:27:08 +00:00
vilmibm 92177e7a66 add referral and remove community question 2019-04-16 01:23:35 +00:00
vilmibm d316fef6b6 Merge branch 'master' into new-signup-questions 2019-04-16 00:59:15 +00:00
vilmibm 5271fdc8f5 disable guestbook submissions for now 2019-04-16 00:58:08 +00:00
vilmibm 330ab81d63 tweak captcha 2019-03-08 22:12:28 +00:00
vilmibm 08ff82094d add some new signup questions 2019-02-14 23:45:45 +00:00
Nate Smith 3c4eabd42e
Merge pull request #34 from tildetown/feature/enhanced-ticket-view
Better ticket admin viewing
2019-01-25 11:05:31 -06:00
Mallory Hancock 9f9fbf949b add in readonly field to admin form 2019-01-19 11:17:19 -08:00
Mallory Hancock 3879017dd1 add filters and submitted field for tickets 2019-01-19 10:46:20 -08:00
vilmibm 31e0ef4add do not raise if pkill fails to kill. 2019-01-18 21:31:09 +00:00
vilmibm 1425e48c02 fix more links 2019-01-18 21:11:59 +00:00
vilmibm 165b79d0b6 fix another link 2019-01-18 21:09:21 +00:00
vilmibm 372e1bf84d fix some links in the signup form 2019-01-18 21:08:45 +00:00
vilmibm 3aa777f253 tweak emailing to hopefully improve spam situation 2019-01-18 20:55:08 +00:00
vilmibm eca5242e37 ignore static files 2019-01-18 19:55:03 +00:00
vilmibm 99d6b1a1c4 fix perm 2019-01-15 06:17:56 +00:00
vilmibm 7d4b64b111 upgrade psycopg2 for new postgresql 2019-01-15 05:02:45 +00:00
vilmibm 70a5fc31de ignore live settings 2019-01-15 04:55:15 +00:00
vilmibm 9e54e59628 lol it is a home dir 2019-01-15 03:57:20 +00:00
vilmibm ca1500d265 point to new location of scripts files 2019-01-15 03:47:18 +00:00
vilmibm f095c5ec7f add script for starting server 2019-01-15 03:43:57 +00:00
vilmibm 14639d78f8 oops 2018-09-05 19:33:07 -07:00
vilmibm 8be382da87 kill user processes during rename and send email about it 2018-09-05 19:25:00 -07:00
vilmibm 7fc039559e fix laying out non-{rsa,dss} keys 2018-07-08 19:28:29 -07:00
Nate Smith a908079ba9
Update thanks.html 2018-04-28 18:16:52 -07:00
nate 1bdaede8c2 fix logging 2018-02-24 18:35:03 -08:00
nate 169df3e84d also rename user's group 2018-02-24 18:34:55 -08:00
nate b25ad2de8e sigh
was looking up users based on username to check for need to rename.
2018-02-24 18:26:21 -08:00
nate 13edcad576 logging 2018-02-24 18:23:28 -08:00
Nate Smith 747f1e6f17
Merge pull request #28 from tildetown/rename-users
Support renaming users
2018-02-24 00:22:13 -08:00
nate 48bebb7386 note that we only support debian 2018-02-24 00:14:30 -08:00
nate 3d58870f34 update docs 2018-02-23 14:48:00 -08:00
nate 6ec813a328 fix perms 2018-02-23 14:43:01 -08:00
nate ae4ce741ba clean up notes 2018-02-23 14:32:21 -08:00
nate 0a5dae85a3 handle renaming in pre_save
I chose pre_save so that if renaming fails, the db and disk remain consistent.
2018-02-23 14:31:58 -08:00
nate f7bbdf3f0f add rename ability to user model 2018-02-23 14:31:43 -08:00
nate 7da85faeed clarify variables in rename script 2018-02-23 14:31:25 -08:00
nate 0d8b370b5d write rename_user standalone script 2018-02-23 13:58:54 -08:00
nate 33fee98309 handle pubkey post save more gracefully 2018-02-22 16:38:10 -08:00
nate e150e07001 strengthen warning 2018-02-21 23:40:19 -08:00
nate ae69c79a51 i knew i would miss one 2018-02-21 23:32:47 -08:00
nate 76c476f258 lol redo this 2018-02-21 23:31:16 -08:00
nate 61e5e3b4fc Revert "upgrade postgresql library"
This reverts commit d2f7363a3b.
2018-02-21 22:47:45 -08:00
35 changed files with 652 additions and 122 deletions

8
.gitignore vendored
View File

@ -3,3 +3,11 @@
__pycache__
*.pyc
*.egg-info
.bash_history
.viminfo
ttadmin/settings_live.py
ttadmin/static
venv/
build/
dist/

View File

@ -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
scripts/create_keyfile.py 100644 → 100755
View File

View File

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

12
scripts/start.sh 100755
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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