diff --git a/backend/.dockerignore b/backend/.dockerignore index 9ea6851..bfc33c4 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -1,2 +1,4 @@ target/ -**/target \ No newline at end of file +**/target +ta-lib/ +ta-lib-0.4.0-src.tar.gz \ No newline at end of file diff --git a/backend/.env b/backend/.env index c2cd4ee..48beed3 100644 --- a/backend/.env +++ b/backend/.env @@ -1 +1 @@ -DATABASE_URL=postgres://postgres:mysecretpassword@localhost:5433/newsletter +DATABASE_URL=postgres://postgres:mysecretpassword@172.17.0.2:5432/newsletterdb diff --git a/backend/.gitignore b/backend/.gitignore index ea8c4bf..57654cc 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1 +1,3 @@ /target +ta-lib/ +ta-lib-0.4.0-src.tar.gz \ No newline at end of file diff --git a/backend/diesel.toml b/backend/diesel.toml index dc4af59..43c5f1f 100644 --- a/backend/diesel.toml +++ b/backend/diesel.toml @@ -6,4 +6,4 @@ file = "src/database/schema.rs" custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] [migrations_directory] -dir = "/home/christian/Videos/Newsletter/src/database" +dir = "./src/database" diff --git a/backend/src/auth/authentication.rs b/backend/src/auth/authentication.rs index 5263805..9a11b44 100644 --- a/backend/src/auth/authentication.rs +++ b/backend/src/auth/authentication.rs @@ -54,16 +54,20 @@ pub async fn auth_verify_otp( otp: String, conn: &mut Database, email_addr: String, -) -> Result { +) -> Result<(), SubscriptionError> { let totp = Otp::new(); let result = totp .check_current(&otp) .map_err(|_| SubscriptionError::InternalError)?; - match subscribe(conn, email_addr) - .await - .map_err(|_| SubscriptionError::DatabaseError) - { - Ok(_) => Ok(result), - Err(e) => Err(e), + if result { + match subscribe(conn, email_addr) + .await + .map_err(|_| SubscriptionError::DatabaseError) + { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } else { + Err(SubscriptionError::OtpError) } } diff --git a/backend/src/auth/errors.rs b/backend/src/auth/errors.rs index f8efc21..f2529e8 100644 --- a/backend/src/auth/errors.rs +++ b/backend/src/auth/errors.rs @@ -15,13 +15,15 @@ pub enum SubscriptionError { #[error("Error storing subscruber")] DatabaseError, #[error("could not send otp")] - MailError + MailError, + #[error("Invalid OTP")] + OtpError } impl SubscriptionError { /// Converts the error to an axum JSON representation. pub fn json(&self) -> Json { - Json(json!({ - "error": self.to_string() - })) + Json({ + json!(self.to_string()) + }) } } diff --git a/backend/src/database/00000000000000_diesel_initial_setup/down.sql b/backend/src/database/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/backend/src/database/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/backend/src/database/00000000000000_diesel_initial_setup/up.sql b/backend/src/database/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/backend/src/database/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/backend/src/database/2024-10-12-191146_migrations/down.sql b/backend/src/database/2024-12-31-221109_migrations/down.sql similarity index 100% rename from backend/src/database/2024-10-12-191146_migrations/down.sql rename to backend/src/database/2024-12-31-221109_migrations/down.sql diff --git a/backend/src/database/2024-10-12-191146_migrations/up.sql b/backend/src/database/2024-12-31-221109_migrations/up.sql similarity index 100% rename from backend/src/database/2024-10-12-191146_migrations/up.sql rename to backend/src/database/2024-12-31-221109_migrations/up.sql diff --git a/backend/src/database/queries.rs b/backend/src/database/queries.rs index dfee37d..ef3880b 100644 --- a/backend/src/database/queries.rs +++ b/backend/src/database/queries.rs @@ -17,7 +17,7 @@ impl Otp { Algorithm::SHA1, 6, 1, - 20, + 30, Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()) .to_bytes() .unwrap(), @@ -30,7 +30,7 @@ impl Otp { Algorithm::SHA1, 6, 1, - 20, + 30, Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()) .to_bytes() .unwrap(), diff --git a/backend/src/main.rs b/backend/src/main.rs index 551c11b..c58c28f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,6 +1,4 @@ -use std::sync::{Arc, Mutex}; use axum::{ - http::Method, response::IntoResponse, routing::{get, post}, Extension, Router, @@ -10,6 +8,7 @@ use newsletter::{ database::connection::establish_connection, web::subscribe::{post_subscribe, post_verify_email, EmailOtp}, }; +use std::sync::{Arc, Mutex}; use tokio::net::TcpListener; use tower::ServiceBuilder; use tower_http::{ @@ -37,8 +36,8 @@ async fn main() { let email = Arc::new(Mutex::new(String::new())); let code = EmailOtp { code: otp }; - let port = 80809; - let socket = format!("0.0.0.0:{}",port); + let port = 8000; + let socket = format!("0.0.0.0:{}", port); let listener = TcpListener::bind(socket).await.unwrap(); let router = Router::new() .route("/", get(welcome)) diff --git a/backend/src/web/errors.rs b/backend/src/web/errors.rs index 190faeb..e183eaf 100644 --- a/backend/src/web/errors.rs +++ b/backend/src/web/errors.rs @@ -3,6 +3,6 @@ use axum::{http::{Response, StatusCode}, response::IntoResponse}; pub fn error_page(e: &dyn std::error::Error) -> impl IntoResponse { Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(format!("error: {}", e)) + .body(format!("{}", e)) .unwrap() } diff --git a/backend/src/web/subscribe.rs b/backend/src/web/subscribe.rs index 4dd8309..e54154e 100644 --- a/backend/src/web/subscribe.rs +++ b/backend/src/web/subscribe.rs @@ -51,6 +51,6 @@ pub async fn post_verify_email( let email = email.0.lock().unwrap().clone(); match auth_verify_otp(otp.code.clone(), &mut database, email).await { Ok(_) => StatusCode::ACCEPTED.into_response(), - Err(e) => e.json().into_response(), + Err(e) => error_page(&e).into_response(), } } diff --git a/docker-compose.yml b/docker-compose.yml index b34e5a8..520a303 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,32 +1,32 @@ -version: '3.8' # Version of Docker Compose +version: '3.8' services: postgres: image: postgres:latest - container_name: newsletter + container_name: newsletter2 ports: - - "5434:5432" # Host Port:Container Port + - "5432:5432" environment: POSTGRES_PASSWORD: mysecretpassword backend: build: - context: ./backend # Path to the backend Dockerfile - image: newsletter_k8s_backend + context: ./backend + image: newsletter_backend container_name: backend ports: - - "8000:8000" # Adjust if your backend uses a different port + - "8000:8000" environment: - DATABASE_URL: postgres://postgres:mysecretpassword@localhost:5433/newsletter + DATABASE_URL: postgres://postgres:mysecretpassword@localhost:5432/newsletter depends_on: - postgres frontend: build: context: ./frontend # Path to the frontend Dockerfile - image: newsletter_k8s_frontend + image: newsletter_frontend container_name: frontend ports: - - "8080:80" # Adjust if your frontend uses a different port + - "5173:5173" # Adjust if your frontend uses a different port depends_on: - backend diff --git a/frontend/src/components/pinform.tsx b/frontend/src/components/pinform.tsx index a95c719..09e6c5e 100644 --- a/frontend/src/components/pinform.tsx +++ b/frontend/src/components/pinform.tsx @@ -2,10 +2,11 @@ import { useFormik } from 'formik'; import OtpInput from 'formik-otp-input'; +import { useState } from 'react'; +import { Alert } from './response'; +import { useNavigate } from 'react-router-dom'; - -// CSS Styles, adjust according to your needs const formStyle: React.CSSProperties = { display: 'flex', flexDirection: 'column', @@ -32,16 +33,18 @@ const submitButtonStyle = { marginTop: '20px', }; -// Form component + const OtpForm = () => { + const navigate = useNavigate(); + const [alertMessage, setAlertMessage] = useState(null); const formik = useFormik({ initialValues: { otp: '', - // ... other form fields if you wish + }, onSubmit: (values) => { - - fetch('http://localhost:8000/verify_otp', { + + fetch('http://0.0.0.0:8000/verify_otp', { method: 'POST', headers: { @@ -54,37 +57,53 @@ const OtpForm = () => { }) }) + .then(async (response) => { + if (!response.ok) { + const message = await response.text(); + setAlertMessage(message); + throw new Error(message); + } + return response; + }) + .then(() => navigate('/')) + .catch((error) => { + setAlertMessage(error.message); + }); }, }); return ( -
- - {formik.errors.otp && formik.touched.otp && ( -
{formik.errors.otp}
+ <> + {alertMessage && ( + setAlertMessage(null)} + /> )} - - +
+ + {formik.errors.otp && formik.touched.otp && ( +
{formik.errors.otp}
+ )} + + + ); }; diff --git a/frontend/src/components/response.tsx b/frontend/src/components/response.tsx index ac87416..bab3a37 100644 --- a/frontend/src/components/response.tsx +++ b/frontend/src/components/response.tsx @@ -1,14 +1,22 @@ +import { useEffect } from "react"; -export default function Response(res: string) { - return ( - <> -
- Holy smokes! - {res} - - Close - -
- - ) -} \ No newline at end of file +export function Alert({ message, onClose }: { message: string; onClose: () => void }) { + useEffect(() => { + const timer = setTimeout(() => { + onClose(); // Automatically dismiss after 5 seconds + }, 5000); + return () => clearTimeout(timer); + }, [onClose]); + + return ( +
+
+ {message} +
+
+ ); +} diff --git a/frontend/src/components/subscription.tsx b/frontend/src/components/subscription.tsx index c128797..63b9e3c 100644 --- a/frontend/src/components/subscription.tsx +++ b/frontend/src/components/subscription.tsx @@ -1,63 +1,74 @@ - import { useFormik } from 'formik'; import { useNavigate } from 'react-router-dom'; -import Response from './response'; - +import { useState} from 'react'; +import { Alert } from './response'; export const SignupForm = () => { - const navigate = useNavigate(); - // Pass the useFormik() hook initial form values and a submit function that will - // be called when the form is submitted - const formik = useFormik({ - initialValues: { - email: '', - }, - onSubmit: value => { - fetch('http://localhost:8000/subscribe', { - method: 'POST', - headers: { - - 'Content-Type': 'application/json', + const navigate = useNavigate(); + const [alertMessage, setAlertMessage] = useState(null); - "Access-Control-Allow-Origin": "*", - }, - body: JSON.stringify({ - email: value.email - - }) - }) - - .then(response => response.text()) - .then(data => Response(data)) - - .then(()=> navigate('/email_verification')); - + const formik = useFormik({ + initialValues: { + email: '', + }, + onSubmit: (values) => { + fetch('http://0.0.0.0:8000/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', }, - }) - return ( -
-
-
- -
-
-
- - + body: JSON.stringify({ email: values.email }), + }) + .then(async (response) => { + if (!response.ok) { + const message = await response.text(); + setAlertMessage(message); + throw new Error(message); + } + return response; + }) + .then(() => navigate('/email_verification')) + .catch((error) => { + setAlertMessage(error.message); + }); + }, + }); -
+ return ( +
+ {alertMessage && ( + setAlertMessage(null)} // Dismiss alert when the close button is clicked + /> + )} +
+
+
+ +
- ); +
+ +
+
+
+ ); }; -