User management
Signed-off-by: Tiago Garcia <tiago.rgarcia@ua.pt>
This commit is contained in:
parent
a28dc261f5
commit
2a40e0ccf1
|
@ -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)
|
|
@ -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]
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}}\"}"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
from .data_checks import validate_session_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
|
Loading…
Reference in New Issue