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__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
*.egg-info
|
*.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] Helpdesk
|
||||||
* [x] User account management
|
* [x] User account management
|
||||||
* [ ] Start/stop services
|
* [ ] Start/stop services
|
||||||
* [ ] Cost reporting from AWS
|
* [ ] Cost reporting from DO
|
||||||
* [ ] Status monitoring
|
* [ ] Status monitoring
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
* Python 3.5+
|
* Python 3.5+
|
||||||
* PostgreSQL 9+
|
* PostgreSQL 9+
|
||||||
|
* Ubuntu or Debian
|
||||||
|
|
||||||
## Installation / setup
|
## 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
|
|
@ -15,10 +15,13 @@
|
||||||
* create ttadmin user
|
* create ttadmin user
|
||||||
* ttadmin db user (or just rely on ident..?) / database created
|
* ttadmin db user (or just rely on ident..?) / database created
|
||||||
* copy `create_keyfile.py` from `scripts/` and put it in `/opt/bin/`.
|
* copy `create_keyfile.py` from `scripts/` and put it in `/opt/bin/`.
|
||||||
* `chmod o+x /opt/bin/create_keyfile.py``
|
* copy `rename_user.py` from `scripts/` and put it in `/tilde/bin/`.
|
||||||
|
* `chmod u+x /opt/bin/create_keyfile.py``
|
||||||
* add to sudoers:
|
* 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
|
* 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
|
* 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)',
|
'License :: OSI Approved :: Affero GNU General Public License v3 (AGPLv3)',
|
||||||
],
|
],
|
||||||
packages=['ttadmin'],
|
packages=['ttadmin'],
|
||||||
install_requires = ['Django==1.10.2',
|
install_requires = ['Django==1.11.29',
|
||||||
'sshpubkeys==2.2.0',
|
'sshpubkeys==2.2.0',
|
||||||
'psycopg2-binary==2.7.4',
|
'psycopg2-binary==2.8.5',
|
||||||
'requests==2.12.5',
|
|
||||||
'gunicorn==19.6.0',
|
'gunicorn==19.6.0',
|
||||||
'Mastodon.py==1.1.1',
|
'Mastodon.py==1.4.5',
|
||||||
'tweepy==3.5.0'],
|
'tweepy==3.7.0'],
|
||||||
include_package_data = True,
|
include_package_data = True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,15 +4,15 @@ from random import shuffle
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms import ChoiceField
|
from django.forms import ChoiceField
|
||||||
|
|
||||||
CAPTCHA_CHOICES = [('two', 'zorp borp'),
|
CAPTCHA_CHOICES = [('number', 'zorp borp'),
|
||||||
('three', 'quop bop'),
|
('hey', 'quop bop'),
|
||||||
('four', 'NO, I AM NOT A ROBOT'),
|
('never', 'NO, I AM NOT A ROBOT'),
|
||||||
('five', 'crackle zop'),
|
('eleven', 'crackle zop'),
|
||||||
('six', '*rusty screech*'),
|
('twelve', '*rusty screech*'),
|
||||||
('seven', 'mother, give me legs')]
|
('eighty', 'mother, give me legs')]
|
||||||
shuffle(CAPTCHA_CHOICES)
|
shuffle(CAPTCHA_CHOICES)
|
||||||
CAPTCHA_CHOICES.insert(0, ('one', 'beep boop'),)
|
CAPTCHA_CHOICES.insert(0, ('one', 'beep boop'),)
|
||||||
NOT_A_ROBOT = 'four'
|
NOT_A_ROBOT = 'never'
|
||||||
|
|
||||||
def validate_captcha(captcha):
|
def validate_captcha(captcha):
|
||||||
if captcha != NOT_A_ROBOT:
|
if captcha != NOT_A_ROBOT:
|
||||||
|
|
|
@ -1,32 +1,26 @@
|
||||||
import logging
|
import logging
|
||||||
|
from smtplib import SMTP_SSL, SMTPException
|
||||||
import requests
|
from email.message import EmailMessage
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
FROM='root@tilde.town'
|
|
||||||
|
|
||||||
def send_email(to, body, subject='a message from tilde.town', frum=FROM,):
|
def send_email(to, body, subject='a message from tilde.town'):
|
||||||
"""Sends an email using mailgun. Logs on failure."""
|
"""Sends an email using external SMTP. Logs on failure."""
|
||||||
response = requests.post(
|
em = EmailMessage()
|
||||||
settings.MAILGUN_URL,
|
em['Subject'] = subject
|
||||||
auth=('api', settings.MAILGUN_KEY),
|
em['From'] = 'root@tilde.town'
|
||||||
data={
|
em['To'] = to
|
||||||
'from': frum,
|
em.set_content(body)
|
||||||
'to': to,
|
try:
|
||||||
'subject': subject,
|
with SMTP_SSL(port=settings.SMTP_PORT, host=settings.SMTP_HOST) as smtp:
|
||||||
'text': body
|
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
|
return True
|
||||||
|
|
||||||
if not success:
|
|
||||||
logger.error('{}: failed to send email "{}" to {}'.format(
|
|
||||||
response.status_code,
|
|
||||||
subject,
|
|
||||||
to))
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>tilde.town guestbook</h1>
|
<h1>tilde.town guestbook</h1>
|
||||||
|
<p><em>don't try to post urls. it won't work.</em></p>
|
||||||
<marquee>~*~*~*~*say hello*~*~*~*~</marquee>
|
<marquee>~*~*~*~*say hello*~*~*~*~</marquee>
|
||||||
<form class="tilde" action="{% url 'guestbook:guestbook' %}" method="post">
|
<form class="tilde" action="{% url 'guestbook:guestbook' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import re
|
||||||
|
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
|
@ -5,6 +7,8 @@ from django.views.generic.edit import FormView
|
||||||
from .forms import GuestbookForm
|
from .forms import GuestbookForm
|
||||||
from .models import GuestbookMessage
|
from .models import GuestbookMessage
|
||||||
|
|
||||||
|
SUSPICIOUS_RE = re.compile(r'https?://')
|
||||||
|
|
||||||
|
|
||||||
class GuestbookView(FormView):
|
class GuestbookView(FormView):
|
||||||
form_class = GuestbookForm
|
form_class = GuestbookForm
|
||||||
|
@ -17,5 +21,7 @@ class GuestbookView(FormView):
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
del form.cleaned_data['captcha']
|
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)
|
t = GuestbookMessage.objects.create(**form.cleaned_data)
|
||||||
return redirect('guestbook:guestbook')
|
return redirect('guestbook:guestbook')
|
||||||
|
|
|
@ -1,7 +1,44 @@
|
||||||
from django.contrib import admin
|
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)
|
@admin.register(Ticket)
|
||||||
class TicketAdmin(admin.ModelAdmin):
|
class TicketAdmin(admin.ModelAdmin):
|
||||||
list_display = ('issue_status', 'issue_type', 'name', 'email')
|
inlines = [ImmutableNoteInline, NewNoteInline]
|
||||||
fields = ('name', 'email', 'issue_status', 'issue_type', 'issue_text')
|
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 = (
|
ISSUE_TYPE_CHOICES = (
|
||||||
('logging_in', 'help logging in'),
|
('logging_in', 'help logging in'),
|
||||||
|
@ -18,6 +19,7 @@ ISSUE_STATUS_CHOICES = (
|
||||||
|
|
||||||
|
|
||||||
class Ticket(Model):
|
class Ticket(Model):
|
||||||
|
submitted = DateTimeField(auto_now_add=True)
|
||||||
name = CharField(blank=False, null=False, max_length=100)
|
name = CharField(blank=False, null=False, max_length=100)
|
||||||
email = EmailField(blank=False, null=False)
|
email = EmailField(blank=False, null=False)
|
||||||
issue_type = CharField(choices=ISSUE_TYPE_CHOICES,
|
issue_type = CharField(choices=ISSUE_TYPE_CHOICES,
|
||||||
|
@ -30,6 +32,17 @@ class Ticket(Model):
|
||||||
null=False,
|
null=False,
|
||||||
max_length=50,
|
max_length=50,
|
||||||
default=ISSUE_STATUS_CHOICES[0][0])
|
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):
|
def __str__(self):
|
||||||
return '{} from {}'.format(self.issue_type, self.name)
|
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
|
* set a different SECRET_KEY
|
||||||
* change the password for the database or delete the password and use ident
|
* change the password for the database or delete the password and use ident
|
||||||
* change DEBUG to False
|
* change DEBUG to False
|
||||||
* set mailgun api info
|
* set smtp password
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -101,8 +101,9 @@ STATIC_URL = '/static/'
|
||||||
# Not used during local development, but used in staging+live environments
|
# Not used during local development, but used in staging+live environments
|
||||||
STATIC_ROOT = 'static'
|
STATIC_ROOT = 'static'
|
||||||
|
|
||||||
MAILGUN_URL = "OVERWRITE THIS"
|
SMTP_PORT=465
|
||||||
MAILGUN_KEY = "OVERWRITE THIS"
|
SMTP_HOST="smtp.zoho.com"
|
||||||
|
SMTP_PASSWORD="OVERWRITE THIS"
|
||||||
|
|
||||||
# Mastodon credentials
|
# Mastodon credentials
|
||||||
MASTO_CLIENT_ID = "OVERWRITE THIS"
|
MASTO_CLIENT_ID = "OVERWRITE THIS"
|
||||||
|
|
|
@ -2,8 +2,8 @@ from django.conf.urls import url, include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^help/', include('help.urls')),
|
# url(r'^help/', include('help.urls')),
|
||||||
url(r'^users/', include('users.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),
|
url(r'^admin/', admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,19 +9,27 @@ class PubkeyInline(admin.TabularInline):
|
||||||
model = Pubkey
|
model = Pubkey
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
def bulk_review(madmin, req, qs):
|
def bulk_accept(madmin, req, qs):
|
||||||
for townie in qs:
|
for townie in qs:
|
||||||
townie.reviewed = True
|
townie.state = Townie.ACCEPTED
|
||||||
townie.save()
|
townie.save()
|
||||||
post_users_to_social(qs)
|
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)
|
@admin.register(Townie)
|
||||||
class TownieAdmin(admin.ModelAdmin):
|
class TownieAdmin(admin.ModelAdmin):
|
||||||
inlines = [PubkeyInline]
|
inlines = [PubkeyInline]
|
||||||
list_display = ('username', 'reviewed', 'email')
|
list_display = ('username', 'state', 'email')
|
||||||
ordering = ('reviewed',)
|
readonly_fields = ('reasons', 'plans', 'socials')
|
||||||
|
ordering = ('state',)
|
||||||
exclude = ('first_name', 'last_name', 'password', 'groups', 'user_permissions', 'last_login', 'is_staff', 'is_active', 'is_superuser')
|
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')
|
search_fields = ('username', 'email', 'displayname')
|
||||||
|
|
|
@ -12,9 +12,9 @@ submission_throttle = {}
|
||||||
throttle_submission = throttler(submission_throttle)
|
throttle_submission = throttler(submission_throttle)
|
||||||
|
|
||||||
|
|
||||||
USERNAME_RE = re.compile(r'[a-z][a-z0-9_]+')
|
USERNAME_RE = re.compile(r'^[a-z][a-z0-9_]+$')
|
||||||
USERNAME_MIN_LENGTH = 3
|
USERNAME_MIN_LENGTH = 2
|
||||||
DISPLAY_NAME_RE = re.compile(r"[a-zA-Z0-9_\-']+")
|
DISPLAY_NAME_RE = re.compile(r"^[a-zA-Z0-9_\-']+$")
|
||||||
DISPLAY_MIN_LENGTH = 2
|
DISPLAY_MIN_LENGTH = 2
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ def validate_username(username):
|
||||||
raise ValidationError('Username too short.')
|
raise ValidationError('Username too short.')
|
||||||
if not USERNAME_RE.match(username):
|
if not USERNAME_RE.match(username):
|
||||||
raise ValidationError('Username must be all lowercase, start with a letter, and only use the _ special character')
|
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:
|
if duplicate > 0:
|
||||||
raise ValidationError('Username already in use :(')
|
raise ValidationError('Username already in use :(')
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ def validate_displayname(display_name):
|
||||||
if not DISPLAY_NAME_RE.match(display_name):
|
if not DISPLAY_NAME_RE.match(display_name):
|
||||||
raise ValidationError("Valid characters: a-z, A-Z, 0-9, -, _, and '.")
|
raise ValidationError("Valid characters: a-z, A-Z, 0-9, -, _, and '.")
|
||||||
|
|
||||||
|
|
||||||
def validate_pubkey(pubkey):
|
def validate_pubkey(pubkey):
|
||||||
# TODO see if I can get the type out
|
# TODO see if I can get the type out
|
||||||
key = ssh.SSHKey(pubkey, strict_mode=False, skip_option_parsing=True)
|
key = ssh.SSHKey(pubkey, strict_mode=False, skip_option_parsing=True)
|
||||||
|
@ -52,32 +53,63 @@ class TownieForm(Form):
|
||||||
validators=(validate_username,),
|
validators=(validate_username,),
|
||||||
help_text='lowercase and no spaces. underscore ok',
|
help_text='lowercase and no spaces. underscore ok',
|
||||||
label='username')
|
label='username')
|
||||||
|
|
||||||
email = EmailField(
|
email = EmailField(
|
||||||
help_text='only used to message you about your account and nothing else.',
|
help_text='only used to message you about your account and nothing else.',
|
||||||
label='e-mail')
|
label='e-mail')
|
||||||
|
|
||||||
displayname = CharField(
|
displayname = CharField(
|
||||||
validators=(validate_displayname,),
|
validators=(validate_displayname,),
|
||||||
help_text='100% optional. pseudonyms welcome.',
|
help_text='100% optional. pseudonyms welcome.',
|
||||||
label='display name',
|
label='display name',
|
||||||
required=False)
|
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(
|
reasons = CharField(
|
||||||
widget=Textarea,
|
widget=Textarea,
|
||||||
required=False,
|
required=True,
|
||||||
label='what interests you about tilde.town?',
|
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()
|
captcha = CaptchaField()
|
||||||
|
|
||||||
pubkey = CharField(
|
pubkey = CharField(
|
||||||
widget=Textarea,
|
widget=Textarea,
|
||||||
validators=(validate_pubkey,),
|
validators=(validate_pubkey,),
|
||||||
label='SSH public key',
|
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(
|
pubkey_type = ChoiceField(
|
||||||
choices=SSH_TYPE_CHOICES,
|
choices=SSH_TYPE_CHOICES,
|
||||||
label='SSH public key type',
|
label='SSH public key type',
|
||||||
help_text="unless you know what you're doing you can leave this be.")
|
help_text="unless you know what you're doing you can leave this be.")
|
||||||
|
|
||||||
aup = BooleanField(
|
aup = BooleanField(
|
||||||
label='i super agree to our acceptable use policy',
|
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.')
|
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):
|
def clean(self):
|
||||||
result = super().clean()
|
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 tempfile import TemporaryFile
|
||||||
|
|
||||||
from django.db.models import Model
|
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.dispatch import receiver
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import TextField, BooleanField, CharField, ForeignKey
|
from django.db.models import TextField, BooleanField, CharField, ForeignKey
|
||||||
|
@ -19,6 +19,7 @@ SSH_TYPE_CHOICES = (
|
||||||
('ssh-rsa', 'ssh-rsa',),
|
('ssh-rsa', 'ssh-rsa',),
|
||||||
('ssh-dss', 'ssh-dss',),
|
('ssh-dss', 'ssh-dss',),
|
||||||
('ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256'),
|
('ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256'),
|
||||||
|
('ssh-ed25519', 'ssh-ed25519'),
|
||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_INDEX_PATH = '/etc/skel/public_html/index.html'
|
DEFAULT_INDEX_PATH = '/etc/skel/public_html/index.html'
|
||||||
|
@ -32,7 +33,7 @@ else:
|
||||||
|
|
||||||
KEYFILE_HEADER = """########## GREETINGS! ##########
|
KEYFILE_HEADER = """########## GREETINGS! ##########
|
||||||
# Hi! This file is automatically managed by tilde.town. You
|
# 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"""
|
# totally fine: you can put them in ~/.ssh/authorized_keys"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,25 +43,57 @@ class Townie(User):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Townie'
|
verbose_name = 'Townie'
|
||||||
verbose_name_plural = 'Townies'
|
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")
|
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='')
|
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)
|
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
|
@property
|
||||||
def home(self):
|
def home(self):
|
||||||
return os.path.join('/home', self.username)
|
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')
|
welcome_tmpl = get_template('users/welcome_email.txt')
|
||||||
context = {
|
context = {
|
||||||
'username': self.username,
|
'username': self.username,
|
||||||
'admin_name': admin_name,
|
'admin_name': 'vilmibm',
|
||||||
}
|
}
|
||||||
text = welcome_tmpl.render(context)
|
text = welcome_tmpl.render(context)
|
||||||
from_address = '{}@tilde.town'.format(admin_name)
|
success = send_email(self.email, text, subject='tilde.town!')
|
||||||
success = send_email(self.email, text, subject='tilde.town!',
|
|
||||||
frum=from_address)
|
|
||||||
if not success:
|
if not success:
|
||||||
Ticket.objects.create(name='system',
|
Ticket.objects.create(name='system',
|
||||||
email='root@tilde.town',
|
email='root@tilde.town',
|
||||||
|
@ -84,7 +117,7 @@ class Townie(User):
|
||||||
"""A VERY NOT IDEMPOTENT create function. Originally, I had ambitions
|
"""A VERY NOT IDEMPOTENT create function. Originally, I had ambitions
|
||||||
to have this be idempotent and able to incrementally update a user as
|
to have this be idempotent and able to incrementally update a user as
|
||||||
needed, but decided that was overkill for now."""
|
needed, but decided that was overkill for now."""
|
||||||
assert(self.reviewed)
|
assert(self.accepted)
|
||||||
dot_ssh_path = '/home/{}/.ssh'.format(self.username)
|
dot_ssh_path = '/home/{}/.ssh'.format(self.username)
|
||||||
|
|
||||||
error = _guarded_run(['sudo',
|
error = _guarded_run(['sudo',
|
||||||
|
@ -95,7 +128,7 @@ class Townie(User):
|
||||||
'--disabled-password',
|
'--disabled-password',
|
||||||
self.username])
|
self.username])
|
||||||
if error:
|
if error:
|
||||||
logging.error(error)
|
logger.error(error)
|
||||||
return
|
return
|
||||||
|
|
||||||
error = _guarded_run(['sudo',
|
error = _guarded_run(['sudo',
|
||||||
|
@ -105,7 +138,7 @@ class Townie(User):
|
||||||
self.username])
|
self.username])
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
logging.error(error)
|
logger.error(error)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create .ssh
|
# Create .ssh
|
||||||
|
@ -114,7 +147,7 @@ class Townie(User):
|
||||||
'mkdir',
|
'mkdir',
|
||||||
dot_ssh_path])
|
dot_ssh_path])
|
||||||
if error:
|
if error:
|
||||||
logging.error(error)
|
logger.error(error)
|
||||||
return
|
return
|
||||||
|
|
||||||
def write_authorized_keys(self):
|
def write_authorized_keys(self):
|
||||||
|
@ -134,54 +167,114 @@ class Townie(User):
|
||||||
fp.seek(0)
|
fp.seek(0)
|
||||||
error = _guarded_run(['sudo',
|
error = _guarded_run(['sudo',
|
||||||
'--user={}'.format(self.username),
|
'--user={}'.format(self.username),
|
||||||
'/opt/bin/create_keyfile.py',
|
'/town/src/tildetown-admin/scripts/create_keyfile.py',
|
||||||
self.username],
|
self.username],
|
||||||
stdin=fp)
|
stdin=fp)
|
||||||
if error:
|
if error:
|
||||||
logging.error(error)
|
logger.error(error)
|
||||||
|
|
||||||
def generate_authorized_keys(self):
|
def generate_authorized_keys(self):
|
||||||
"""returns a string suitable for writing out to an authorized_keys
|
"""returns a string suitable for writing out to an authorized_keys
|
||||||
file"""
|
file"""
|
||||||
content = KEYFILE_HEADER
|
content = KEYFILE_HEADER
|
||||||
for pubkey in self.pubkey_set.all():
|
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)
|
content += '\n{}'.format(pubkey.key)
|
||||||
else:
|
else:
|
||||||
content += '\n{} {}'.format(key.key_type, pubkey.key)
|
content += '\n{} {}'.format(pubkey.key_type, pubkey.key)
|
||||||
|
|
||||||
return content
|
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):
|
class Pubkey(Model):
|
||||||
key_type = CharField(max_length=50,
|
key_type = CharField(max_length=50,
|
||||||
blank=False,
|
blank=False,
|
||||||
null=False,
|
null=False,
|
||||||
choices=SSH_TYPE_CHOICES,
|
choices=SSH_TYPE_CHOICES)
|
||||||
)
|
|
||||||
key = TextField(blank=False, null=False)
|
key = TextField(blank=False, null=False)
|
||||||
townie = ForeignKey(Townie)
|
townie = ForeignKey(Townie)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=Townie)
|
@receiver(post_save, sender=Pubkey)
|
||||||
def on_townie_pre_save(sender, instance, **kwargs):
|
def on_pubkey_post_save(sender, instance, **kwargs):
|
||||||
existing = Townie.objects.filter(username=instance.username)
|
# Ensure we're checking the townie as it exists at the point of pubkey
|
||||||
if not existing:
|
# save. If a user is being reviewed, we'll write their key file in the
|
||||||
# we're making a new Townie; this means someone just signed up. We
|
# townie pre save.
|
||||||
# don't care at all about their state on disk.
|
townie = Townie.objects.filter(username=instance.townie.username)
|
||||||
|
if not townie:
|
||||||
return
|
return
|
||||||
|
|
||||||
existing = existing[0]
|
townie = townie[0]
|
||||||
|
|
||||||
needs_creation = not existing.reviewed and instance.reviewed == True
|
if townie.accepted:
|
||||||
regen_keyfile = needs_creation or set(existing.pubkey_set.all()) != set(instance.pubkey_set.all())
|
townie.write_authorized_keys()
|
||||||
|
|
||||||
if needs_creation:
|
|
||||||
instance.create_on_disk()
|
|
||||||
instance.send_welcome_email()
|
|
||||||
|
|
||||||
if regen_keyfile:
|
@receiver(pre_save, sender=Townie)
|
||||||
instance.write_authorized_keys()
|
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):
|
def _guarded_run(cmd_args, **run_args):
|
||||||
|
@ -200,22 +293,3 @@ def _guarded_run(cmd_args, **run_args):
|
||||||
issue_text='error while running {}: {}'.format(
|
issue_text='error while running {}: {}'.format(
|
||||||
cmd_args, e))
|
cmd_args, e))
|
||||||
return 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
|
For more information about where to save keys, how to
|
||||||
use them, and how to use terminals (on all platforms),
|
use them, and how to use terminals (on all platforms),
|
||||||
check out the <a
|
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>.
|
primer</a>.
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</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 error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
|
||||||
|
|
||||||
|
{% if signups_enabled %}
|
||||||
<form class="tilde" action="{% url 'users:signup' %}" method="post">
|
<form class="tilde" action="{% url 'users:signup' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<table id="signup">
|
<table id="signup">
|
||||||
|
@ -40,5 +41,11 @@
|
||||||
<input type="submit" value="sign up <3" />
|
<input type="submit" value="sign up <3" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -12,23 +12,22 @@
|
||||||
<h1>thanks for signing up for <a href="https://tilde.town">tilde.town!</a>!</h1>
|
<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
|
<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
|
each new account and manually activates it, so it might take 1-3
|
||||||
days. if you think your account has been overlooked, tweet
|
days. if you think your account has been overlooked, file a
|
||||||
<a href="https://twitter.com/tildetown">@tildetown</a> or file a
|
<a href="https://cgi.tilde.town/help/tickets">ticket</a>.
|
||||||
<a href="https://tilde.town/helpdesk">ticket</a>.
|
|
||||||
</p>
|
</p>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<h2>in the meantime, check out some of our projects...</h2>
|
<h2>in the meantime, check out some of our projects...</h2>
|
||||||
<ul>
|
<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://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="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/~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/~kc/blackout/">black out</a></li>
|
||||||
<li><a href="https://tilde.town/~subtransience/machinecode/index.html">the machine room</a></li>
|
<li><a href="https://tilde.town/~subtransience/machinecode/index.html">the machine room</a></li>
|
||||||
</ul>
|
</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>
|
||||||
<td style="padding:1em">
|
<td style="padding:1em">
|
||||||
<a href="http://giphy.com/gifs/cyndipop-golden-girls-bea-arthur-ToMjGpK80QLT7KLWPLO">
|
<a href="http://giphy.com/gifs/cyndipop-golden-girls-bea-arthur-ToMjGpK80QLT7KLWPLO">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Welcome to tilde.town, ~{{username}}!
|
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:
|
and login with:
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,18 @@ from django.views.generic.edit import FormView
|
||||||
from .forms import TownieForm
|
from .forms import TownieForm
|
||||||
from .models import Townie, Pubkey
|
from .models import Townie, Pubkey
|
||||||
|
|
||||||
|
SIGNUPS_ENABLED = True
|
||||||
|
|
||||||
class SignupView(FormView):
|
class SignupView(FormView):
|
||||||
form_class = TownieForm
|
form_class = TownieForm
|
||||||
template_name = 'users/signup.html'
|
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
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
del form.cleaned_data['captcha']
|
del form.cleaned_data['captcha']
|
||||||
|
|
Loading…
Reference in New Issue