diff --git a/app/cli.py b/app/cli.py index 4c96e649..6ce806c7 100644 --- a/app/cli.py +++ b/app/cli.py @@ -6,7 +6,9 @@ from app import db import click import os -from app.models import Settings, BannedInstances, Interest +from app.auth.email import send_verification_email +from app.auth.util import random_token +from app.models import Settings, BannedInstances, Interest, Role, User, RolePermission from app.utils import file_get_contents @@ -48,33 +50,63 @@ def register(app): db.drop_all() db.configure_mappers() db.create_all() - db.session.append(Settings(name='allow_nsfw', value=json.dumps(False))) - db.session.append(Settings(name='allow_nsfl', value=json.dumps(False))) - db.session.append(Settings(name='allow_dislike', value=json.dumps(True))) - db.session.append(Settings(name='allow_local_image_posts', value=json.dumps(True))) - db.session.append(Settings(name='allow_remote_image_posts', value=json.dumps(True))) - db.session.append(Settings(name='registration_open', value=json.dumps(True))) - db.session.append(Settings(name='approve_registrations', value=json.dumps(False))) - db.session.append(Settings(name='federation', value=json.dumps(True))) - db.session.append(BannedInstances(domain='lemmygrad.ml')) - db.session.append(BannedInstances(domain='gab.com')) - db.session.append(BannedInstances(domain='rqd2.net')) - db.session.append(BannedInstances(domain='exploding-heads.com')) - db.session.append(BannedInstances(domain='hexbear.net')) - db.session.append(BannedInstances(domain='threads.net')) + db.session.add(Settings(name='allow_nsfw', value=json.dumps(False))) + db.session.add(Settings(name='allow_nsfl', value=json.dumps(False))) + db.session.add(Settings(name='allow_dislike', value=json.dumps(True))) + db.session.add(Settings(name='allow_local_image_posts', value=json.dumps(True))) + db.session.add(Settings(name='allow_remote_image_posts', value=json.dumps(True))) + db.session.add(Settings(name='registration_open', value=json.dumps(True))) + db.session.add(Settings(name='approve_registrations', value=json.dumps(False))) + db.session.add(Settings(name='federation', value=json.dumps(True))) + db.session.add(BannedInstances(domain='lemmygrad.ml')) + db.session.add(BannedInstances(domain='gab.com')) + db.session.add(BannedInstances(domain='rqd2.net')) + db.session.add(BannedInstances(domain='exploding-heads.com')) + db.session.add(BannedInstances(domain='hexbear.net')) + db.session.add(BannedInstances(domain='threads.net')) interests = file_get_contents('interests.txt') - db.session.append(Interest(name='🕊 Chilling', communities=parse_communities(interests, 'chilling'))) - db.session.append(Interest(name='💭 Interesting stuff', communities=parse_communities(interests, 'interesting stuff'))) - db.session.append(Interest(name='📰 News & Politics', communities=parse_communities(interests, 'news & politics'))) - db.session.append(Interest(name='🎮 Gaming', communities=parse_communities(interests, 'gaming'))) - db.session.append(Interest(name='🤓 Linux', communities=parse_communities(interests, 'linux'))) - db.session.append(Interest(name='♻️ Environment', communities=parse_communities(interests, 'environment'))) - db.session.append(Interest(name='🏳‍🌈 LGBTQ+', communities=parse_communities(interests, 'lgbtq'))) - db.session.append(Interest(name='🛠 Programming', communities=parse_communities(interests, 'programming'))) - db.session.append(Interest(name='🖥️ Tech', communities=parse_communities(interests, 'tech'))) - db.session.append(Interest(name='🤗 Mental Health', communities=parse_communities(interests, 'mental health'))) + db.session.add(Interest(name='🕊 Chilling', communities=parse_communities(interests, 'chilling'))) + db.session.add(Interest(name='💭 Interesting stuff', communities=parse_communities(interests, 'interesting stuff'))) + db.session.add(Interest(name='📰 News & Politics', communities=parse_communities(interests, 'news & politics'))) + db.session.add(Interest(name='🎮 Gaming', communities=parse_communities(interests, 'gaming'))) + db.session.add(Interest(name='🤓 Linux', communities=parse_communities(interests, 'linux'))) + db.session.add(Interest(name='♻️ Environment', communities=parse_communities(interests, 'environment'))) + db.session.add(Interest(name='🏳‍🌈 LGBTQ+', communities=parse_communities(interests, 'lgbtq'))) + db.session.add(Interest(name='🛠 Programming', communities=parse_communities(interests, 'programming'))) + db.session.add(Interest(name='🖥️ Tech', communities=parse_communities(interests, 'tech'))) + db.session.add(Interest(name='🤗 Mental Health', communities=parse_communities(interests, 'mental health'))) + + # Initial roles + anon_role = Role(name='Anonymous user', weight=0) + anon_role.permissions.append(RolePermission(permission='register')) + db.session.add(anon_role) + + auth_role = Role(name='Authenticated user', weight=1) + db.session.add(auth_role) + + staff_role = Role(name='Staff', weight=2) + staff_role.permissions.append(RolePermission(permission='approve registrations')) + staff_role.permissions.append(RolePermission(permission='manage users')) + db.session.add(staff_role) + + admin_role = Role(name='Admin', weight=3) + admin_role.permissions.append(RolePermission(permission='change user roles')) + admin_role.permissions.append(RolePermission(permission='manage users')) + db.session.add(admin_role) + + # Admin user + user_name = input("Admin user name (ideally not 'admin'): ") + email = input("Admin email address: ") + password = input("Admin password: ") + verification_token = random_token(16) + admin_user = User(user_name=user_name, email=email, verification_token=verification_token) + admin_user.set_password(password) + admin_user.roles.append(admin_role) + send_verification_email(admin_user) + print("Check your email inbox for a verification link.") + db.session.commit() - print("Done") + print("Initial setup is finished.") def parse_communities(interests_source, segment): diff --git a/app/constants.py b/app/constants.py index 531dd5aa..d57183a7 100644 --- a/app/constants.py +++ b/app/constants.py @@ -8,6 +8,7 @@ POST_TYPE_POLL = 5 DATETIME_MS_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" +# Community subscription levels SUBSCRIPTION_OWNER = 3 SUBSCRIPTION_MODERATOR = 2 SUBSCRIPTION_MEMBER = 1 diff --git a/app/models.py b/app/models.py index a721d56d..5cecda86 100644 --- a/app/models.py +++ b/app/models.py @@ -113,6 +113,13 @@ class Community(db.Model): ).all() +user_role = db.Table('user_role', + db.Column('user_id', db.Integer, db.ForeignKey('user.id')), + db.Column('role_id', db.Integer, db.ForeignKey('role.id')), + db.PrimaryKeyConstraint('user_id', 'role_id') +) + + class User(UserMixin, db.Model): query_class = FullTextSearchQuery id = db.Column(db.Integer, primary_key=True) @@ -129,7 +136,6 @@ class User(UserMixin, db.Model): show_nsfl = db.Column(db.Boolean, default=False) created = db.Column(db.DateTime, default=datetime.utcnow) last_seen = db.Column(db.DateTime, default=datetime.utcnow, index=True) - role = db.Column(db.Integer, default=0) avatar_id = db.Column(db.Integer, db.ForeignKey('file.id')) cover_id = db.Column(db.Integer, db.ForeignKey('file.id')) public_key = db.Column(db.Text) @@ -164,6 +170,8 @@ class User(UserMixin, db.Model): posts = db.relationship('Post', backref='author', lazy='dynamic', cascade="all, delete-orphan") post_replies = db.relationship('PostReply', backref='author', lazy='dynamic', cascade="all, delete-orphan") + roles = db.relationship('Role', secondary=user_role, lazy='dynamic', cascade="all, delete") + def __repr__(self): return ''.format(self.user_name) @@ -489,6 +497,17 @@ class FilterKeyword(db.Model): user_id = db.Column(db.Integer, db.ForeignKey('user.id')) +class Role(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50)) + weight = db.Column(db.Integer, default=0) + permissions = db.relationship('RolePermission') + + +class RolePermission(db.Model): + role_id = db.Column(db.Integer, db.ForeignKey('role.id'), primary_key=True) + permission = db.Column(db.String, primary_key=True, index=True) + @login.user_loader def load_user(id): diff --git a/migrations/versions/882e33231c5b_user_roles_additional.py b/migrations/versions/882e33231c5b_user_roles_additional.py new file mode 100644 index 00000000..d900973b --- /dev/null +++ b/migrations/versions/882e33231c5b_user_roles_additional.py @@ -0,0 +1,38 @@ +"""user roles additional + +Revision ID: 882e33231c5b +Revises: f1f0c854ae18 +Create Date: 2023-10-18 22:10:16.602027 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '882e33231c5b' +down_revision = 'f1f0c854ae18' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('role_permission', schema=None) as batch_op: + batch_op.create_foreign_key(None, 'role', ['role_id'], ['id']) + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_column('role') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('role', sa.INTEGER(), autoincrement=False, nullable=True)) + + with op.batch_alter_table('role_permission', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + + # ### end Alembic commands ### diff --git a/migrations/versions/f1f0c854ae18_user_roles_permissions.py b/migrations/versions/f1f0c854ae18_user_roles_permissions.py new file mode 100644 index 00000000..8d633faa --- /dev/null +++ b/migrations/versions/f1f0c854ae18_user_roles_permissions.py @@ -0,0 +1,53 @@ +"""user roles permissions + +Revision ID: f1f0c854ae18 +Revises: e82f86c550ac +Create Date: 2023-10-18 21:39:43.281172 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f1f0c854ae18' +down_revision = 'e82f86c550ac' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('role', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=True), + sa.Column('weight', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('role_permission', + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('permission', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('role_id', 'permission') + ) + with op.batch_alter_table('role_permission', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_role_permission_permission'), ['permission'], unique=False) + + op.create_table('user_role', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'role_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user_role') + with op.batch_alter_table('role_permission', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_role_permission_permission')) + + op.drop_table('role_permission') + op.drop_table('role') + # ### end Alembic commands ###