From 89033c085ec3f0811f98d95f4965c6541ab17e4d Mon Sep 17 00:00:00 2001 From: ZhuoweiWen Date: Sat, 20 Jul 2024 20:12:23 -0400 Subject: [PATCH] Email server setup Signed-off-by: ZhuoweiWen --- back-end/Dockerfile.backend | 44 +++++----- back-end/Dockerfile.worker | 44 +++++----- .../database_controller/user_ops.py | 20 ++++- back-end/database/models.py | 6 +- back-end/requirements.txt | 1 + back-end/routes.py | 70 ++++++++++------ back-end/utils/config.py | 8 ++ back-end/utils/flask_app.py | 11 ++- back-end/utils/settings.py | 1 + front-end/Dockerfile | 41 ++------- front-end/pages/emailVerification.js | 83 +++++++++++++++++++ front-end/pages/register.js | 78 ++++++++--------- 12 files changed, 258 insertions(+), 149 deletions(-) create mode 100644 front-end/pages/emailVerification.js diff --git a/back-end/Dockerfile.backend b/back-end/Dockerfile.backend index cd69a9a..0a15193 100644 --- a/back-end/Dockerfile.backend +++ b/back-end/Dockerfile.backend @@ -33,37 +33,37 @@ RUN git clone https://github.com/mapbox/tippecanoe.git && \ # Check if Tippecanoe is in PATH RUN which tippecanoe || { echo 'Tippecanoe not found in PATH'; exit 1; } -# Set the working directory to /Signal-Server -WORKDIR /Signal-Server +# # Set the working directory to /Signal-Server +# WORKDIR /Signal-Server -# Clone Signal-Server repository -RUN git clone https://github.com/Cloud-RF/Signal-Server.git . +# # Clone Signal-Server repository +# RUN git clone https://github.com/Cloud-RF/Signal-Server.git . -# Change to the source directory and build the binaries -WORKDIR /Signal-Server/src +# # Change to the source directory and build the binaries +# WORKDIR /Signal-Server/src -# Before running cmake, we should ensure that the RUNTIME DESTINATION is specified in CMakeLists.txt. -RUN sed -i 's/install(TARGETS signalserver)/install(TARGETS signalserver RUNTIME DESTINATION bin)/' CMakeLists.txt -RUN sed -i 's/install(TARGETS signalserverHD)/install(TARGETS signalserverHD RUNTIME DESTINATION bin)/' CMakeLists.txt -RUN sed -i 's/install(TARGETS signalserverLIDAR)/install(TARGETS signalserverLIDAR RUNTIME DESTINATION bin)/' CMakeLists.txt +# # Before running cmake, we should ensure that the RUNTIME DESTINATION is specified in CMakeLists.txt. +# RUN sed -i 's/install(TARGETS signalserver)/install(TARGETS signalserver RUNTIME DESTINATION bin)/' CMakeLists.txt +# RUN sed -i 's/install(TARGETS signalserverHD)/install(TARGETS signalserverHD RUNTIME DESTINATION bin)/' CMakeLists.txt +# RUN sed -i 's/install(TARGETS signalserverLIDAR)/install(TARGETS signalserverLIDAR RUNTIME DESTINATION bin)/' CMakeLists.txt -# Run cmake and make, specifying a runtime destination -RUN cmake -DCMAKE_INSTALL_PREFIX=/usr/local . && \ - make +# # Run cmake and make, specifying a runtime destination +# RUN cmake -DCMAKE_INSTALL_PREFIX=/usr/local . && \ +# make -# Add /usr/local/bin to the PATH, just in case it's not -ENV PATH="/Signal-Server/src:${PATH}" +# # Add /usr/local/bin to the PATH, just in case it's not +# ENV PATH="/Signal-Server/src:${PATH}" -# Check if signalserver is in PATH -RUN which signalserver || { echo 'Signal-Server not found in PATH'; exit 1; } +# # Check if signalserver is in PATH +# RUN which signalserver || { echo 'Signal-Server not found in PATH'; exit 1; } -WORKDIR /Signal-Server/output/GoogleEarth -# Modify runsig.sh to use signalserverHD instead of signalserver -RUN sed -i 's/time signalserver -sdf/time signalserverHD -sdf/' runsig.sh +# WORKDIR /Signal-Server/output/GoogleEarth +# # Modify runsig.sh to use signalserverHD instead of signalserver +# RUN sed -i 's/time signalserver -sdf/time signalserverHD -sdf/' runsig.sh -ENV PATH="/Signal-Server/output/GoogleEarth:${PATH}" +# ENV PATH="/Signal-Server/output/GoogleEarth:${PATH}" -Run which runsig.sh || { echo 'runsig not found in PATH'; exit 1; } +# Run which runsig.sh || { echo 'runsig not found in PATH'; exit 1; } WORKDIR /app diff --git a/back-end/Dockerfile.worker b/back-end/Dockerfile.worker index 70c8a24..a7a2142 100644 --- a/back-end/Dockerfile.worker +++ b/back-end/Dockerfile.worker @@ -33,37 +33,37 @@ RUN git clone https://github.com/mapbox/tippecanoe.git && \ # Check if Tippecanoe is in PATH RUN which tippecanoe || { echo 'Tippecanoe not found in PATH'; exit 1; } -# Set the working directory to /Signal-Server -WORKDIR /Signal-Server +# # Set the working directory to /Signal-Server +# WORKDIR /Signal-Server -# Clone Signal-Server repository -RUN git clone https://github.com/Cloud-RF/Signal-Server.git . +# # Clone Signal-Server repository +# RUN git clone https://github.com/Cloud-RF/Signal-Server.git . -# Change to the source directory and build the binaries -WORKDIR /Signal-Server/src +# # Change to the source directory and build the binaries +# WORKDIR /Signal-Server/src -# Before running cmake, we should ensure that the RUNTIME DESTINATION is specified in CMakeLists.txt. -RUN sed -i 's/install(TARGETS signalserver)/install(TARGETS signalserver RUNTIME DESTINATION bin)/' CMakeLists.txt -RUN sed -i 's/install(TARGETS signalserverHD)/install(TARGETS signalserverHD RUNTIME DESTINATION bin)/' CMakeLists.txt -RUN sed -i 's/install(TARGETS signalserverLIDAR)/install(TARGETS signalserverLIDAR RUNTIME DESTINATION bin)/' CMakeLists.txt +# # Before running cmake, we should ensure that the RUNTIME DESTINATION is specified in CMakeLists.txt. +# RUN sed -i 's/install(TARGETS signalserver)/install(TARGETS signalserver RUNTIME DESTINATION bin)/' CMakeLists.txt +# RUN sed -i 's/install(TARGETS signalserverHD)/install(TARGETS signalserverHD RUNTIME DESTINATION bin)/' CMakeLists.txt +# RUN sed -i 's/install(TARGETS signalserverLIDAR)/install(TARGETS signalserverLIDAR RUNTIME DESTINATION bin)/' CMakeLists.txt -# Run cmake and make, specifying a runtime destination -RUN cmake -DCMAKE_INSTALL_PREFIX=/usr/local . && \ - make +# # Run cmake and make, specifying a runtime destination +# RUN cmake -DCMAKE_INSTALL_PREFIX=/usr/local . && \ +# make -# Add /usr/local/bin to the PATH, just in case it's not -ENV PATH="/Signal-Server/src:${PATH}" +# # Add /usr/local/bin to the PATH, just in case it's not +# ENV PATH="/Signal-Server/src:${PATH}" -# Check if signalserver is in PATH -RUN which signalserver || { echo 'Signal-Server not found in PATH'; exit 1; } +# # Check if signalserver is in PATH +# RUN which signalserver || { echo 'Signal-Server not found in PATH'; exit 1; } -WORKDIR /Signal-Server/output/GoogleEarth -# Modify runsig.sh to use signalserverHD instead of signalserver -RUN sed -i 's/time signalserver -sdf/time signalserverHD -sdf/' runsig.sh +# WORKDIR /Signal-Server/output/GoogleEarth +# # Modify runsig.sh to use signalserverHD instead of signalserver +# RUN sed -i 's/time signalserver -sdf/time signalserverHD -sdf/' runsig.sh -ENV PATH="/Signal-Server/output/GoogleEarth:${PATH}" +# ENV PATH="/Signal-Server/output/GoogleEarth:${PATH}" -Run which runsig.sh || { echo 'runsig not found in PATH'; exit 1; } +# Run which runsig.sh || { echo 'runsig not found in PATH'; exit 1; } WORKDIR /app diff --git a/back-end/controllers/database_controller/user_ops.py b/back-end/controllers/database_controller/user_ops.py index 416e693..f3f93fe 100755 --- a/back-end/controllers/database_controller/user_ops.py +++ b/back-end/controllers/database_controller/user_ops.py @@ -43,26 +43,38 @@ def get_user_with_username(user_name, session=None): if owns_session: session.close() -def create_user_in_db(username, password, providerid, brandname): +def create_user_in_db(username, password): session = Session() try: existing_user = get_user_with_username(username, session) if existing_user: - logger.debug(existing_user) return {"error": "Username already exists"} hashed_password = generate_password_hash(password, method='sha256') - new_user = user(username=username, password=hashed_password, provider_id=providerid, brand_name=brandname) + new_user = user(username=username, password=hashed_password) session.add(new_user) session.commit() - return {"success": new_user.id} + return {"success": new_user} except Exception as e: session.rollback() return {"error": str(e)} + finally: + session.close() + +def verify_user_email(user_id, email): + session = Session() + try: + userVal = session.query(user).filter(user.username == email, user.id == user_id).one() + if userVal: + userVal.verified = True + session.commit() + except Exception as e: + session.rollback() + return {"error": str(e)} finally: session.close() \ No newline at end of file diff --git a/back-end/database/models.py b/back-end/database/models.py index afe8814..ffcc434 100755 --- a/back-end/database/models.py +++ b/back-end/database/models.py @@ -5,6 +5,8 @@ from datetime import datetime class organization(Base): + __tablename__ = 'organization' + id = Column(Integer, primary_key=True) name = Column(String(100), unique=True, nullable=False) provider_id = Column(Integer) @@ -17,8 +19,10 @@ class user(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) - username = Column(String(50), unique=True) + username = Column(String(50), unique=True, nullable=False) password = Column(String(256)) + is_admin = Column(Boolean, default=False) + verified = Column(Boolean, default=False) organization_id = Column(Integer, ForeignKey('organization.id')) organization = relationship('Organization', back_populates='users') diff --git a/back-end/requirements.txt b/back-end/requirements.txt index 422b925..cb2047c 100755 --- a/back-end/requirements.txt +++ b/back-end/requirements.txt @@ -20,6 +20,7 @@ Fiona==1.9.2 Flask==2.2.3 Flask-Cors==3.0.10 Flask-JWT-Extended==4.5.2 +Flask-Mail==0.10.0 Flask-Login==0.6.2 Flask-SQLAlchemy==3.0.3 folium==0.14.0 diff --git a/back-end/routes.py b/back-end/routes.py index f5c3ad1..fadc0de 100755 --- a/back-end/routes.py +++ b/back-end/routes.py @@ -12,14 +12,15 @@ get_jwt_identity, decode_token ) -from datetime import datetime +from jwt import ExpiredSignatureError +from datetime import datetime, timedelta import shortuuid from celery.result import AsyncResult from celery import chain -from utils.settings import DATABASE_URL, COOKIE_EXP_TIME, backend_port +from utils.settings import DATABASE_URL, COOKIE_EXP_TIME, backend_port, frontend_url from database.sessions import Session from controllers.database_controller import fabric_ops, kml_ops, user_ops, vt_ops, file_ops, folder_ops, mbtiles_ops, challenge_ops, editfile_ops -from utils.flask_app import app, jwt +from utils.flask_app import app, jwt, mail from controllers.celery_controller.celery_config import celery from controllers.celery_controller.celery_tasks import process_data, deleteFiles, toggle_tiles, run_signalserver, raster2vector, preview_fabric_locaiton_coverage, async_folder_copy_for_import, add_files_to_folder from utils.namingschemes import DATETIME_FORMAT, EXPORT_CSV_NAME_TEMPLATE, SIGNALSERVER_RASTER_DATA_NAME_TEMPLATE @@ -31,20 +32,7 @@ from utils.logger_config import logger import json from shapely.geometry import shape -# logger = logging.getLogger(__name__) - - - -# logging.basicConfig(level=logging.DEBUG) -# app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URL -# app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') -# app.config["JWT_SECRET_KEY"] = base64.b64encode(os.getenv('JWT_SECRET').encode()) -# app.config["JWT_TOKEN_LOCATION"] = [os.getenv('JWT_TOKEN_LOCATION')] -# app.config['JWT_ACCESS_COOKIE_NAME'] = os.getenv('JWT_ACCESS_COOKIE_NAME') -# app.config['JWT_COOKIE_CSRF_PROTECT'] = False -# app.config['JWT_ACCESS_TOKEN_EXPIRES'] = COOKIE_EXP_TIME -# jwt = JWTManager(app) - +from flask_mail import Message db_name = os.environ.get("POSTGRES_DB") @@ -219,22 +207,56 @@ def search_location(folderid): +def send_verification_email(email, token): + msg = Message('Email Verification', recipients=[email]) + verification_url = f'{frontend_url}/emailVerification/{token}' + msg.body = f'Please click the link to verify your email: {verification_url}' + mail.send(msg) + +@app.route('/api/verify/', methods=['GET']) +def verify_email(token): + try: + decoded_token = decode_token(token) + user_id = decoded_token['identity']['id'] + email = decoded_token['identity']['email'] + user_ops.verify_user_email(user_id, email) + return jsonify({'status': 'success', 'message': 'Email verified successfully.'}), 200 + except ExpiredSignatureError: + return jsonify({'status': 'error', 'message': 'The token has expired.'}), 400 + except Exception as e: + return jsonify({'status': 'error', 'message': 'Invalid token.'}), 400 + +@app.route('/api/resend_verification', methods=['POST']) +def resend_verification(): + data = request.get_json() + email = data.get('email') + user = user_ops.get_user_by_email(email) + if not user: + return jsonify({'status': 'error', 'message': 'Email address not found.'}), 400 + + email_token = create_access_token(identity={'id': user.id, 'email': email}, expires_delta=timedelta(hours=1)) + send_verification_email(email, email_token) + return jsonify({'status': 'success', 'message': 'Verification email resent.'}), 200 + @app.route('/api/register', methods=['POST']) def register(): data = request.get_json() - username = data.get('username') + email = data.get('email') password = data.get('password') - providerid = data.get('providerId') - brandname = data.get('brandName') - response = user_ops.create_user_in_db(username, password, providerid, brandname) + response = user_ops.create_user_in_db(email, password) if 'error' in response: return jsonify({'status': 'error', 'message': response["error"]}), 400 - access_token = create_access_token(identity={'id': response["success"], 'username': username}) + userVal = response["success"] + access_token = create_access_token(identity={'id': userVal.id, 'verified': userVal.verified}) + + email_token = create_access_token(identity={'id': userVal.id, 'email': email}, expires_delta=timedelta(hours=1)) + + send_verification_email(email, email_token) - response = make_response(jsonify({'status': 'success', 'token': access_token})) + response = make_response(jsonify({'status': 'success', 'token': email_token})) response.set_cookie('token', access_token, httponly=False, samesite='Lax', secure=False) return response, 200 @@ -250,7 +272,7 @@ def login(): if user is not None and check_password_hash(user.password, pword): user_id = user.id - access_token = create_access_token(identity={'id': user_id, 'username': username}) + access_token = create_access_token(identity={'id': user_id, 'verified': user.verified}) response = make_response(jsonify({'status': 'success', 'token': access_token})) response.set_cookie('token', access_token, httponly=False, samesite='Lax', secure=False) return response diff --git a/back-end/utils/config.py b/back-end/utils/config.py index ab69401..6cc54bc 100644 --- a/back-end/utils/config.py +++ b/back-end/utils/config.py @@ -12,3 +12,11 @@ class Config: SQLALCHEMY_DATABASE_URI = DATABASE_URL CELERY_BROKER_URL = 'redis://redis:6379/0' CELERY_RESULT_BACKEND = 'redis://redis:6379/0' + + # Email configurations + MAIL_SERVER = os.getenv('MAIL_SERVER') + MAIL_PORT = int(os.getenv('MAIL_PORT', 587)) + MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'true').lower() in ['true', '1', 't'] + MAIL_USE_SSL = os.getenv('MAIL_USE_SSL', 'false').lower() in ['true', '1', 't'] + MAIL_USERNAME = os.getenv('MAIL_USERNAME') + MAIL_PASSWORD = os.getenv('MAIL_PASSWORD') \ No newline at end of file diff --git a/back-end/utils/flask_app.py b/back-end/utils/flask_app.py index b7d3f40..83fc5e4 100644 --- a/back-end/utils/flask_app.py +++ b/back-end/utils/flask_app.py @@ -3,15 +3,22 @@ from flask_cors import CORS from flask_jwt_extended import JWTManager import os +from flask_mail import Mail + + def create_app(): app = Flask(__name__) app.config.from_object(Config) CORS(app, supports_credentials=True) + mail = Mail() + # Initialize other extensions jwt = JWTManager(app) - return app, jwt + mail.init_app(app) + + return app, jwt, mail -app, jwt = create_app() \ No newline at end of file +app, jwt, mail = create_app() \ No newline at end of file diff --git a/back-end/utils/settings.py b/back-end/utils/settings.py index f00528f..b21c3fc 100755 --- a/back-end/utils/settings.py +++ b/back-end/utils/settings.py @@ -8,6 +8,7 @@ db_host = os.getenv('DB_HOST') db_port = os.getenv('DB_PORT') backend_port = os.getenv('DEVELOP_BACKEND_PORT') +frontend_url = os.getenv('FRONTEND_URL') DATABASE_URL = f'postgresql://{db_user}:{db_password}@{db_host}:{db_port}/postgres' BATCH_SIZE = 50000 COOKIE_EXP_TIME = timedelta(days=7) # Cookie will expire in 7 days diff --git a/front-end/Dockerfile b/front-end/Dockerfile index 8046621..38f4c25 100755 --- a/front-end/Dockerfile +++ b/front-end/Dockerfile @@ -1,38 +1,15 @@ -FROM alpine:3.17 +# Use an official Node.js runtime as a parent image (version 18) +FROM node:20 +# Set the working directory in the container WORKDIR /app -# Install system packages and Node.js -RUN apk update && \ - apk add --no-cache git \ - build-base \ - sqlite-dev \ - zlib-dev \ - bash \ - nodejs \ - npm - -# Verify installation -RUN echo "Node version:" \ - && node --version \ - && echo "npm version:" \ - && npm --version - -# Install application packages -COPY package*.json ./ -RUN npm install - +# Copy the frontend application directory contents into the container at /usr/src/app COPY . . -CMD ["npm", "run", "dev"] - -# RUN npm install --only=production - -# # Copy the rest of the application files -# COPY . . - -# # Build the Next.js app for production -# RUN npm run build +# Install any needed packages specified in package.json +# Note: The base image already includes Node.js and npm, so we don't need to install them separately. +RUN npm install -# # Command to start the production server -# CMD ["npm", "run", "start"] +# Run npm run dev when the container launches +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/front-end/pages/emailVerification.js b/front-end/pages/emailVerification.js new file mode 100644 index 0000000..4def208 --- /dev/null +++ b/front-end/pages/emailVerification.js @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { Button, Container, Typography, TextField, Box } from '@mui/material'; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +const EmailVerification = () => { + const { token } = useParams(); + const [showResend, setShowResend] = useState(false); + + useEffect(() => { + fetch(`/api/verify/${token}`) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + toast.success( + "Your email have been successfully verified", + { + position: toast.POSITION.TOP_RIGHT, + autoClose: 5000, + } + ); + } else { + setShowResend(true); + toast.error(data.message); + } + }); + }, [token]); + + const resendVerification = () => { + const email = prompt('Please enter your email address:'); + if (email) { + fetch('/api/resend_verification', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email: email }) + }) + .then(response => response.json()) + .then(data => { + if (data.status === "success") { + toast.success( + "Verification email resent", + { + position: toast.POSITION.TOP_RIGHT, + autoClose: 5000, + } + ); + } + else { + toast.error( + data.message, + { + position: toast.POSITION.TOP_RIGHT, + autoClose: 5000, + } + ); + } + + }); + } + }; + + return ( + + + Email Verification + {showResend && ( + + )} + + ); +}; + +export default EmailVerification; \ No newline at end of file diff --git a/front-end/pages/register.js b/front-end/pages/register.js index 97a1ef8..c49dff3 100755 --- a/front-end/pages/register.js +++ b/front-end/pages/register.js @@ -2,7 +2,8 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { TextField, Button, Typography, Container } from "@mui/material"; import Navbar from "../components/Navbar"; -import Swal from "sweetalert2"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; import { backend_url } from "../utils/settings"; import { styled } from "@mui/system"; @@ -27,14 +28,28 @@ const RegisterButtonContainer = styled("div")({ const Register = () => { const router = useRouter(); - const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const [providerId, setProviderId] = useState(""); - const [brandName, setBrandName] = useState(""); + + const validateEmail = (email) => { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(String(email).toLowerCase()); + }; const handleRegister = async (e) => { e.preventDefault(); + if (!validateEmail(email)) { + toast.error( + "Please enter a valid email address", + { + position: toast.POSITION.TOP_RIGHT, + autoClose: 5000, + } + ); + return; + } + console.log("url " + (backend_url + "/api/register")); try { const response = await fetch(`${backend_url}/api/register`, { @@ -42,7 +57,7 @@ const Register = () => { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ username, password, providerId, brandName }), + body: JSON.stringify({ email, password }), credentials: "include", }); @@ -53,11 +68,13 @@ const Register = () => { } } else if (response.status === 400) { const data = await response.json(); - if (data.message === "Username already exists") { - Swal.fire( - "Error", - "Username already exists. Please try another one.", - "error" + if (data.message === "Email already exists") { + toast.error( + "Email already exists", + { + position: toast.POSITION.TOP_RIGHT, + autoClose: 5000, + } ); } } @@ -68,6 +85,7 @@ const Register = () => { return (
+ @@ -80,14 +98,14 @@ const Register = () => { margin="normal" required fullWidth - id="username" - label="Username" - name="username" - autoComplete="username" + id="email" + label="Email" + name="email" + autoComplete="email" autoFocus - value={username} - onChange={(e) => setUsername(e.target.value)} - key="username-input" + value={email} + onChange={(e) => setEmail(e.target.value)} + key="email-input" /> { onChange={(e) => setPassword(e.target.value)} key="password-input" /> - setProviderId(e.target.value)} - key="providerId-input" - /> - setBrandName(e.target.value)} - key="brandName-input" - />