Compare commits

...

39 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
26 changed files with 435 additions and 87 deletions

View File

@ -10,7 +10,7 @@ _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

View File

@ -9,4 +9,4 @@ VENV=/town/venvs/ttadmin
source $VENV/bin/activate source $VENV/bin/activate
export DJANGO_SETTINGS_MODULE=settings_live export DJANGO_SETTINGS_MODULE=settings_live
cd $APP_ROOT cd $APP_ROOT
gunicorn -b0.0.0.0:$PORT ttadmin.wsgi gunicorn -t120 -b0.0.0.0:$PORT ttadmin.wsgi

View File

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

View File

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

View File

@ -1,35 +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()
ADMIN_NAME='vilmibm'
EXTERNAL_FROM='root@tilde.town'
REPLY_TO='tildetown@protonmail.ch'
def send_email(to, body, subject='a message from tilde.town'): 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': EXTERNAL_FROM, em.set_content(body)
'h:Reply-To': REPLY_TO, try:
'to': to, with SMTP_SSL(port=settings.SMTP_PORT, host=settings.SMTP_HOST) as smtp:
'subject': subject, smtp.login('root@tilde.town', settings.SMTP_PASSWORD)
'text': body 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

View File

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

View File

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

View File

@ -1,9 +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):
readonly_fields = ('submitted',) inlines = [ImmutableNoteInline, NewNoteInline]
list_display = ('submitted', 'issue_status', 'issue_type', 'name', 'email') readonly_fields = ('submitted', 'issue_type')
list_filter = ('issue_status', 'issue_type') list_display = ('submitted', 'issue_status', 'assigned', 'issue_type', 'name', 'email',)
fields = ('submitted', 'name', 'email', 'issue_status', 'issue_type', 'issue_text') 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,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, DateTimeField 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'),
@ -31,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"

View File

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

View File

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

View File

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

View File

@ -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 :(')
@ -53,31 +53,62 @@ 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/getting-started/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):

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

@ -10,7 +10,7 @@ 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
from django.template.loader import get_template from django.template.loader import get_template
from common.mailing import send_email, ADMIN_NAME from common.mailing import send_email
from help.models import Ticket from help.models import Ticket
logger = logging.getLogger() logger = logging.getLogger()
@ -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'
@ -42,20 +43,54 @@ 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 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): 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)
success = send_email(self.email, text, subject='tilde.town!') success = send_email(self.email, text, subject='tilde.town!')
@ -82,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',
@ -204,7 +239,7 @@ def on_pubkey_post_save(sender, instance, **kwargs):
townie = townie[0] townie = townie[0]
if townie.reviewed: if townie.accepted:
townie.write_authorized_keys() townie.write_authorized_keys()
@ -216,12 +251,22 @@ def on_townie_pre_save(sender, instance, **kwargs):
existing = Townie.objects.get(id=instance.id) existing = Townie.objects.get(id=instance.id)
# See if we need to create this user on disk. # See if we need to create the user on disk.
if not existing.reviewed and instance.reviewed is True: if existing.unreviewed and instance.accepted:
logger.info('Creating user {} on disk.'.format(instance.username)) logger.info('Creating user {} on disk.'.format(instance.username))
try:
instance.create_on_disk() instance.create_on_disk()
instance.send_welcome_email()
instance.write_authorized_keys() 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 return
# See if this user needs a rename on disk # See if this user needs a rename on disk
@ -248,15 +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 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

View File

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

View File

@ -27,7 +27,7 @@
<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">

View File

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