User management

Signed-off-by: Tiago Garcia <tiago.rgarcia@ua.pt>
This commit is contained in:
Tiago Garcia 2024-11-16 22:59:54 +00:00
parent a28dc261f5
commit 2a40e0ccf1
Signed by: TiagoRG
GPG Key ID: DFCD48E3F420DB42
11 changed files with 359 additions and 20 deletions

View File

@ -1,6 +1,7 @@
from flask import Flask
import sqlalchemy.exc
from flask import Flask, request, jsonify
from routes import org_bp, user_bp, file_bp
from database import db_connection
from database import db_connection, db
from models import Organization, User, File, Session
app = Flask(__name__)
@ -10,18 +11,35 @@ app.config["SQLALCHEMY_AUTOCOMMIT"] = False
app.config["SQLALCHEMY_AUTOFLUSH"] = False
db_connection.init_app(app)
with app.app_context():
try:
db_connection.session.query(Session).delete()
db_connection.session.commit()
except sqlalchemy.exc.OperationalError:
pass
db_connection.create_all()
app.register_blueprint(org_bp, url_prefix="/org")
app.register_blueprint(user_bp, url_prefix="/user")
app.register_blueprint(file_bp, url_prefix="/file")
@app.route("/reset")
@app.route("/", methods=["GET"])
def index():
return jsonify({"message": "Welcome to the API"}), 200
@app.route("/reset", methods=["POST"])
def reset():
with app.app_context():
db_connection.drop_all()
db_connection.create_all()
return "Database reset"
password = request.json["password"]
if password != "123":
return jsonify({"error": "Invalid password"}), 403
try:
with app.app_context():
db_connection.drop_all()
db_connection.create_all()
except sqlalchemy.exc.OperationalError:
return jsonify({"error": "Database error"}), 500
return jsonify({"message": "Database reset"}), 200
if __name__ == "__main__":
app.run(debug=True)

View File

@ -5,8 +5,9 @@ class Organization(db_connection.Model):
id = db_connection.Column(db_connection.Integer, primary_key=True, index=True)
name = db_connection.Column(db_connection.String, unique=True, index=True, nullable=False)
owner_id = db_connection.Column(db_connection.Integer, db_connection.ForeignKey('users.id'), nullable=False)
owner = db_connection.relationship('User', back_populates='orgs')
users = db_connection.Column(db_connection.JSON, nullable=False, default=dict)
owner_id = db_connection.Column(db_connection.Integer, db_connection.ForeignKey('users.id'))
owner = db_connection.relationship('User', backref=db_connection.backref('owned_organization', uselist=False))
files = db_connection.relationship('File', back_populates='org')
def to_dict(self):
@ -14,5 +15,11 @@ class Organization(db_connection.Model):
"id": self.id,
"name": self.name,
"owner": self.owner.to_dict(),
"users": [{"id": user_id, "user_data": {
"username": user_data["username"],
"full_name": user_data["full_name"],
"email": user_data["email"],
"status": user_data["status"]
}} for user_id, user_data in self.users.items()],
"files": [{"id": file.id, "name": file.name, "file_handle": file.file_handle} for file in self.files]
}

View File

@ -10,7 +10,8 @@ class User(db_connection.Model):
full_name = db_connection.Column(db_connection.String, nullable=False)
email = db_connection.Column(db_connection.String, unique=True, index=True, nullable=False)
public_keys = db_connection.Column(db_connection.JSON, nullable=False, default=dict)
orgs = db_connection.relationship('Organization', back_populates='owner')
orgs = db_connection.Column(db_connection.JSON, nullable=False, default=dict)
files = db_connection.relationship('File', back_populates='creator')
def to_dict(self):
@ -19,7 +20,6 @@ class User(db_connection.Model):
"username": self.username,
"full_name": self.full_name,
"email": self.email,
"public_keys": [{"org_id": org_id, "key": public_key} for org_id, public_key in self.public_keys.items()],
"orgs": [{"id": org.id, "name": org.name} for org in self.orgs],
"orgs": [{"id": org_id, "name": org_data["name"], "status": org_data["status"]} for org_id, org_data in self.orgs.items()],
"files": [{"id": file.id, "name": file.name, "file_handle": file.file_handle} for file in self.files]
}

View File

@ -2,3 +2,13 @@ from flask import Blueprint, request, jsonify
from services import FileService
file_bp = Blueprint("file", __name__)
@file_bp.route("/get", methods=["GET"])
def file_get():
data = request.json
file_handle = data["file_handle"]
file = FileService.get_file_by_file_handle(file_handle)
if not file:
return jsonify({"error": "File not found"}), 404
return jsonify(file.to_dict()), 200

View File

@ -1,5 +1,7 @@
import json
from flask import Blueprint, request, jsonify
from services import UserService, SessionService, OrganizationService
from utils import data_checks
user_bp = Blueprint("user", __name__)
@ -23,3 +25,131 @@ def user_login():
session = SessionService.create_session(user, org)
return jsonify(session.to_dict()), 201
@user_bp.route("/logout", methods=["POST"])
def user_logout():
data = request.json
session_file = data["session_file"]
session_data = json.loads(session_file)
session_token = session_data["token"]
session = SessionService.get_session(session_token)
if not session:
return jsonify({"error": "Not authenticated"}), 401
SessionService.delete_session(session)
return jsonify({"message": "Logged out"}), 200
@user_bp.route("/list", methods=["GET"])
def user_list():
data = request.json
if "session_file" not in data:
return jsonify({"error": "No session file"}), 400
session_file = data["session_file"]
session_data = json.loads(session_file)
session = data_checks.validate_session_file(session_data)
if isinstance(session, tuple):
return session
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
if "username" in data:
user = UserService.get_user_by_username(data["username"])
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify(user.to_dict()), 200
users = OrganizationService.get_users_in_organization(org)
return jsonify(users), 200
@user_bp.route("/create", methods=["POST"])
def user_create():
data = request.json
if "session_file" not in data or "username" not in data or "full_name" not in data or "email" not in data or "public_key" not in data:
return jsonify({"error": "Missing required fields"}), 400
session_file = data["session_file"]
session_data = json.loads(session_file)
session = data_checks.validate_session_file(session_data)
if isinstance(session, tuple):
return session
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
if org.owner.id != session.user_id:
return jsonify({"error": "Not authorized to create users"}), 403
user = UserService.get_user_by_username(data["username"])
if not user:
user = UserService.create_user(
username=data["username"],
full_name=data["full_name"],
email=data["email"],
public_key=data["public_key"],
org=org
)
return jsonify(user.to_dict()), 201
@user_bp.route("/suspend", methods=["POST"])
def user_suspend():
data = request.json
if "session_file" not in data or "username" not in data:
return jsonify({"error": "Missing required fields"}), 400
session_file = data["session_file"]
session_data = json.loads(session_file)
session = data_checks.validate_session_file(session_data)
if isinstance(session, tuple):
return session
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
if org.owner.id != session.user_id:
return jsonify({"error": "Not authorized to suspend users"}), 403
user = UserService.get_user_by_username(data["username"])
if not user:
return jsonify({"error": "User not found"}), 404
return OrganizationService.suspend_user(org, user)
@user_bp.route("/activate", methods=["POST"])
def user_unsuspend():
data = request.json
if "session_file" not in data or "username" not in data:
return jsonify({"error": "Missing required fields"}), 400
session_file = data["session_file"]
session_data = json.loads(session_file)
session = data_checks.validate_session_file(session_data)
if isinstance(session, tuple):
return session
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
if org.owner.id != session.user_id:
return jsonify({"error": "Not authorized to unsuspend users"}), 403
user = UserService.get_user_by_username(data["username"])
if not user:
return jsonify({"error": "User not found"}), 404
return OrganizationService.activate_user(org, user)

View File

@ -1,7 +1,8 @@
import os.path
from database import db
from models import Organization
from models import Organization, User
from sqlalchemy.orm.attributes import flag_modified
class OrganizationService:
@ -19,8 +20,13 @@ class OrganizationService:
organization = Organization(
name=name,
owner_id=user.id,
owner=user
owner=user,
users={user.id: {
"username": user.username,
"full_name": user.full_name,
"email": user.email,
"status": "active"
}}
)
db.add(organization)
@ -32,6 +38,10 @@ class OrganizationService:
return organization
@staticmethod
def list_organizations():
return db.query(Organization).all()
@staticmethod
def get_organization(org_id: int) -> Organization | None:
return db.query(Organization).filter(Organization.id == org_id).first()
@ -41,5 +51,49 @@ class OrganizationService:
return db.query(Organization).filter(Organization.name == name).first()
@staticmethod
def list_organizations():
return db.query(Organization).all()
def get_users_in_organization(org: Organization) -> list[User]:
return db.query(Organization).filter(Organization.id == org.id).first().users
@staticmethod
def get_user_status(org: Organization, user_id: int) -> str:
return db.query(Organization).filter(Organization.id == org.id).first().users[str(user_id)]["status"]
@staticmethod
def add_user_to_organization(org: Organization, user: User) -> Organization:
org.users[str(user.id)] = {
"username": user.username,
"full_name": user.full_name,
"email": user.email,
"status": "active"
}
flag_modified(org, "users")
db.commit()
db.refresh(org)
return org
@staticmethod
def suspend_user(org: Organization, user: User) -> tuple:
if OrganizationService.get_user_status(org, user.id) != "active":
return {"error": "User already suspended"}, 400
if org.owner.id == user.id:
return {"error": "Cannot suspend owner"}, 400
org.users[str(user.id)]["status"] = "suspended"
flag_modified(org, "users")
db.commit()
db.refresh(org)
return {"message": "User suspended"}, 200
@staticmethod
def activate_user(org: Organization, user: User) -> tuple:
if OrganizationService.get_user_status(org, user.id) != "suspended":
return {"error": "User already active"}, 400
org.users[str(user.id)]["status"] = "active"
flag_modified(org, "users")
db.commit()
db.refresh(org)
return {"message": "User activated"}, 200

View File

@ -17,7 +17,7 @@ class SessionService:
return session
@staticmethod
def get_session_by_token(token: str) -> Session | None:
def get_session(token: str) -> Session | None:
return db.query(Session).filter(Session.token == token).first()
@staticmethod

View File

@ -5,16 +5,22 @@ from models import User, Organization
class UserService:
@staticmethod
def create_user(username: str, full_name: str, email: str, public_key: str, org: Organization = None) -> User:
from services import OrganizationService
user = User(
username=username,
full_name=full_name,
email=email,
public_keys={org.id: public_key} if org else {},
orgs=[org] if org else []
orgs={org.id: {
"name": org.name,
"status": "active"
}} if org else {}
)
db.add(user)
db.commit()
db.refresh(user)
if org:
OrganizationService.add_user_to_organization(org, user)
return user
@staticmethod
@ -27,7 +33,12 @@ class UserService:
@staticmethod
def add_org_to_user(user: User, org: Organization) -> User:
user.orgs.append(org)
orgs = user.orgs.copy()
orgs[org.id] = {
"name": org.name,
"status": "active"
}
user.orgs = orgs
db.commit()
db.refresh(user)
return user

View File

@ -0,0 +1,80 @@
### Reset database
POST http://localhost:5000/reset
Content-Type: application/json
{
"password": "123"
}
### Create a new organization
POST http://localhost:5000/org/create
Content-Type: application/json
{
"name": "org",
"username": "username",
"full_name": "Full Name",
"email": "user@mail.com",
"public_key": "null"
}
### Login
POST http://localhost:5000/user/login
Content-Type: application/json
{
"username": "username",
"org": "org",
"public_key": "null"
}
> {% client.global.set("token", response.body["token"]) %}
### List organizations
GET http://localhost:5000/org/list
### Create a new user
POST http://localhost:5000/user/create
Content-Type: application/json
{
"session_file": "{\"token\":\"{{token}}\"}",
"username": "newuser",
"full_name": "Full Name",
"email": "newuser@mail.com",
"public_key": "null2"
}
### List users
GET http://localhost:5000/user/list
Content-Type: application/json
{
"session_file": "{\"token\":\"{{token}}\"}"
}
### Suspend user
POST http://localhost:5000/user/suspend
Content-Type: application/json
{
"session_file": "{\"token\":\"{{token}}\"}",
"username": "newuser"
}
### Activate user
POST http://localhost:5000/user/activate
Content-Type: application/json
{
"session_file": "{\"token\":\"{{token}}\"}",
"username": "newuser"
}
### Logout
POST http://localhost:5000/user/logout
Content-Type: application/json
{
"session_file": "{\"token\":\"{{token}}\"}"
}

View File

@ -0,0 +1 @@
from .data_checks import validate_session_file

View File

@ -0,0 +1,28 @@
import json
from flask import jsonify
from services import SessionService, OrganizationService
from models import Session
def validate_session_file(data) -> tuple | Session:
"""
Check if the session file is valid, and return the session object if it is
:param data: session file data (json)
:return: Session object or error response
"""
if "token" not in data:
return jsonify({"error": "No session token"}), 400
session_token = data["token"]
session = SessionService.get_session(session_token)
if not session:
return jsonify({"error": "Not authenticated"}), 401
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
status = OrganizationService.get_user_status(org, session.user_id)
if status != "active":
return jsonify({"error": "User is not active"}), 403
return session