diff --git a/README.md b/README.md index f9cf96cf..e72e0afc 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,24 @@ -# registry -Registry for Fortran package manager +# Registry for Fortran package manager -## Python Flask app with Nginx and Mongo database +Currently for the testing phase : +1. backend APIs are hosted at: http://registry-apis.vercel.app/ +2. frontend is hosted at: https://registry-frontend.vercel.app/ +3. Documentation for the APIs are available at: https://registry-apis.vercel.app/apidocs/ -Project structure: +**Please note: the current registry is a playground: its database will be fully deleted once its functionality is established. Please do not use it for production yet! more information will follow then.** + +The fpm release [0.8.2](https://fortran-lang.discourse.group/t/fpm-version-0-8-2-released-centralized-registry-playground/5792) introduces fpm support for uploading packages to the fpm-registry server directly from the command-line interface, via + +``` +fpm publish --token +``` + +fpm will now interact with a web interface that will help to manage the namespaces & packages. + + +## fpm - registry : Python Flask app with Nginx and Mongo database + +backend Project structure: ``` . ├── compose.yaml @@ -20,7 +35,7 @@ Project structure: ``` -## Deploy with docker compose +## Instructions for Deploy with docker compose ``` $ sudo chmod 666 /var/run/docker.sock (for root access) @@ -38,9 +53,9 @@ Hello world, Mongo Flask set MONGO_URI=MONGO_DB_ATLAS_URL (in .env file in flask directory) The MONGO_URI must be set in the environment (or, alternatively, in the .env file in the flask directory) to the URL value of the MongoDB to use. For example,If deploying to production, MONGO_URI should be set to mongo container address. set the following env variables in the .env file in the flask folder: - - SALT=MYSALT - - MONGO_URI - - MONGO_DB_NAME + - SALT + - MONGO_URI + - MONGO_DB_NAME - SUDO_PASSWORD - MONGO_USER_NAME - MONGO_PASSWORD @@ -53,3 +68,27 @@ Stop and remove the containers ``` $ docker compose down ``` + +## Steps to setup mongodump for registry Archives functionality +fpm - registry archives automatically created at weekly intervals by the mongodump command and stored in a tar archives format in the static directory of flask , to support caching and direct rendering of archives without manually fetching the mongodb for each archive request. to reduce the resource used by mongodb , we will only be installing the `mongodb-org-tools` only. the steps to setup mongodump on a Ubuntu linux 22.04 are: + +1. Import the public key used by the package management system. + +``` + wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add - + ``` + + 2. Create a list file for MongoDB. + + ``` +echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list + ``` + + 3. Reload local package database and install the tools: + + ``` + sudo apt-get update + sudo apt install mongodb-org-tools + ``` + +for more details, please refer: [mongodb tools installation docs](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/). \ No newline at end of file diff --git a/flask/auth.py b/flask/auth.py index 933179f4..ff418116 100644 --- a/flask/auth.py +++ b/flask/auth.py @@ -23,7 +23,7 @@ env_var["host"] = host env_var["salt"] = salt env_var["sudo_password"] = sudo_password - smtp = smtplib.SMTP('smtp.gmail.com', 587) + smtp = smtplib.SMTP("smtp.gmail.com", 587) smtp.starttls() smtp.login(fortran_email, fortran_password) @@ -47,14 +47,22 @@ def login(): password = request.form.get("password") password += salt hashed_password = hashlib.sha256(password.encode()).hexdigest() - query = {"$and": [{"$or": [{"email": user_identifier}, {"username": user_identifier}]}, {"password": hashed_password}]} - + query = { + "$and": [ + {"$or": [{"email": user_identifier}, {"username": user_identifier}]}, + {"password": hashed_password}, + ] + } + # search for the user both by user name or email user = db.users.find_one(query) if not user: return jsonify({"message": "Invalid email or password", "code": 401}), 401 + if not user["isverified"]: + return jsonify({"message": "Please verify your email", "code": 401}), 401 + uuid = generate_uuid() if user["loggedCount"] == 0 else user["uuid"] user["loggedCount"] += 1 @@ -124,24 +132,23 @@ def signup(): "createdAt": datetime.utcnow(), "uuid": uuid, "loggedCount": 1, + "isverified": False, + "newemail":'', } - if hashed_password == sudo_hashed_password: - user["roles"] = ["admin"] - forgot_password(email) - else: - user["roles"] = ["user"] - if not registry_user: + if hashed_password == sudo_hashed_password: + user["roles"] = ["admin"] + forgot_password(email) + else: + user["roles"] = ["user"] db.users.insert_one(user) - + send_verify_email(email) return ( jsonify( { - "message": "Signup successful", - "uuid": uuid, + "message": "Signup successful, Please verify your email", "code": 200, - "username": user["username"], } ), 200, @@ -196,40 +203,30 @@ def reset_password(): if not uuid: return jsonify({"message": "Unauthorized", "code": 401}), 401 - - if not oldpassword: - return jsonify({"message": "Please enter old password", "code": 400}), 400 - + if not password: return jsonify({"message": "Please enter new password", "code": 400}), 400 - + user = db.users.find_one({"uuid": uuid}) salt = env_var["salt"] if not user: return jsonify({"message": "User not found", "code": 404}), 404 - if not oldpassword: - password += salt - hashed_password = hashlib.sha256(password.encode()).hexdigest() - db.users.update_one( - {"uuid": uuid}, - {"$set": {"password": hashed_password}}, - ) - return jsonify({"message": "Password reset successful", "code": 200}), 200 - - oldpassword += salt - hashed_password = hashlib.sha256(oldpassword.encode()).hexdigest() - if hashed_password != user["password"]: - return jsonify({"message": "Invalid password", "code": 401}), 401 + if oldpassword: + oldpassword += salt + hashed_password = hashlib.sha256(oldpassword.encode()).hexdigest() + if hashed_password != user["password"]: + return jsonify({"message": "Invalid old password", "code": 401}), 401 + + password += salt + hashed_password = hashlib.sha256(password.encode()).hexdigest() db.users.update_one( - {"uuid": uuid}, - {"$set": {"password": hashed_password}}, - ) + {"uuid": uuid}, + {"$set": {"password": hashed_password}}, + ) return jsonify({"message": "Password reset successful", "code": 200}), 200 - - @app.route("/auth/forgot-password", methods=["POST"]) @swag_from("documentation/forgot_password.yaml", methods=["POST"]) @@ -244,6 +241,9 @@ def forgot_password(*email): if not user: return jsonify({"message": "User not found", "code": 404}), 404 + if not user["isverified"]: + return jsonify({"message": "Please verify your email", "code": 401}), 401 + uuid = generate_uuid() db.users.update_one({"email": email}, {"$set": {"uuid": uuid, "loggedCount": 1}}) @@ -257,8 +257,8 @@ def forgot_password(*email): Thank you, The Fortran-lang Team""" - message = f'Subject: Password reset link\nTo: {email}\n{message}' - + message = f"Subject: Password reset link\nTo: {email}\n{message}" + # sending the mail smtp.sendmail(to_addrs=email, msg=message, from_addr=fortran_email) @@ -266,3 +266,87 @@ def forgot_password(*email): jsonify({"message": "Password reset link sent to your email", "code": 200}), 200, ) + + +def send_verify_email(email): + query = {"$and": [{"$or": [{"email": email}, {"newemail": email}]}]} + + user = db.users.find_one(query) + + if not user: + return jsonify({"message": "User not found", "code": 404}), 404 + + uuid = user["uuid"] + + message = f"""\n + Dear {user['username']}, + + We received a request to verify your email. To verify your email, please copy paste the link below in a new browser window: + + {env_var['host']}/account/verify/{uuid} + + Thank you, + The Fortran-lang Team""" + + message = f"Subject: Verify email \nTo: {email}\n{message}" + + # sending the mail + smtp.sendmail(to_addrs=email, msg=message, from_addr=fortran_email) + + return ( + jsonify({"message": "verification link sent to your email", "code": 200}), + 200, + ) + + +@app.route("/auth/verify-email", methods=["POST"]) +def verify_email(): + uuid = request.form.get("uuid") + + if not uuid: + return jsonify({"message": "Unauthorized", "code": 401}), 401 + + user = db.users.find_one({"uuid": uuid}) + + if not user: + return jsonify({"message": "User not found", "code": 404}), 404 + + if user["newemail"] != "": + db.users.update_one( + {"uuid": uuid}, {"$set": {"email": user["newemail"], "newemail": ""}} + ) + + if not user['isverified']: + db.users.update_one({"uuid": uuid}, {"$set": {"isverified": True}}) + + return jsonify({"message": "Successfully Verified Email", "code": 200}), 200 + + +@app.route("/auth/change-email", methods=["POST"]) +def change_email(): + uuid = request.form.get("uuid") + new_email = request.form.get("newemail") + + if not uuid: + return jsonify({"message": "Unauthorized", "code": 401}), 401 + + user = db.users.find_one({"uuid": uuid}) + + if not user: + return jsonify({"message": "User not found", "code": 404}), 404 + + if not new_email: + return jsonify({"message": "Please enter new email", "code": 400}), 400 + + used_email = db.users.find_one({"email": new_email}) + + if used_email: + return jsonify({"message": "Email already in use", "code": 400}), 400 + + db.users.update_one( + {"uuid": uuid}, + {"$set": {"newemail": new_email}}, + ) + send_verify_email(new_email) + + return jsonify({"message": "Please verify your new email.", "code": 200}), 200 diff --git a/flask/mongo.py b/flask/mongo.py index 4d321d16..501529b0 100644 --- a/flask/mongo.py +++ b/flask/mongo.py @@ -4,15 +4,15 @@ from dotenv import load_dotenv from gridfs import GridFS from app import app -from flask import send_file +from flask import jsonify import subprocess load_dotenv() -database_name = os.environ['MONGO_DB_NAME'] +database_name = os.environ["MONGO_DB_NAME"] try: - mongo_uri = os.environ['MONGO_URI'] - mongo_username = os.environ['MONGO_USER_NAME'] - mongo_password = os.environ['MONGO_PASSWORD'] + mongo_uri = os.environ["MONGO_URI"] + mongo_username = os.environ["MONGO_USER_NAME"] + mongo_password = os.environ["MONGO_PASSWORD"] client = MongoClient(mongo_uri) except KeyError as err: print("Add MONGO_URI to .env file") @@ -21,35 +21,24 @@ file_storage = GridFS(db, collection="tarballs") -@app.route("/registry/clone", methods=["GET"]) +@app.route("/registry/archives", methods=["GET"]) def clone(): - filename = "registry.tar.gz" - static_path = os.path.join(os.getcwd(), "static") - file_path = os.path.join(static_path, filename) + folder_path = "static" + file_list = os.listdir(folder_path) - # Check if the file exists and was modified less than 1 week ago - if os.path.exists(file_path): - mod_time = datetime.fromtimestamp(os.path.getmtime(file_path)) - if datetime.now() - mod_time < timedelta(days=7): - return send_file(file_path, as_attachment=True) - - generate_latest_tarball() - return send_file(file_path, as_attachment=True) + # Check if the folder exists and was modified more than 1 week ago + if os.path.exists(folder_path): + mod_time = datetime.fromtimestamp(os.path.getmtime(folder_path)) + if datetime.now() - mod_time > timedelta(days=7): + generate_latest_tarball() + return jsonify( + {"message": "Successfully Fetched Archives", "archives": file_list, "code": 200} + ) -def generate_latest_tarball(): - backup_dir = "static" - if not os.path.exists(backup_dir): - os.mkdir(backup_dir) +def generate_latest_tarball(): # Execute the mongodump command - command = f"mongodump --host {mongo_uri} --authenticationDatabase admin --username {mongo_username} --password {mongo_password} --db {database_name} --out {backup_dir}" - subprocess.call(command, shell=True) - - # Create a tar archive of the backup directory - archive_name = "registry.tar.gz" - archive_path = os.path.join(backup_dir, archive_name) - - # Create the archive file - command = f"tar -czvf {archive_path} static/" + archive_date = datetime.datetime.now().strftime("%Y-%m-%d") + command = f"mongodump --uri={mongo_uri}--archive=static/registry-{archive_date}.tar.gz --db={database_name} --gzip --excludeCollection=users" subprocess.call(command, shell=True) \ No newline at end of file diff --git a/flask/packages.py b/flask/packages.py index ee544a21..c38a6204 100644 --- a/flask/packages.py +++ b/flask/packages.py @@ -7,12 +7,17 @@ from datetime import datetime, timedelta from auth import generate_uuid from app import swagger +import zipfile +import tarfile +import io +import toml from flasgger.utils import swag_from from urllib.parse import unquote import math import semantic_version from license_expression import get_spdx_licensing -# from validate_package import validate_package +# import validate_package + parameters = { "name": "name", @@ -22,6 +27,7 @@ "downloads": "downloads", } + def is_valid_version_str(version_str): """ Function to verify whether the version string is valid or not. @@ -39,6 +45,7 @@ def is_valid_version_str(version_str): except: return False + def is_valid_license_identifier(license_str): """ Function to check whether the license string is a valid identifier or not. @@ -56,6 +63,7 @@ def is_valid_license_identifier(license_str): except: return False + @app.route("/packages", methods=["GET"]) @swag_from("documentation/search_packages.yaml", methods=["GET"]) def search_packages(): @@ -119,9 +127,18 @@ def search_packages(): i["namespace"] = namespace["namespace"] i["author"] = author["username"] search_packages.append(i) - return jsonify({"code": 200, "packages": search_packages, "total_pages": total_pages}), 200 + return ( + jsonify( + {"code": 200, "packages": search_packages, "total_pages": total_pages} + ), + 200, + ) else: - return jsonify({"status": "error", "message": "packages not found", "code": 404}), 404 + return ( + jsonify({"status": "error", "message": "packages not found", "code": 404}), + 404, + ) + @app.route("/packages", methods=["POST"]) @swag_from("documentation/package_upload.yaml", methods=["POST"]) @@ -130,97 +147,172 @@ def upload(): package_name = request.form.get("package_name") package_version = request.form.get("package_version") package_license = request.form.get("package_license") + dry_run = request.form.get("dry_run") tarball = request.files["tarball"] + dry_run = True if dry_run == "true" else False + if not upload_token: - return jsonify({"code": 400, "message": "Upload token missing"}) - + return jsonify({"code": 400, "message": "Upload token missing"}), 400 + if not package_name: - return jsonify({"code": 400, "message": "Package name is missing"}) - + return jsonify({"code": 400, "message": "Package name is missing"}), 400 + if not package_version: - return jsonify({"code": 400, "message": "Package version is missing"}) - + return jsonify({"code": 400, "message": "Package version is missing"}), 400 + if not package_license: - return jsonify({"code": 400, "message": "Package license is missing"}) - + return jsonify({"code": 400, "message": "Package license is missing"}), 400 + # Check whether version string is valid or not. if package_version == "0.0.0" or not is_valid_version_str(package_version): - return jsonify({"code": 400, "message": "Version is not valid"}) - + return jsonify({"code": 400, "message": "Version is not valid"}), 400 + # Check whether license identifier is valid or not. if not is_valid_license_identifier(license_str=package_license): - return jsonify({"code": 400, "message": f"Invalid license identifier {package_license}. Please check the SPDX license identifier list."}) - + return ( + jsonify( + { + "code": 400, + "message": f"Invalid license identifier {package_license}. Please check the SPDX license identifier list.", + } + ), + 400, + ) + # Find the document that contains the upload token. - namespace_doc = db.namespaces.find_one({"upload_tokens": {"$elemMatch": {"token": upload_token}}}) - package_doc = db.packages.find_one({"upload_tokens": {"$elemMatch": {"token": upload_token}}}) + namespace_doc = db.namespaces.find_one( + {"upload_tokens": {"$elemMatch": {"token": upload_token}}} + ) + package_doc = db.packages.find_one( + {"upload_tokens": {"$elemMatch": {"token": upload_token}}} + ) if not namespace_doc and not package_doc: - return jsonify({"code": 401, "message": "Invalid upload token"}) + return jsonify({"code": 401, "message": "Invalid upload token"}), 401 if namespace_doc: - upload_token_doc = next(item for item in namespace_doc['upload_tokens'] if item['token'] == upload_token) - package_doc = db.packages.find_one({"name": package_name, "namespace": namespace_doc["_id"]}) + upload_token_doc = next( + item + for item in namespace_doc["upload_tokens"] + if item["token"] == upload_token + ) + package_doc = db.packages.find_one( + {"name": package_name, "namespace": namespace_doc["_id"]} + ) elif package_doc: if package_doc["name"] != package_name: - return jsonify({"code": 401, "message": "Invalid upload token"}) - - upload_token_doc = next(item for item in package_doc['upload_tokens'] if item['token'] == upload_token) + return jsonify({"code": 401, "message": "Invalid upload token"}), 401 + + upload_token_doc = next( + item + for item in package_doc["upload_tokens"] + if item["token"] == upload_token + ) namespace_doc = db.namespaces.find_one({"_id": package_doc["namespace"]}) # Check if the token is expired. # Expire the token after one week of it's creation. - if check_token_expiry(upload_token_created_at=upload_token_doc['createdAt']): - return jsonify({"code": 401, "message": "Upload token has been expired. Please generate a new one"}) + if check_token_expiry(upload_token_created_at=upload_token_doc["createdAt"]): + return ( + jsonify( + { + "code": 401, + "message": "Upload token has been expired. Please generate a new one", + } + ), + 401, + ) # Get the user connected to the upload token. user_id = upload_token_doc["createdBy"] user = db.users.find_one({"_id": user_id}) if not user: - return jsonify({"code": 404, "message": "User not found"}) - + return jsonify({"code": 404, "message": "User not found"}), 404 + if not package_doc: # User should be either namespace maintainer or namespace admin to upload a package. - if checkUserUnauthorizedForNamespaceTokenCreation(user_id=user["_id"], namespace_doc=namespace_doc): - return jsonify({"code": 401, "message": "Unauthorized"}) + if checkUserUnauthorizedForNamespaceTokenCreation( + user_id=user["_id"], namespace_doc=namespace_doc + ): + return jsonify({"code": 401, "message": "Unauthorized"}), 401 else: # User should be either namespace maintainer or namespace admin or package maintainer to upload a package. - if checkUserUnauthorized(user_id=user["_id"], package_namespace=namespace_doc, package_doc=package_doc): - return jsonify({"message": "Unauthorized", "code": 401}) - - package_doc = db.packages.find_one({"name": package_name, "namespace": namespace_doc["_id"]}) + if checkUserUnauthorized( + user_id=user["_id"], + package_namespace=namespace_doc, + package_doc=package_doc, + ): + return jsonify({"message": "Unauthorized", "code": 401}), 401 + + package_doc = db.packages.find_one( + {"name": package_name, "namespace": namespace_doc["_id"]} + ) + + if tarball.content_type not in ["application/gzip", "application/zip"]: + return jsonify({"code": 400, "message": "Invalid file type"}), 400 tarball_name = "{}-{}.tar.gz".format(package_name, package_version) # Upload the tarball to the Grid FS storage. - file_object_id = file_storage.put(tarball, content_type="application/gzip", filename=tarball_name) + # try: TODO: Enable this after Validation is Enabled + # zipball, tarball = get_file_object(tarball) + # except Exception as e: + # return jsonify({"code": 400, "message": "Invalid package tarball."}), 400 + + # zipfile_object_id = file_storage.put( + # zipball, content_type=zipball.content_type, filename=tarball_name + ".zip" + # ) + # tarfile_object_id = file_storage.put( + # tarball, content_type=tarball.content_type, filename=tarball_name + ".tar.gz" + # ) + file_object_id = file_storage.put( + tarball, content_type=tarball.content_type, filename=tarball_name + ) + + # Extract the package metadata from the tarball's fpm.toml file. + try: + package_data = extract_fpm_toml(tarball) + except Exception as e: + return jsonify({"code": 400, "message": "Invalid package tarball."}), 400 # TODO: Uncomment this when the package validation is enabled + # validate the package with fpm - # validate the package - # valid_package = validate_package(tarball_name, tarball_name) + # valid_package = validate_package.validate_package(tarball,"{}-{}".format(package_name, package_version)) # if not valid_package: # return jsonify({"status": "error", "message": "Invalid package", "code": 400}), 400 - # No previous recorded versions of the package found. if not package_doc: - package_obj = { - "name": package_name, - "namespace": namespace_doc["_id"], - "description": "Sample Test description", - "license": package_license, - "createdAt": datetime.utcnow(), - "updatedAt": datetime.utcnow(), - "author": user["_id"], - "maintainers": [], - "copyright": "Test copyright", - "tags": ["fortran", "fpm"], - "isDeprecated": False, - } + try: + package_obj = { + "name": package_name, + "namespace": namespace_doc["_id"], + "description": package_data["description"], + "homepage": package_data["homepage"], + "repository": package_data["repository"], + "license": package_license, + "createdAt": datetime.utcnow(), + "updatedAt": datetime.utcnow(), + "author": user["_id"], + "maintainers": [user["_id"]], + "copyright": package_data["copyright"], + "tags": ["fortran", "fpm"], + "isDeprecated": False, + } + except KeyError as e: + return ( + jsonify( + { + "code": 400, + "message": f"Invalid package metadata. {e} is missing", + } + ), + 400, + ) version_obj = { "version": package_version, @@ -228,17 +320,26 @@ def upload(): "dependencies": "Test dependencies", "createdAt": datetime.utcnow(), "isDeprecated": False, - "download_url": f"/tarballs/{file_object_id}" + # "download_url_zip": f"/tarballs/{zipfile_object_id}", TODO: Uncomment this when the package validation is enabled + # "download_url_tar": f"/tarballs/{tarfile_object_id}", } package_obj["versions"] = [] # Append the first version document. package_obj["versions"].append(version_obj) + + if dry_run: + return jsonify({"message": "Dry run Successful.", "code": 200}) + db.packages.insert_one(package_obj) package = db.packages.find_one( - {"name": package_name, "versions.version": package_version, "namespace": namespace_doc["_id"]} + { + "name": package_name, + "versions.version": package_version, + "namespace": namespace_doc["_id"], + } ) # Add the package id to the namespace. @@ -251,14 +352,19 @@ def upload(): # Current user is the author of the package. user["authorOf"].append(package["_id"]) + db.users.update_one({"_id": user["_id"]}, {"$set": user}) return jsonify({"message": "Package Uploaded Successfully.", "code": 200}) else: # Check if version of the package already exists in the backend. - package_version_doc = db.packages.find_one({ - "name": package_name, "namespace": namespace_doc["_id"], "versions.version": package_version - }) + package_version_doc = db.packages.find_one( + { + "name": package_name, + "namespace": namespace_doc["_id"], + "versions.version": package_version, + } + ) if package_version_doc: return jsonify({"message": "Version already exists", "code": 400}), 400 @@ -269,18 +375,27 @@ def upload(): "dependencies": "Test dependencies", "isDeprecated": False, "createdAt": datetime.utcnow(), - "download_url": f"/tarballs/{file_object_id}" + "download_url": f"/tarballs/{file_object_id}", + # "download_url_zip": f"/tarballs/{zipfile_object_id}", + # "download_url_tar": f"/tarballs/{tarfile_object_id}", } package_doc["versions"].append(new_version) - package_doc["versions"] = sorted(package_doc["versions"], key=lambda x: x['version']) + package_doc["versions"] = sorted( + package_doc["versions"], key=lambda x: x["version"] + ) package_doc["updatedAt"] = datetime.utcnow() + + if dry_run: + return jsonify({"message": "Dry run Successful.", "code": 200}), 200 + db.packages.update_one( {"_id": package_doc["_id"]}, {"$set": package_doc}, ) - return jsonify({"message": "Package Uploaded Successfully.", "code": 200}) + return jsonify({"message": "Package Uploaded Successfully.", "code": 200}), 200 + def check_token_expiry(upload_token_created_at): """ @@ -300,9 +415,9 @@ def check_token_expiry(upload_token_created_at): # Check if the time difference is greater than 1 week if time_diff > timedelta(weeks=1): return True - + return False - + @app.route('/tarballs/', methods=["GET"]) @swag_from("documentation/get_tarball.yaml", methods=["GET"]) def serve_gridfs_file(oid): @@ -310,7 +425,12 @@ def serve_gridfs_file(oid): file = file_storage.get(ObjectId(oid)) # Return the file data as a Flask response object - return send_file(file, download_name=file.filename, as_attachment=True, mimetype=file.content_type) + return send_file( + file, + download_name=file.filename, + as_attachment=True, + mimetype=file.content_type, + ) except NoFile: abort(404) @@ -320,7 +440,7 @@ def check_version(current_version, new_version): return new_list > current_list -@app.route("/packages//", methods=["GET"]) +@app.route("/packages//", methods=["GET", "POST"]) @swag_from("documentation/get_package.yaml", methods=["GET"]) def get_package(namespace_name, package_name): # Get namespace from namespace name. @@ -328,7 +448,10 @@ def get_package(namespace_name, package_name): # Check if namespace exists. if not namespace: - return jsonify({"status": "error", "message": "Namespace not found", "code": 404}), 404 + return ( + jsonify({"status": "error", "message": "Namespace not found", "code": 404}), + 404, + ) # Get package from a package_name and namespace's id. package = db.packages.find_one( @@ -337,8 +460,7 @@ def get_package(namespace_name, package_name): # Check if package is not found. if not package: - return jsonify({"message": "Package not found", "code": 404}) - + return jsonify({"message": "Package not found", "code": 404}), 404 # Get the package author from id. package_author = db.users.find_one({"_id": package["author"]}) @@ -359,7 +481,7 @@ def get_package(namespace_name, package_name): return jsonify({"data": package_response_data, "code": 200}) - + @app.route("/packages///verify", methods=["POST"]) @swag_from("documentation/verify_user_role.yaml", methods=["POST"]) def verify_user_role(namespace_name, package_name): @@ -376,18 +498,31 @@ def verify_user_role(namespace_name, package_name): namespace = db.namespaces.find_one({"namespace": namespace_name}) if not namespace: - return jsonify({"status": "error", "message": "Namespace not found", "code": 404}), 404 - - package = db.packages.find_one({"name": package_name, "namespace": namespace["_id"]}) + return ( + jsonify({"status": "error", "message": "Namespace not found", "code": 404}), + 404, + ) + + package = db.packages.find_one( + {"name": package_name, "namespace": namespace["_id"]} + ) if not package: - return jsonify({"status": "error", "message": "Package not found", "code": 404}), 404 - - if str(user["_id"]) in [str(obj_id) for obj_id in namespace["maintainers"]] or str(user["_id"]) in [str(obj_id) for obj_id in namespace["admins"]] or str(user["_id"]) in [str(obj_id) for obj_id in package["maintainers"]]: + return ( + jsonify({"status": "error", "message": "Package not found", "code": 404}), + 404, + ) + + if ( + str(user["_id"]) in [str(obj_id) for obj_id in namespace["maintainers"]] + or str(user["_id"]) in [str(obj_id) for obj_id in namespace["admins"]] + or str(user["_id"]) in [str(obj_id) for obj_id in package["maintainers"]] + ): return jsonify({"status": "success", "code": 200, "isVerified": True}), 200 else: return jsonify({"status": "error", "code": 401, "isVerified": False}), 401 - + + @app.route("/packages///", methods=["GET"]) @swag_from("documentation/get_version.yaml", methods=["GET"]) def get_package_from_version(namespace_name, package_name, version): @@ -396,7 +531,7 @@ def get_package_from_version(namespace_name, package_name, version): # Check if namespace does not exists. if not namespace: - return jsonify({"message": "Namespace not found", "code": 404}) + return jsonify({"message": "Namespace not found", "code": 404}), 404 # Get package from a package_name, namespace's id and version. package = db.packages.find_one( @@ -409,7 +544,7 @@ def get_package_from_version(namespace_name, package_name, version): # Check if package is not found. if not package: - return jsonify({"message": "Package not found", "code": 404}) + return jsonify({"message": "Package not found", "code": 404}), 404 else: # Get the package author from id. @@ -434,7 +569,7 @@ def get_package_from_version(namespace_name, package_name, version): "description": package["description"], } - return jsonify({"data": package_response_data, "code": 200}) + return jsonify({"data": package_response_data, "code": 200}), 200 @app.route("/packages///delete", methods=["POST"]) @@ -457,7 +592,7 @@ def delete_package(namespace_name, package_name): { "status": "error", "message": "User is not authorized to delete the package", - "code": 401 + "code": 401, } ), 401, @@ -467,7 +602,7 @@ def delete_package(namespace_name, package_name): namespace = db.namespaces.find_one({"namespace": namespace_name}) if not namespace: - return jsonify({"message": "Namespace not found", "code": 404}) + return jsonify({"message": "Namespace not found", "code": 404}), 404 # Find package using package_name & namespace_name. package = db.packages.find_one( @@ -485,7 +620,7 @@ def delete_package(namespace_name, package_name): if package_deleted.deleted_count > 0: return jsonify({"message": "Package deleted successfully", "code": 200}), 200 else: - return jsonify({"message": "Internal Server Error", "code": 500}) + return jsonify({"message": "Internal Server Error", "code": 500}), 500 @app.route( @@ -510,7 +645,7 @@ def delete_package_version(namespace_name, package_name, version): { "status": "error", "message": "User is not authorized to delete the package", - "code": 401 + "code": 401, } ), 401, @@ -521,7 +656,7 @@ def delete_package_version(namespace_name, package_name, version): # Check if namespace does not exists. if not namespace: - return jsonify({"message": "Namespace does not found", "code": 404}) + return jsonify({"message": "Namespace does not found", "code": 404}), 404 # Perform the pull operation. result = db.packages.update_one( @@ -532,7 +667,12 @@ def delete_package_version(namespace_name, package_name, version): if result.matched_count: return jsonify({"message": "Package version deleted successfully"}), 200 else: - return jsonify({"status": "error", "message": "Package version not found", "code": 404}), 404 + return ( + jsonify( + {"status": "error", "message": "Package version not found", "code": 404} + ), + 404, + ) @app.route("/packages///uploadToken", methods=["POST"]) @@ -543,48 +683,65 @@ def create_token_upload_token_package(namespace_name, package_name): if not uuid: return jsonify({"code": 401, "message": "Unauthorized"}), 401 - + # Get the user from uuid. user_doc = db.users.find_one({"uuid": uuid}) if not user_doc: return jsonify({"code": 401, "message": "Unauthorized"}), 401 - + # Get the namespace from namespace_name. namespace_doc = db.namespaces.find_one({"namespace": namespace_name}) if not namespace_doc: return jsonify({"code": 404, "message": "Namespace not found"}), 404 - + # Get the package from package_name & namespace_id. - package_doc = db.packages.find_one({"name": package_name, "namespace": namespace_doc["_id"]}) + package_doc = db.packages.find_one( + {"name": package_name, "namespace": namespace_doc["_id"]} + ) if not package_doc: return jsonify({"code": 404, "message": "Package not found"}), 404 - + # Check if the user is authorized to generate package token. # Only package maintainers will have the option to generate tokens for a package. - if not str(user_doc["_id"]) in [str(obj_id) for obj_id in package_doc["maintainers"]]: - return jsonify({"code": 401, "message": "Only package maintainers can create tokens"}), 401 - + if not str(user_doc["_id"]) in [ + str(obj_id) for obj_id in package_doc["maintainers"] + ]: + return ( + jsonify( + {"code": 401, "message": "Only package maintainers can create tokens"} + ), + 401, + ) + # Generate the token. upload_token = generate_uuid() upload_token_obj = { "token": upload_token, "createdAt": datetime.utcnow(), - "createdBy": user_doc["_id"] + "createdBy": user_doc["_id"], } db.packages.update_one( - {"_id": package_doc["_id"]}, - {"$addToSet": {"upload_tokens": upload_token_obj}} + {"_id": package_doc["_id"]}, {"$addToSet": {"upload_tokens": upload_token_obj}} + ) + + return ( + jsonify( + { + "code": 200, + "message": "Upload token created successfully", + "uploadToken": upload_token, + } + ), + 200, ) - - return jsonify({"code": 200, "message": "Upload token created successfully", "uploadToken": upload_token}), 200 -@app.route("/packages///maintainers", methods=["POST"]) -@swag_from("documentation/package_maintainers.yaml", methods=["POST"]) +@app.route("/packages///maintainers", methods=["GET"]) +@swag_from("documentation/package_maintainers.yaml", methods=["GET"]) def package_maintainers(namespace, package): uuid = request.form.get("uuid") @@ -600,26 +757,25 @@ def package_maintainers(namespace, package): if not namespace_doc: return jsonify({"message": "Namespace not found", "code": 404}) - - package_doc = db.packages.find_one({"name": package, "namespace": namespace_doc["_id"]}) + + package_doc = db.packages.find_one( + {"name": package, "namespace": namespace_doc["_id"]} + ) if not package_doc: return jsonify({"message": "Package not found", "code": 404}) - - if str(user["_id"]) not in [str(obj_id) for obj_id in namespace_doc["maintainers"]] and str(user["_id"]) not in [str(obj_id) for obj_id in namespace_doc["admins"]] and str(user["_id"]) not in [str(obj_id) for obj_id in package_doc["maintainers"]]: - return jsonify({"code": 401, "message": "Unauthorized"}), 401 - + maintainers = [] for i in package_doc["maintainers"]: maintainer = db.users.find_one({"_id": i}, {"_id": 1, "username": 1}) - maintainers.append({ - "id": str(maintainer["_id"]), - "username": maintainer["username"] - }) + maintainers.append( + {"id": str(maintainer["_id"]), "username": maintainer["username"]} + ) return jsonify({"code": 200, "users": maintainers}), 200 + def sort_versions(versions): """ Sorts the list of version in the reverse order. Such that the latest version comes at @@ -627,16 +783,72 @@ def sort_versions(versions): """ return sorted(versions, key=lambda x: [int(i) for i in x.split(".")], reverse=True) + # This function checks if user is authorized to upload/update a package in a namespace. def checkUserUnauthorized(user_id, package_namespace, package_doc): admins_id_list = [str(obj_id) for obj_id in package_namespace["admins"]] maintainers_id_list = [str(obj_id) for obj_id in package_namespace["maintainers"]] pkg_maintainers_id_list = [str(obj_id) for obj_id in package_doc["maintainers"]] str_user_id = str(user_id) - return str_user_id not in admins_id_list and str_user_id not in maintainers_id_list and str_user_id not in pkg_maintainers_id_list + return ( + str_user_id not in admins_id_list + and str_user_id not in maintainers_id_list + and str_user_id not in pkg_maintainers_id_list + ) + def checkUserUnauthorizedForNamespaceTokenCreation(user_id, namespace_doc): admins_id_list = [str(obj_id) for obj_id in namespace_doc["admins"]] maintainers_id_list = [str(obj_id) for obj_id in namespace_doc["maintainers"]] str_user_id = str(user_id) - return str_user_id not in admins_id_list and str_user_id not in maintainers_id_list \ No newline at end of file + return str_user_id not in admins_id_list and str_user_id not in maintainers_id_list + + +def extract_fpm_toml(file_obj): + tar = tarfile.open(fileobj=file_obj, mode='r') + + fpm_toml_file = None + for file in tar.getmembers(): + if file.name == 'fpm.toml': + fpm_toml_file = file + break + + if fpm_toml_file is None: + raise ValueError("fpm.toml file not found in the tarball.") + + extracted_file = tar.extractfile(fpm_toml_file) + toml_data = extracted_file.read() + tar.close() + parsed_toml = toml.loads(toml_data) + return parsed_toml + +def convert_zip_to_tar(zip_file): + tar_file = io.BytesIO() + with zipfile.ZipFile(zip_file, 'r') as zip_ref: + with tarfile.open(fileobj=tar_file, mode='w') as tar_ref: + for file_name in zip_ref.namelist(): + file_data = zip_ref.read(file_name) + tar_info = tarfile.TarInfo(name=file_name) + tar_info.size = len(file_data) + tar_ref.addfile(tar_info, fileobj=io.BytesIO(file_data)) + + tar_file.seek(0) # Reset the file position for reading + return tar_file + + +def convert_to_zip(file_obj): + with tarfile.open(file_obj, "r:gz") as tar: + tar.extractall() + with zipfile.ZipFile("package.zip", "w") as zip_ref: + zip_ref.write("fpm.toml") + zip_ref.write("package") + return zip_ref + + +def get_file_object(file_obj): + if file_obj.content_type == "application/zip": + return file_obj, convert_zip_to_tar(file_obj) + elif file_obj.content_type == "application/gzip": + return convert_to_zip(file_obj), file_obj + else: + raise Exception("Invalid file type") diff --git a/flask/requirements.txt b/flask/requirements.txt index 6a3de376..095b0f95 100755 --- a/flask/requirements.txt +++ b/flask/requirements.txt @@ -8,3 +8,4 @@ flasgger license-expression semantic-version docker +toml \ No newline at end of file diff --git a/flask/server.py b/flask/server.py index e3c806dc..7273e62b 100755 --- a/flask/server.py +++ b/flask/server.py @@ -6,7 +6,7 @@ import user import packages import namespaces -# import validate_package +# import validate_package # TODO: Uncomment this when the package validation is enabled @app.route("/") def index(): diff --git a/flask/user.py b/flask/user.py index f69d1861..0ea807de 100644 --- a/flask/user.py +++ b/flask/user.py @@ -215,47 +215,49 @@ def admin(): ) + @app.route("/users/admin/transfer", methods=["POST"]) @swag_from("documentation/transfer_account.yaml", methods=["POST"]) def transfer_account(): - uuid = request.form.get("uuid") - if not uuid: - return jsonify({"message": "Unauthorized", "code": 401}), 401 - else: - user = db.users.find_one({"uuid": uuid}) - - if not user: - return jsonify({"message": "User not found", "code": 404}), 404 - - if "admin" not in user["roles"]: - return jsonify({"message": "Unauthorized", "code": 401}), 401 - else: - old_user = request.form.get("old_username") - new_user = request.form.get("new_username") - new_email = request.form.get("new_email") - db.users.update_one( - {"username": old_user}, - { - "$set": { - "email": new_email, - "username": new_user, - "uuid": "", - "loggedCount": 0, - "loginAt": None, - "lastLogout": None, - } - }, - ) - forgot_password(new_email) - return ( - jsonify( - { - "message": "Account Transfer Successful and Password reset request sent.", - "code": 200, - } - ), - 200, - ) + return jsonify({"message": "This Functionality has been disabled.", "code": 501}), 501 + # uuid = request.form.get("uuid") + # if not uuid: + # return jsonify({"message": "Unauthorized", "code": 401}), 401 + # else: + # user = db.users.find_one({"uuid": uuid}) + + # if not user: + # return jsonify({"message": "User not found", "code": 404}), 404 + + # if "admin" not in user["roles"]: + # return jsonify({"message": "Unauthorized", "code": 401}), 401 + # else: + # old_user = request.form.get("old_username") + # new_user = request.form.get("new_username") + # new_email = request.form.get("new_email") + # db.users.update_one( + # {"username": old_user}, + # { + # "$set": { + # "email": new_email, + # "username": new_user, + # "uuid": "", + # "loggedCount": 0, + # "loginAt": None, + # "lastLogout": None, + # } + # }, + # ) + # forgot_password(new_email) + # return ( + # jsonify( + # { + # "message": "Account Transfer Successful and Password reset request sent.", + # "code": 200, + # } + # ), + # 200, + # ) @app.route("//maintainer", methods=["POST"]) diff --git a/flask/validate_package.Dockerfile b/flask/validate_package.Dockerfile index 0343f96c..131ed081 100644 --- a/flask/validate_package.Dockerfile +++ b/flask/validate_package.Dockerfile @@ -8,11 +8,10 @@ RUN apk add --no-cache \ gfortran \ git \ musl-dev \ + tar \ wget -# Create a non-root user -RUN adduser -D registry -USER registry +USER root WORKDIR /home/registry # Set up fpm diff --git a/flask/validate_package.py b/flask/validate_package.py index beb7c7eb..8cb699cf 100755 --- a/flask/validate_package.py +++ b/flask/validate_package.py @@ -1,9 +1,8 @@ import os import docker -import tarfile from app import app -#package testing container +# Package testing container client = docker.from_env() container = client.containers.run( "registry", @@ -13,22 +12,17 @@ ) -# Copying package file to container -def copy_to(package, dst, container): +def validate_package(package, packagename): data = package.read() - container.put_archive(os.path.dirname(dst), data) - - -def validate_package(tarball,packagename): - copy_to(tarball, f'/home/registry/{packagename}.zip', container) - container.exec_run('unzip package.zip') - build_response = container.exec_run('sh -c "/home/registry/fpm build"') - # execute_response = container.exec_run('sh -c "timeout 15s fpm run"',demux=True) - if build_response.output[0] == None: - if '' in build_response.output[1].decode(): - # package build failed - return False - else: - # package build success - return True - + container.put_archive(os.path.dirname(f"/home/registry/{packagename}.tar.gz"), data) + build_response = container.exec_run(f'sh -c "cd /home/registry/{packagename} && /home/registry/fpm build"') + container.exec_run(f'sh -c "cd /home/registry/ && rm -rf {packagename}"') + if b'' in build_response.output: + # Package build failed + # print("build failed") + return False + if b'[100%] Project compiled successfully.' in build_response.output: + # Package build success + # print("build success") + # print(build_response.output) + return True diff --git a/registry/package.json b/registry/package.json index cd529ce9..abd851fd 100644 --- a/registry/package.json +++ b/registry/package.json @@ -3,27 +3,27 @@ "version": "0.1.0", "private": true, "dependencies": { - "@reduxjs/toolkit": "^1.9.2", + "@reduxjs/toolkit": "^1.9.5", "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "axios": "^1.2.2", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", + "axios": "^1.4.0", "bootstrap": "^5.2.3", - "mdb-react-ui-kit": "^5.1.0", + "mdb-react-ui-kit": "^6.0.0", "mdbreact": "^5.2.0", "react": "^18.2.0", - "react-bootstrap": "^2.7.0", + "react-bootstrap": "^2.7.4", "react-cookie": "^4.1.1", "react-dom": "^18.2.0", - "react-icons": "^4.7.1", + "react-icons": "^4.8.0", "react-redux": "^8.0.5", - "react-router-dom": "^6.7.0", + "react-router-dom": "^6.11.2", "react-scripts": "5.0.1", - "react-toastify": "^9.1.2", + "react-toastify": "^9.1.3", "redux": "^4.2.1", "redux-persist": "^6.0.0", - "styled-components": "^5.3.6", - "web-vitals": "^2.1.4" + "styled-components": "^5.3.10", + "web-vitals": "^3.3.1" }, "scripts": { "start": "react-scripts start", diff --git a/registry/src/App.js b/registry/src/App.js index 059c6f8a..df56cad8 100644 --- a/registry/src/App.js +++ b/registry/src/App.js @@ -12,8 +12,10 @@ import NoPage from "./pages/404"; import UserPage from "./pages/user"; import PackagePage from "./pages/package"; import NamespaceForm from "./pages/createNamespace"; +import VerifyEmail from "./pages/verifyEmail"; import NamespacePage from "./pages/namespace"; import AdminSection from "./pages/admin"; +import Archives from "./pages/archives"; import ForgotPassword from "./pages/forgotpassword"; import ResetPassword from "./pages/resetpassword"; import { BrowserRouter, Routes, Route } from "react-router-dom"; @@ -26,12 +28,17 @@ function App() { } /> + } /> } /> } /> } /> + } + /> } /> } /> } /> diff --git a/registry/src/pages/404.js b/registry/src/pages/404.js index 9c539920..51b37292 100644 --- a/registry/src/pages/404.js +++ b/registry/src/pages/404.js @@ -1,15 +1,18 @@ import React from "react"; -import './404.css'; +import "./404.css"; const NoPage = () => { - return ( -
-

404

-

Page Not Found

-

The Page you are looking for doesn't exist or an other error occured. Go to Home Page.

-
+
+

+

404

+

Page Not Found

+

+ The Page you are looking for doesn't exist or an other error occured. Go + to Home Page. +

+
); }; -export default NoPage; \ No newline at end of file +export default NoPage; diff --git a/registry/src/pages/Navbar.js b/registry/src/pages/Navbar.js index 2b1a4090..a56b1b7b 100644 --- a/registry/src/pages/Navbar.js +++ b/registry/src/pages/Navbar.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useNavigate, useLocation } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; import Image from "react-bootstrap/Image"; @@ -8,6 +8,7 @@ import Navbar from "react-bootstrap/Navbar"; import NavDropdown from "react-bootstrap/NavDropdown"; import { logout } from "../store/actions/authActions"; import { searchPackage, setQuery } from "../store/actions/searchActions"; +import { adminAuth } from "../store/actions/adminActions"; const NavbarComponent = () => { const dispatch = useDispatch(); @@ -19,6 +20,10 @@ const NavbarComponent = () => { const username = useSelector((state) => state.auth.username); const uuid = useSelector((state) => state.auth.uuid); + useEffect(() => { + dispatch(adminAuth(uuid)); + }, [isAuthenticated, uuid]); + const signOut = () => { dispatch(logout(uuid)); }; @@ -37,6 +42,7 @@ const NavbarComponent = () => { fluid width={60} height={60} + alt="Fortran-logo" /> @@ -49,6 +55,9 @@ const NavbarComponent = () => { )} @@ -127,8 +139,8 @@ const SearchBar = () => { } }; - const handleKeyDown = event => { - if (event.key === 'Enter') { + const handleKeyDown = (event) => { + if (event.key === "Enter") { search(); } }; diff --git a/registry/src/pages/account.js b/registry/src/pages/account.js index e3a41886..8d7a27df 100644 --- a/registry/src/pages/account.js +++ b/registry/src/pages/account.js @@ -5,9 +5,11 @@ import { reset, getUserAccount, resetMessages, + change, } from "../store/actions/accountActions"; import Row from "react-bootstrap/Row"; +import Modal from "react-bootstrap/Modal"; import Col from "react-bootstrap/Col"; import Image from "react-bootstrap/Image"; import Table from "react-bootstrap/Table"; @@ -23,17 +25,24 @@ import "mdbreact/dist/css/mdb.css"; const Account = () => { const email = useSelector((state) => state.account.email); const error = useSelector((state) => state.account.error); + const message = useSelector((state) => state.account.message); const successMsg = useSelector( (state) => state.account.resetPasswordSuccessMsg ); const [oldPassword, setoldPassword] = useState(""); const [newPassword, setnewPassword] = useState(""); + const [newEmail, setNewEmail] = useState(""); const [fromValidationErrors, setFormValidationError] = useState({}); const [show, setShow] = useState(false); const dateJoined = useSelector((state) => state.account.dateJoined); const username = useSelector((state) => state.auth.username); const uuid = useSelector((state) => state.auth.uuid); const isLoading = useSelector((state) => state.account.isLoading); + const isLoadingEmail = useSelector((state) => state.account.isLoadingEmail); + const messageEmail = useSelector((state) => state.account.message); + const isLoadingPassword = useSelector( + (state) => state.account.isLoadingPassword + ); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -43,10 +52,6 @@ const Account = () => { } else { dispatch(getUserAccount(uuid)); } - - if (error !== null || successMsg !== null) { - resetMessages(); - } }); const validateForm = () => { @@ -63,16 +68,66 @@ const Account = () => { return Object.keys(errors).length === 0; }; + const validateFormEmail = () => { + let errors = {}; + setFormValidationError(errors); + if (!newEmail) { + errors.email = "New Email is required"; + } + setFormValidationError(errors); + return Object.keys(errors).length === 0; + }; + const handleSubmit = async (e) => { e.preventDefault(); if (validateForm()) { dispatch(resetMessages()); dispatch(reset(oldPassword, newPassword, uuid)); + setnewPassword(""); + setoldPassword(""); + } + setShow(true); + }; + + const handleSubmitEmail = async (e) => { + e.preventDefault(); + + if (validateFormEmail()) { + dispatch(change(newEmail, uuid)); + setNewEmail(""); } setShow(true); }; + const clearForm = () => { + setFormValidationError({}); + setNewEmail(""); + setnewPassword(""); + setoldPassword(""); + dispatch(resetMessages()); + }; + + const handleCloseModal = () => { + clearForm(); + setShowModal(false); + }; + + const handleCloseEmailModal = () => { + clearForm(); + setshowemailModal(false); + }; + const handleOpenModal = () => { + setShowModal(true); + }; + + const handleOpenEmailModal = () => { + setshowemailModal(true); + }; + + const [showModal, setShowModal] = useState(false); + const [showemailModal, setshowemailModal] = useState(false); + return isLoading ? (
@@ -96,11 +151,19 @@ const Account = () => { alt={`Avatar for ${username} from gravatar.com`} title={`Avatar for ${username} from gravatar.com`} /> +
+
+ + @{username} + We use gravatar.com to generate your profile picture based on your primary email address — - {email} . + {email} .
+
+ + @@ -125,55 +188,95 @@ const Account = () => { {email} - -
Change Password
- -
- - - Old Password - - - setoldPassword(e.target.value)} - /> - - - - - - New Password - - - setnewPassword(e.target.value)} - /> - - - - {fromValidationErrors.password && ( -

{fromValidationErrors.password}

- )} -

{successMsg}

-

{error}

- - - {show && error} - - -
+ + + Reset Password + + +
+ + + Old Password + + + setoldPassword(e.target.value)} + /> + + + + + New Password + + + setnewPassword(e.target.value)} + /> + + + + {fromValidationErrors.password && ( +

{fromValidationErrors.password}

+ )} +

{error}

+

{messageEmail}

+
+
+ + + + +
+ + + Change Email + + +
+ + + New Email + + + setNewEmail(e.target.value)} + /> + + + + {fromValidationErrors.email && ( +

{fromValidationErrors.email}

+ )} +

{message}

+
+
+ + + + +
); }; diff --git a/registry/src/pages/admin.js b/registry/src/pages/admin.js index f8991df0..b401339a 100644 --- a/registry/src/pages/admin.js +++ b/registry/src/pages/admin.js @@ -1,17 +1,8 @@ import React, { useEffect, useState } from "react"; import { Container } from "react-bootstrap"; +import Button from "react-bootstrap/Button"; import { useDispatch, useSelector } from "react-redux"; -import { useNavigate } from "react-router-dom"; import { - adminAuth, - deleteUser, - deleteNamespace, - deletePackage, - deleteRelease, - deprecatePackage, -} from "../store/actions/adminActions"; -import { - MDBBtn, MDBModal, MDBModalDialog, MDBModalContent, @@ -21,536 +12,381 @@ import { MDBModalFooter, MDBIcon, } from "mdb-react-ui-kit"; - -const isEmpty = (...values) => { - return values.some((value) => value === ""); -}; +import { + adminAuth, + deleteUser, + deleteNamespace, + deletePackage, + deleteRelease, + deprecatePackage, +} from "../store/actions/adminActions"; +import NoPage from "./404"; const AdminSection = () => { - const [deprecateModal, setdeprecateModal] = useState(false); const uuid = useSelector((state) => state.auth.uuid); const dispatch = useDispatch(); - const navigate = useNavigate(); const message = useSelector((state) => state.admin.message); const statuscode = useSelector((state) => state.admin.statuscode); + const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); const isAdmin = useSelector((state) => state.admin.isAdmin); - const toggleShowDeprecateModal = () => { - if (!isEmpty(deprecatePackageNamespaceName, deprecatepackageName)) { - setdeprecateModal(!deprecateModal); - } else { - toggleShowemptyModal(); + + useEffect(() => { + dispatch(adminAuth(uuid)); + }, [isAuthenticated, uuid]); + + useEffect(() => { + if (statuscode != null) { + openModal(statuscode + " Status Code", message, null); } - }; - const [deprecatepackageName, setdeprecatepackageName] = useState(""); + }, [statuscode, message]); - const [deprecatePackageNamespaceName, setdeprecatePackageNamespaceName] = - useState(""); + const [formData, setFormData] = useState({ + namespaceName: "", + packageName: "", + releaseName: "", + userName: "", + newPassword: "", + }); - const [emptyModal, setemptyModal] = useState(false); + const [modalData, setModalData] = useState({ + showModal: false, + modalTitle: "", + modalMessage: "", + modalAction: null, + }); - const [messageModal, setmessageModal] = useState(false); - const toggleShowmessageModal = () => setmessageModal(!messageModal); - const toggleShowemptyModal = () => setemptyModal(!emptyModal); + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ ...formData, [name]: value }); + }; - const [deleteNamespaceModal, setdeleteNamespaceModal] = useState(false); - const toggleShowDeleteNamespaceModal = () => { - if (!isEmpty(deletenamespaceName)) { - setdeleteNamespaceModal(!deleteNamespaceModal); - } else { - toggleShowemptyModal(); - } + const openModal = (title, message, action) => { + setModalData({ + showModal: true, + modalTitle: title, + modalMessage: message, + modalAction: action, + }); }; - const [deletenamespaceName, setdeletenamespaceName] = useState(""); + const toggleShowModal = () => { + setModalData({ ...modalData, showModal: !modalData.showModal }); + }; - const [deletePackageModal, setdeletePackageModal] = useState(false); - const toggleShowDeletePackageModal = () => { - if (!isEmpty(deletepackagenamespaceName, deletepackageName)) { - setdeletePackageModal(!deletePackageModal); - } else { - toggleShowemptyModal(); + const isEmpty = (...values) => { + if (values.some((value) => value === "")) { + openModal("Empty Fields", "Please fill out all fields.", () => { + toggleShowModal(); + }); + return true; } + return false; }; - const [deletepackageName, setdeletepackageName] = useState(""); - const [deletepackagenamespaceName, setdeletepackagenamespaceName] = - useState(""); - const [deleteUserModal, setdeleteUserModal] = useState(false); - const toggleShowDeleteUserModal = () => { - if (!isEmpty(deleteuserName)) { - setdeleteUserModal(!deleteUserModal); - } else { - toggleShowemptyModal(); + const handleAction = () => { + if (modalData.modalAction) { + modalData.modalAction(); } + toggleShowModal(); }; - const [deleteuserName, setdeleteuserName] = useState(""); + const handleDeletePackage = () => { + if (!isEmpty(formData.namespaceName, formData.packageName)) { + openModal( + "Delete Package", + `You will not be able to recover ${formData.namespaceName}/${formData.packageName} package after you delete it.`, + () => { + dispatch( + deletePackage(formData.namespaceName, formData.packageName, uuid) + ); + } + ); + } + + // clear the form data + setFormData({ + namespaceName: "", + packageName: "", + }); + }; - const [deleteReleaseModal, setdeleteReleaseModal] = useState(false); - const toggleShowDeleteReleaseModal = () => { + const handleDeleteRelease = () => { if ( !isEmpty( - deletereleasenamespaceName, - deletereleasepackageName, - deletereleaseName + formData.namespaceName, + formData.packageName, + formData.releaseName, + uuid ) ) { - setdeleteReleaseModal(!deleteReleaseModal); - } else { - toggleShowemptyModal(); + openModal( + "Delete Release", + `You will not be able to recover ${formData.namespaceName}/${formData.packageName}/${formData.releaseName} release after you delete it.`, + () => { + dispatch( + deleteRelease( + formData.namespaceName, + formData.packageName, + formData.releaseName, + uuid + ) + ); + } + ); } - }; - const [deletereleasepackageName, setdeletereleasepackageName] = useState(""); - const [deletereleasenamespaceName, setdeletereleasenamespaceName] = - useState(""); - const [deletereleaseName, setdeletereleaseName] = useState(""); - // const [changePasswordModal, setchangePasswordModal] = useState(false); - // const toggleShowChangePasswordModal = () => { - // if (!isEmpty(userName, newPassword)) { - // setchangePasswordModal(!changePasswordModal); - // } else { - // toggleShowemptyModal(); - // } - // }; - - // const [userName, setUserName] = useState(""); - // const [newPassword, setNewPassword] = useState(""); + // clear the form data + setFormData({ + namespaceName: "", + packageName: "", + releaseName: "", + }); + }; - useEffect(() => { - dispatch(adminAuth()); - if (!isAdmin) { - navigate("/404"); + const handleDeleteUser = () => { + if (!isEmpty(formData.userName, uuid)) { + openModal( + "Delete User", + `You will not be able to recover ${formData.userName} user after you delete it.`, + () => { + dispatch(deleteUser(formData.userName, uuid)); + } + ); } - }, [isAdmin]); - useEffect(() => { - toggleShowmessageModal(); - }, [message]); - - const handleDeprecatePackage = () => { - dispatch( - deprecatePackage( - deprecatePackageNamespaceName, - deprecatepackageName, - uuid - ) - ); - setdeprecatePackageNamespaceName(""); - setdeprecatepackageName(""); + // clear the form data + setFormData({ + userName: "", + }); }; const handleDeleteNamespace = () => { - dispatch(deleteNamespace(deletenamespaceName, uuid)); - setdeletenamespaceName(""); - }; + if (!isEmpty(formData.namespaceName, uuid)) { + openModal( + "Delete Namespace", + `You will not be able to recover ${formData.namespaceName} namespace after you delete it.`, + () => { + dispatch(deleteNamespace(formData.namespaceName, uuid)); + } + ); + } - const handleDeletePackage = () => { - dispatch( - deletePackage(deletepackagenamespaceName, deletepackageName, uuid) - ); - setdeletepackagenamespaceName(""); - setdeletepackageName(""); + // clear the form data + setFormData({ + namespaceName: "", + }); }; - const handleDeleteUser = () => { - dispatch(deleteUser(deleteuserName, uuid)); - setdeleteuserName(""); - }; + const handleDeprecatePackage = () => { + if (!isEmpty(formData.namespaceName, formData.packageName, uuid)) { + openModal( + "Delete Package", + `You will not be able to recover ${formData.namespaceName}/${formData.packageName} package after you delete it.`, + () => { + dispatch( + deprecatePackage(formData.namespaceName, formData.packageName, uuid) + ); + } + ); + } - const handleDeleteRelease = () => { - dispatch( - deleteRelease( - deletereleasenamespaceName, - deletereleasepackageName, - deletereleaseName, - uuid - ) - ); - setdeletereleasenamespaceName(""); - setdeletereleasepackageName(""); - setdeletereleaseName(""); + // clear the form data + setFormData({ + namespaceName: "", + packageName: "", + }); }; - const handleChangePassword = () => { - // dispatch(adminAuth(userName, newPassword)); - // setUserName(""); - // setNewPassword(""); - }; + // const changePassword = () => { // TODO: Enable this feature + // console.log("Changing password for user:", formData.userName); + // // Add the logic to change the password + // // clear the form data + // setFormData({ + // userName: "", + // newPassword: "", + // }); + // }; - return ( - - - - - - Empty Inputs - - - - You must fill all the - fields. - - - - Close - - - - - - - - - - {statuscode} status - - - - - {message} - - - - Close - - - - - + return isAdmin? ( + +

Admin Settings

-

Deprecate package release

+

Delete package

setdeprecatePackageNamespaceName(e.target.value)} + name="namespaceName" + value={formData.namespaceName} + onChange={handleInputChange} style={{ width: 300 }} /> setdeprecatepackageName(e.target.value)} + name="packageName" + value={formData.packageName} + onChange={handleInputChange} style={{ width: 300 }} />

- - Deprecate Package - - - - - - Deprecate Package - - - - You will not be able - to recover {deprecatePackageNamespaceName}/ - {deprecatepackageName} package after you deprecate it. - - - - Close - - - Deprecate Package - - - - - +
-

Delete namespace

+

+

Delete package version

setdeletenamespaceName(e.target.value)} + name="namespaceName" + value={formData.namespaceName} + onChange={handleInputChange} + style={{ width: 300 }} + /> + +

- - Delete Namespace - - - - - - Delete Namespace - - - - You will not be able - to recover {deletenamespaceName} Namespace after you delete it. - - - - Close - - - Delete Namespace - - - - - +
-

Delete package

+

+

Deprecate package

setdeletepackagenamespaceName(e.target.value)} + name="namespaceName" + value={formData.namespaceName} + onChange={handleInputChange} style={{ width: 300 }} /> setdeletepackageName(e.target.value)} + name="packageName" + value={formData.packageName} + onChange={handleInputChange} style={{ width: 300 }} />

- +
-

Delete user

+

+

Delete Namespace

setdeleteuserName(e.target.value)} + placeholder="Namespace Name" + name="namespaceName" + value={formData.namespaceName} + onChange={handleInputChange} style={{ width: 300 }} />

- - Delete User - - - - - - Delete User - - - - You will not be able - to recover {deleteuserName} user after you delete it. - - - - Close - - Delete User - - - - +
-

Delete release

+

+

Delete user

setdeletereleasepackageName(e.target.value)} - style={{ width: 300 }} - /> - setdeletereleasenamespaceName(e.target.value)} - style={{ width: 300 }} - /> - setdeletereleaseName(e.target.value)} + placeholder="User Name" + name="userName" + value={formData.userName} + onChange={handleInputChange} style={{ width: 300 }} />

- - - Delete Release - - - - - - Delete Release - - - - You will not be able - to recover {deletereleasenamespaceName}/ - {deletereleasepackageName}/{deletereleaseName} release after you - delete it. - - - - Close - - Delete Release - - - - +
- {/*
+ {/*
// TODO: Enable this feature +

Change password

setUserName(e.target.value)} + name="userName" + value={formData.userName} + onChange={handleInputChange} style={{ width: 300 }} /> -

-

setNewPassword(e.target.value)} + name="newPassword" + value={formData.newPassword} + onChange={handleInputChange} style={{ width: 300 }} />

- + openModal( + "Change Password", + `You will not be able to recover ${formData.userName} user's password after you change password.`, + changePassword + ) + } style={{ fontSize: 16 }} > Change Password - - - - - - Change Password - - - - You will not be able - to recover {userName} user's password after you change password. - - - - Close - - Change Password - - - - +
*/} + + + + + {modalData.modalTitle} + + + + {" "} + {modalData.modalMessage} + + + + + + + + - ); + ):(); }; export default AdminSection; diff --git a/registry/src/pages/archives.js b/registry/src/pages/archives.js new file mode 100644 index 00000000..f21ae0e7 --- /dev/null +++ b/registry/src/pages/archives.js @@ -0,0 +1,74 @@ +import React, { useEffect } from "react"; +import Spinner from "react-bootstrap/Spinner"; +import { useDispatch, useSelector } from "react-redux"; +import Container from "react-bootstrap/Container"; +import { fetchArchiveData } from "../store/actions/archivesActions"; + +const Archives = () => { + const archives = useSelector((state) => state.archives.archives); + const dispatch = useDispatch(); + const isLoading = useSelector((state) => state.archives.isLoading); + + useEffect(() => { + dispatch(fetchArchiveData()); + }, []); + + const containerStyle = { + textAlign: "left", + fontFamily: "Arial, sans-serif", + fontSize: "18px", + lineHeight: "1.5", + paddingLeft: "30px", + paddingTop: "20px", + paddinBottom: "20px", + color: "#333", + paddingRight: "30px", + }; + + const h2Style = { + textAlign: "left", + alignContent: "left", + fontSize: "26px", + }; + + return !isLoading ? ( +
+

Archives

+

+

fpm - registry Archives

+
+ Here, you will find a collection of registry archives created at weekly + intervals, showcasing the weekly snapshots of our registry. These + archives serve as a valuable resource for tracking the evolution of our + registry over time. +
+

+
+ Each week, An archive is automatically compiled and stores a snapshot of our registry, + capturing the state of namespaces, packages, and tarballs of the fpm - registry. These + archives provide a comprehensive record of the changes and updates made + to our registry. +
+

+ +
+ ) : ( + + + Loading... + + + ); +}; + +export default Archives; diff --git a/registry/src/pages/createNamespace.js b/registry/src/pages/createNamespace.js index 5ecf8dd3..f08afae2 100644 --- a/registry/src/pages/createNamespace.js +++ b/registry/src/pages/createNamespace.js @@ -57,6 +57,7 @@ const NamespaceForm = () => { const handleSubmit = (event) => { event.preventDefault(); dispatch(createNamespace(data)); + setData({ ...data, namespace: "", namespace_description: "" }); }; return !isLoading ? ( diff --git a/registry/src/pages/dashboard.js b/registry/src/pages/dashboard.js index 92b688cb..27263d6a 100644 --- a/registry/src/pages/dashboard.js +++ b/registry/src/pages/dashboard.js @@ -7,6 +7,7 @@ import Col from "react-bootstrap/Col"; import Row from "react-bootstrap/Row"; import Spinner from "react-bootstrap/Spinner"; import Container from "react-bootstrap/Container"; +import { Link } from "react-router-dom"; import AddMaintainerFormDialog from "./addMaintainerDialogForm"; import RemoveMaintainerFormDialog from "./removeMaintainerDialogForm"; import GenerateNamespaceTokenDialogForm from "./generateNamespaceTokenDialogForm"; @@ -28,7 +29,7 @@ const Dashboard = () => { const username = useSelector((state) => state.auth.username); const packages = useSelector((state) => state.dashboard.packages); const namespaces = useSelector((state) => state.dashboard.namespaces); - const isLoading = useSelector((state) => state.dashboard.isLoading); + const isLoading = useSelector((state) => state.dashboard.isLoading || state.auth.isLoading); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -182,7 +183,7 @@ const Dashboard = () => { function Namespaces() { return namespaces.length === 0 ? (
- You are not a maintainer of any namespace yet. + You are not a maintainer of any namespace yet. Add a namespace here
) : ( diff --git a/registry/src/pages/help.js b/registry/src/pages/help.js index b36fddbe..35fcc541 100644 --- a/registry/src/pages/help.js +++ b/registry/src/pages/help.js @@ -1,7 +1,77 @@ import React from "react"; const Help = () => { - return

help

; + const containerStyle = { + textAlign: "left", + fontFamily: "Arial, sans-serif", + fontSize: "18px", + lineHeight: "1.5", + paddingLeft: "30px", + paddingTop: "20px", + paddinBottom: "20px", + color: "#333", + paddingRight: "30px", + }; + + const h2Style = { + textAlign: "left", + alignContent: "left", + fontSize: "26px", + }; + + const codeStyle = { + backgroundColor: "#f5f5f5", + padding: "5px", + fontSize: "18px", + fontFamily: "Courier, monospace", + }; + + return ( +
+

Help

+

+

Account Registration

+
First of all, you will need to register a user account:
+
+ For uploading your package to the registry, you will have to step by + step follow the following points: +
+
+ Register yourself as a user. You will require a unique username, email & + password to set up your account. +
+

+

Namespace Creation

+
+ For uploading a package from fpm, you will have to first create a + namespace. A namespace represents a collection of packages. Each package + is published under a namespace in order to avoid collision of same + package names. Namespace names will be unique always. Now, that you will + have created a namespace with a unique name and a nice description. You + can go to dashboard by from the dropdown options in the Navigation bar + on top.

In the dashboard, you can see the namespace that has + been created by you. You can now generate a token for this namespace. +
+

+

Token Generation and Package Upload

+
+ This token will be valid for 1 week , but you can always generate a new + token. +
+
Use this token to upload packages from the fpm using the CLI:
+

+ fpm publish --token token-here +



+
+ After completing the above steps, you will receive a response in the fpm + command line interface whether your upload was successful or not. If + your upload was successful, you can now again go to the registry + frontend and check the dashboard. It should display the package uploaded + by you. You can now Add/Remove maintainers to your package. Mantainers + have the rights to operate on the same package. +
+
+ ); }; export default Help; diff --git a/registry/src/pages/home.js b/registry/src/pages/home.js index 11c99ab0..d3f84b5d 100644 --- a/registry/src/pages/home.js +++ b/registry/src/pages/home.js @@ -10,7 +10,7 @@ const Home = () => { return ( diff --git a/registry/src/pages/login.js b/registry/src/pages/login.js index 26e1a159..18501808 100644 --- a/registry/src/pages/login.js +++ b/registry/src/pages/login.js @@ -14,6 +14,7 @@ const Login = () => { const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); const errorMessage = useSelector((state) => state.auth.error); + const isLoading = useSelector((state) => state.auth.isLoading); useEffect(() => { if (isAuthenticated) { @@ -57,7 +58,7 @@ const Login = () => { setUser_identifier(e.target.value)} /> @@ -75,7 +76,7 @@ const Login = () => {

{fromValidationErrors.password}

)} {errorMessage != null ?

{errorMessage}

: null} - +

Don't have an account? Sign up

diff --git a/registry/src/pages/package.js b/registry/src/pages/package.js index 491b9ecf..7fc720fe 100644 --- a/registry/src/pages/package.js +++ b/registry/src/pages/package.js @@ -229,16 +229,15 @@ const ViewPackageMaintainersButton = ({ const sideBar = (data) => { return ( - {/* TODO: update Package API for url,website,maintainers */}

Install

fpm install {data.namespace}/{data.name}

Repository

- {/* { data.url } */} + { data.repository }

Homepage

- {/* { data.website } */} + { data.homepage }

License

{data.license} diff --git a/registry/src/pages/register.js b/registry/src/pages/register.js index 82ce99ac..86d92d42 100644 --- a/registry/src/pages/register.js +++ b/registry/src/pages/register.js @@ -14,6 +14,8 @@ const Register = () => { const dispatch = useDispatch(); const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); + const isLoading = useSelector((state) => state.auth.isLoading); + const message = useSelector((state) => state.auth.message); const errorMessage = useSelector((state) => state.auth.error); const handleSubmit = async (e) => { @@ -90,7 +92,8 @@ const Register = () => {

{fromValidationErrors.password}

)} {errorMessage != null ?

{errorMessage}

: null} - + {message != null ?

{message}

: null} +

Already have an account? Log in

diff --git a/registry/src/pages/verifyEmail.js b/registry/src/pages/verifyEmail.js new file mode 100644 index 00000000..a5153860 --- /dev/null +++ b/registry/src/pages/verifyEmail.js @@ -0,0 +1,39 @@ +import React, { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useDispatch, useSelector } from "react-redux"; +import { verify } from "../store/actions/verifyEmailActions"; +import { Link } from "react-router-dom"; +import Container from "react-bootstrap/Container"; + +const VerifyEmail = () => { + const { uuid } = useParams(); + const dispatch = useDispatch(); + const message = useSelector((state) => state.verifyEmail.message); + const statuscode = useSelector((state) => state.verifyEmail.statuscode); + const isLoading = useSelector((state) => state.verifyEmail.isLoading); + + const handleSubmit = async (e) => { + e.preventDefault(); + dispatch(verify(uuid)); + }; + + return ( + +
+

Welcome to fpm Registry!

+ + {message && + (statuscode !== 200 ? ( +

{message}

+ ) : ( +

{message}

+ ))} +

+ Already have an account? Login +

+
+
+ ); +}; + +export default VerifyEmail; diff --git a/registry/src/store/actions/accountActions.js b/registry/src/store/actions/accountActions.js index bcd70a3e..c961ae82 100644 --- a/registry/src/store/actions/accountActions.js +++ b/registry/src/store/actions/accountActions.js @@ -2,11 +2,15 @@ import axios from "axios"; export const RESET_SUCCESS = "RESET_SUCCESS"; export const RESET_FAILURE = "RESET_FAILURE"; +export const CHANGE_EMAIL = "CHANGE_EMAIL"; +export const CHANGE_EMAIL_FAILURE = "CHANGE_EMAIL_FAILURE"; +export const CHANGE_EMAIL_SUCCESS = "CHANGE_EMAIL_SUCCESS"; export const GET_USER_ACCOUNT = "GET_USER_ACCOUNT"; export const GET_USER_ACCOUNT_FAILURE = "GET_USER_ACCOUNT_FAILURE"; // export const DELETE_ACCOUNT = "DELETE_ACCOUNT"; // export const DELETE_ACCOUNT_ERROR = "DELETE_ACCOUNT_ERROR"; export const RESET_PASSWORD = "RESET_PASSWORD"; +export const RESET_PASSWORD_SUCCESS = "RESET_PASSWORD_SUCCESS"; export const RESET_PASSWORD_ERROR = "RESET_PASSWORD_ERROR"; export const RESET_MESSAGES = "RESET_MESSAGES"; @@ -41,6 +45,7 @@ export const getUserAccount = (uuid) => async (dispatch) => { export const reset = (oldpassword, password, uuid) => async (dispatch) => { let formData = new FormData(); + dispatch({ type: RESET_PASSWORD }); formData.append("oldpassword", oldpassword); formData.append("password", password); formData.append("uuid", uuid); @@ -55,7 +60,7 @@ export const reset = (oldpassword, password, uuid) => async (dispatch) => { }, }); console.log(result); - dispatch({ type: RESET_PASSWORD, payload: result.data.message }); + dispatch({ type: RESET_PASSWORD_SUCCESS, payload: result.data.message }); } catch (error) { console.log(error.response.data.message); dispatch({ @@ -65,6 +70,32 @@ export const reset = (oldpassword, password, uuid) => async (dispatch) => { } }; +export const change = (newemail, uuid) => async (dispatch) => { + let formData = new FormData(); + formData.append("newemail", newemail); + formData.append("uuid", uuid); + dispatch({ type: CHANGE_EMAIL }); + + try { + const result = await axios({ + method: "post", + url: `${process.env.REACT_APP_REGISTRY_API_URL}/auth/change-email`, + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + console.log(result.data.message); + dispatch({ type: CHANGE_EMAIL_SUCCESS, payload: result.data.message }); + } catch (error) { + console.log(error.response.data.message); + dispatch({ + type: CHANGE_EMAIL_FAILURE, + payload: error.response.data.message, + }); + } +}; + export const resetMessages = () => (dispatch) => { dispatch({ type: RESET_MESSAGES, diff --git a/registry/src/store/actions/adminActions.js b/registry/src/store/actions/adminActions.js index 55b69743..f95843f6 100644 --- a/registry/src/store/actions/adminActions.js +++ b/registry/src/store/actions/adminActions.js @@ -99,7 +99,6 @@ export const deleteUser = (username, uuid) => async (dispatch) => { } } catch (error) { //on failure - console.log(error); dispatch({ type: DELETE_USER_ERROR, payload: { @@ -119,7 +118,7 @@ export const deleteNamespace = (namespace, uuid) => async (dispatch) => { try { let result = await axios({ method: "post", - url: `${process.env.REACT_APP_REGISTRY_API_URL}/packages/${namespace}/delete`, + url: `${process.env.REACT_APP_REGISTRY_API_URL}/namespace/${namespace}/delete`, data: formData, headers: { "Content-Type": "multipart/form-data", @@ -166,7 +165,7 @@ export const deletePackage = try { let result = await axios({ method: "post", - url: `${process.env.REACT_APP_REGISTRY_API_URL}/packages/${namespacename}/${packagename}/delete"`, + url: `${process.env.REACT_APP_REGISTRY_API_URL}/packages/${namespacename}/${packagename}/delete`, data: formData, headers: { "Content-Type": "multipart/form-data", diff --git a/registry/src/store/actions/archivesActions.js b/registry/src/store/actions/archivesActions.js new file mode 100644 index 00000000..96d72db4 --- /dev/null +++ b/registry/src/store/actions/archivesActions.js @@ -0,0 +1,27 @@ +export const FETCH_ARCHIVES_DATA = "FETCH_ARCHIVES_DATA"; +export const FETCH_ARCHIVES_DATA_SUCCESS = "FETCH_ARCHIVES_DATA_SUCCESS"; +export const FETCH_ARCHIVES_DATA_ERROR = "FETCH_ARCHIVES_DATA_ERROR"; + +export const fetchArchiveData = () => { + return (dispatch) => { + dispatch({ type: FETCH_ARCHIVES_DATA }); + const url = `${process.env.REACT_APP_REGISTRY_API_URL}/registry/archives`; + fetch(url) + .then((res) => { + if (res.ok) { + return res.json(); + } else { + dispatch({ type: FETCH_ARCHIVES_DATA_ERROR}); + } + }) + .then((data) => { + dispatch({ + type: FETCH_ARCHIVES_DATA_SUCCESS, + payload: { + archives: data.archives, + message: data.message, + }, + }); + }); + }; +}; \ No newline at end of file diff --git a/registry/src/store/actions/authActions.js b/registry/src/store/actions/authActions.js index 91509042..74c1c1ae 100644 --- a/registry/src/store/actions/authActions.js +++ b/registry/src/store/actions/authActions.js @@ -1,12 +1,16 @@ import axios from "axios"; export const RESET_ERROR_MESSAGE = "RESET_ERROR_MESSAGE"; +export const LOGIN_REQUEST = "LOGIN_REQUEST"; export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; export const LOGIN_FAILURE = "LOGIN_FAILURE"; export const login = (user_identifier, password) => async (dispatch) => { // Make an api call to login + dispatch({ + type: LOGIN_REQUEST, + }); let formData = new FormData(); formData.append("user_identifier", user_identifier); @@ -51,8 +55,12 @@ export const login = (user_identifier, password) => async (dispatch) => { export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS"; export const LOGOUT_FAILURE = "LOGOUT_FAILURE"; +export const LOGOUT_REQUEST = "LOGOUT_REQUEST"; export const logout = (uuid) => async (dispatch) => { + dispatch({ + type: LOGOUT_REQUEST, + }); let formData = new FormData(); formData.append("uuid", uuid); @@ -91,8 +99,12 @@ export const logout = (uuid) => async (dispatch) => { export const SIGNUP_SUCCESS = "SIGNUP_SUCCESS"; export const SIGNUP_FAILURE = "SIGNUP_FAILURE"; +export const SIGNUP_REQUEST = "SIGNUP_REQUEST"; export const signup = (username, email, password) => async (dispatch) => { + dispatch({ + type: SIGNUP_REQUEST, + }); let formData = new FormData(); formData.append("username", username); @@ -114,8 +126,7 @@ export const signup = (username, email, password) => async (dispatch) => { dispatch({ type: SIGNUP_SUCCESS, payload: { - uuid: result.data.uuid, - username: result.data.username, + message: result.data.message, }, }); } else { diff --git a/registry/src/store/actions/verifyEmailActions.js b/registry/src/store/actions/verifyEmailActions.js new file mode 100644 index 00000000..24a576de --- /dev/null +++ b/registry/src/store/actions/verifyEmailActions.js @@ -0,0 +1,55 @@ +import axios from "axios"; + +export const VERIFY_REQUEST = "VERIFY_REQUEST"; + +export const VERIFY_REQUEST_SUCCESS = "VERIFY_REQUEST_SUCCESS"; +export const VERIFY_REQUEST_FAILURE = "VERIFY_REQUEST_FAILURE"; + +export const verify = (uuid) => async (dispatch) => { + // Make an api call to request to verify email + dispatch({ + type: VERIFY_REQUEST, + }); + let formData = new FormData(); + + formData.append("uuid", uuid); + + try { + let result = await axios({ + method: "post", + url: `${process.env.REACT_APP_REGISTRY_API_URL}/auth/verify-email`, + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + + if (result.data.code === 200) { + dispatch({ + type: VERIFY_REQUEST_SUCCESS, + payload: { + statuscode: result.data.code, + message: result.data.message, + }, + }); + } else { + dispatch({ + type: VERIFY_REQUEST_FAILURE, + payload: { + statuscode: result.data.code, + message: result.data.message, + }, + }); + } + } catch (error) { + //on failure + // console.log(error); + dispatch({ + type: VERIFY_REQUEST_FAILURE, + payload: { + statuscode: error.response.data.code, + message: error.response.data.message, + }, + }); + } +}; diff --git a/registry/src/store/reducers/accountReducer.js b/registry/src/store/reducers/accountReducer.js index f9566bdb..4849d54d 100644 --- a/registry/src/store/reducers/accountReducer.js +++ b/registry/src/store/reducers/accountReducer.js @@ -3,6 +3,10 @@ import { RESET_PASSWORD_ERROR, RESET_PASSWORD, RESET_MESSAGES, + CHANGE_EMAIL_SUCCESS, + CHANGE_EMAIL_FAILURE, + CHANGE_EMAIL, + RESET_PASSWORD_SUCCESS, } from "../actions/accountActions"; const initialState = { @@ -12,6 +16,9 @@ const initialState = { email: "", dateJoined: "", isLoading: true, + isLoadingEmail: false, + isLoadingPassword: false, + message: null, resetPasswordSuccessMsg: null, }; @@ -28,11 +35,37 @@ const accountReducer = (state = initialState, action) => { return { ...state, resetPasswordSuccessMsg: action.payload, + isLoadingPassword: true, + }; + case CHANGE_EMAIL_SUCCESS: + return { + ...state, + message: action.payload, + isLoadingEmail: false, + }; + case CHANGE_EMAIL: + return { + ...state, + message: action.payload, + isLoadingEmail: true, + }; + case CHANGE_EMAIL_FAILURE: + return { + ...state, + message: action.payload, + isLoadingEmail: false, + }; + case RESET_PASSWORD_SUCCESS: + return { + ...state, + message: action.payload, + isLoadingPassword: false, }; case RESET_PASSWORD_ERROR: return { ...state, error: action.payload, + isLoadingPassword: false, }; case RESET_MESSAGES: return { diff --git a/registry/src/store/reducers/adminReducer.js b/registry/src/store/reducers/adminReducer.js index 54145afb..928d2c5b 100644 --- a/registry/src/store/reducers/adminReducer.js +++ b/registry/src/store/reducers/adminReducer.js @@ -16,7 +16,7 @@ import { const initialState = { error: null, isAdmin: false, - message: "", + message: null, statuscode: null, }; diff --git a/registry/src/store/reducers/archivesReducer.js b/registry/src/store/reducers/archivesReducer.js new file mode 100644 index 00000000..b83e0014 --- /dev/null +++ b/registry/src/store/reducers/archivesReducer.js @@ -0,0 +1,35 @@ +import { + FETCH_ARCHIVES_DATA, + FETCH_ARCHIVES_DATA_SUCCESS, + FETCH_ARCHIVES_DATA_ERROR, +} from "../actions/archivesActions"; + +const initialState = { + archives: [], + isLoading: true, +}; + +const archivesReducer = (state = initialState, action) => { + switch (action.type) { + case FETCH_ARCHIVES_DATA: + return { + ...state, + isLoading: true, + }; + case FETCH_ARCHIVES_DATA_SUCCESS: + return { + archives: action.payload.archives, + message: action.payload.message, + isLoading: false, + }; + case FETCH_ARCHIVES_DATA_ERROR: + return { + ...state, + isLoading: false, + }; + default: + return state; + } +}; + +export default archivesReducer; diff --git a/registry/src/store/reducers/authReducer.js b/registry/src/store/reducers/authReducer.js index e5acb069..ee10e9fe 100644 --- a/registry/src/store/reducers/authReducer.js +++ b/registry/src/store/reducers/authReducer.js @@ -6,6 +6,9 @@ import { LOGOUT_SUCCESS, LOGOUT_FAILURE, RESET_ERROR_MESSAGE, + LOGIN_REQUEST, + SIGNUP_REQUEST, + LOGOUT_REQUEST } from "../actions/authActions"; const initialState = { @@ -13,16 +16,34 @@ const initialState = { uuid: null, error: null, username: null, + isLoading: false, + message: null, }; const authReducer = (state = initialState, action) => { switch (action.type) { + case LOGIN_REQUEST: + return { + ...state, + isLoading: true, + }; + case LOGOUT_REQUEST: + return { + ...state, + isLoading: true, + }; + case SIGNUP_REQUEST: + return { + ...state, + isLoading: true, + }; case LOGIN_SUCCESS: return { ...state, isAuthenticated: true, uuid: action.payload.uuid, username: action.payload.username, + isLoading: false, }; case LOGIN_FAILURE: @@ -30,6 +51,7 @@ const authReducer = (state = initialState, action) => { ...state, isAuthenticated: false, error: action.payload.error, + isLoading: false, }; case LOGOUT_SUCCESS: @@ -39,20 +61,21 @@ const authReducer = (state = initialState, action) => { uuid: null, username: null, error: null, + isLoading: false, }; case LOGOUT_FAILURE: return { ...state, error: action.payload.error, + isLoading: false, }; case SIGNUP_SUCCESS: return { ...state, - isAuthenticated: true, - uuid: action.payload.uuid, - username: action.payload.username, + message: action.payload.message, + isLoading: false, }; case SIGNUP_FAILURE: @@ -60,6 +83,7 @@ const authReducer = (state = initialState, action) => { ...state, isAuthenticated: false, error: action.payload.error, + isLoading: false, }; case RESET_ERROR_MESSAGE: return { diff --git a/registry/src/store/reducers/rootReducer.js b/registry/src/store/reducers/rootReducer.js index b418a875..4d784525 100644 --- a/registry/src/store/reducers/rootReducer.js +++ b/registry/src/store/reducers/rootReducer.js @@ -7,6 +7,7 @@ import searchReducer from "./searchReducer"; import namespaceReducer from "./namespaceReducer"; import resetPasswordReducer from "./resetPasswordReducer"; import createNamespaceReducer from "./createNamespaceReducer"; +import archivesReducer from "./archivesReducer"; import adminReducer from "./adminReducer"; import { combineReducers } from "redux"; import addRemoveMaintainerReducer from "./addRemoveMaintainerReducer"; @@ -14,6 +15,7 @@ import generateNamespaceTokenReducer from "./generateNamespaceTokenReducer"; import generatePackageTokenReducer from "./generatePackageTokenReducer"; import addRemoveNamespaceMaintainerReducer from "./namespaceMaintainersReducer"; import addRemoveNamespaceAdminReducer from "./namespaceAdminReducer"; +import verifyEmailReducer from "./verifyEmailReducer"; import userListReducer from "./userListReducer"; const rootReducer = combineReducers({ @@ -32,7 +34,9 @@ const rootReducer = combineReducers({ createNamespace: createNamespaceReducer, addRemoveNamespaceMaintainer: addRemoveNamespaceMaintainerReducer, addRemoveNamespaceAdmin: addRemoveNamespaceAdminReducer, + verifyEmail: verifyEmailReducer, userList: userListReducer, + archives: archivesReducer, }); export default rootReducer; diff --git a/registry/src/store/reducers/verifyEmailReducer.js b/registry/src/store/reducers/verifyEmailReducer.js new file mode 100644 index 00000000..6203cf1c --- /dev/null +++ b/registry/src/store/reducers/verifyEmailReducer.js @@ -0,0 +1,42 @@ +import { + VERIFY_REQUEST_SUCCESS, + VERIFY_REQUEST_FAILURE, + VERIFY_REQUEST, +} from "../actions/verifyEmailActions"; + +const initialState = { + statuscode: 0, + message: "", + isLoading: false, +}; + +const verifyEmailReducer = (state = initialState, action) => { + switch (action.type) { + case VERIFY_REQUEST_SUCCESS: + return { + ...state, + statuscode: action.payload.statuscode, + message: action.payload.message, + isLoading: false, + }; + + case VERIFY_REQUEST_FAILURE: + return { + ...state, + statuscode: action.payload.statuscode, + message: action.payload.message, + isLoading: false, + }; + + case VERIFY_REQUEST: + return { + ...state, + isLoading: true, + }; + + default: + return state; + } +}; + +export default verifyEmailReducer;