Skip to content

Commit

Permalink
feat: added password reset basic functionality - cannot be pushed to …
Browse files Browse the repository at this point in the history
…prod yet due to security issues [2024-12-08]
  • Loading branch information
CHRISCARLON committed Dec 8, 2024
1 parent 0fc71c9 commit 58ec8ef
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 11 deletions.
21 changes: 21 additions & 0 deletions gridwalk-backend/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,25 @@ impl User {
hash: password_hash,
}
}

pub async fn update_password(
&mut self,
database: &Arc<dyn Database>,
new_password: &str,
) -> Result<()> {
let new_hash = hash_password(new_password)?;
self.hash = new_hash;
database.update_user_password(self).await
}

pub async fn reset_password(
database: &Arc<dyn Database>,
email: &str,
new_password: &str,
) -> Result<User> {
let mut user = Self::from_email(database, email).await?;
user.update_password(database, new_password).await?;

Ok(user)
}
}
1 change: 1 addition & 0 deletions gridwalk-backend/src/data/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub trait UserStore: Send + Sync + 'static {
async fn get_workspaces(&self, user: &User) -> Result<Vec<String>>;
async fn get_projects(&self, workspace_id: &str) -> Result<Vec<Project>>;
async fn delete_project(&self, project: &Project) -> Result<()>;
async fn update_user_password(&self, user: &User) -> Result<()>;
}

#[async_trait]
Expand Down
19 changes: 19 additions & 0 deletions gridwalk-backend/src/data/dynamodb/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,25 @@ impl UserStore for Dynamodb {
Ok(())
}

async fn update_user_password(&self, user: &User) -> Result<()> {
// Create update expression for the USER item
let key = format!("USER#{}", user.id);

self.client
.update_item()
.table_name(&self.table_name)
.key("PK", AV::S(key.clone()))
.key("SK", AV::S(key))
.update_expression("SET #hash = :hash")
.expression_attribute_names("#hash", "hash")
.expression_attribute_values(":hash", AV::S(user.hash.clone()))
.send()
.await
.map_err(|e| anyhow!("Failed to update password: {}", e))?;

Ok(())
}

async fn get_user_by_email(&self, email: &str) -> Result<User> {
let email_key = format!("EMAIL#{email}");
match self
Expand Down
51 changes: 51 additions & 0 deletions gridwalk-backend/src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,54 @@ pub async fn profile(
None => Err((StatusCode::FORBIDDEN, "unauthorized".to_string())),
}
}

#[derive(Debug, Deserialize)]
pub struct ResetPasswordRequest {
email: String,
new_password: String,
}

pub async fn reset_password(
State(state): State<Arc<AppState>>,
Extension(auth_user): Extension<AuthUser>,
Json(req): Json<ResetPasswordRequest>,
) -> impl IntoResponse {
// Ensure user is authenticated
let user = match auth_user.user {
Some(user) => user,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({
"error": "Authentication required"
})),
)
}
};

// Only allow users to reset their own password
if user.email != req.email {
return (
StatusCode::FORBIDDEN,
Json(json!({
"error": "Can only reset your own password"
})),
);
}

// Use the static method to handle the update
match User::reset_password(&state.app_data, &req.email, &req.new_password).await {
Ok(_) => (
StatusCode::OK,
Json(json!({
"message": "Password updated successfully"
})),
),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": "Failed to update password"
})),
),
}
}
3 changes: 2 additions & 1 deletion gridwalk-backend/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::routes::{
add_workspace_member, create_connection, create_project, create_workspace, delete_connection,
delete_project, generate_os_token, get_projects, get_workspace_members, get_workspaces,
health_check, list_connections, list_sources, login, logout, profile, register,
remove_workspace_member, tiles, upload_layer,
remove_workspace_member, reset_password, tiles, upload_layer,
};
use axum::{
extract::DefaultBodyLimit,
Expand Down Expand Up @@ -34,6 +34,7 @@ pub fn create_app(app_state: AppState) -> Router {
.route("/workspaces", get(get_workspaces))
.route("/logout", post(logout))
.route("/profile", get(profile))
.route("/password_reset", post(reset_password))
.route("/workspace", post(create_workspace))
.route("/workspace/members", post(add_workspace_member))
.route(
Expand Down
109 changes: 107 additions & 2 deletions gridwalk-ui/src/app/login/components/loginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import React, { useState } from "react";
import { Lock, Mail, User } from "lucide-react";
import { Lock, Mail, User, ArrowLeft } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
Card,
Expand All @@ -22,9 +22,16 @@ interface AuthFormData {
last_name?: string;
}

const resetPasswordAction = async (email: string): Promise<void> => {
// TODO: Implement password reset logic
console.log("Password reset requested for:", email);
throw new Error("Password reset not yet implemented");
};

export default function AuthForm(): JSX.Element {
const router = useRouter();
const [isLogin, setIsLogin] = useState(true);
const [showForgotPassword, setShowForgotPassword] = useState(false);
const [formData, setFormData] = useState<AuthFormData>({
email: "",
password: "",
Expand All @@ -33,6 +40,8 @@ export default function AuthForm(): JSX.Element {
});
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [resetEmail, setResetEmail] = useState("");
const [isResetSuccess, setIsResetSuccess] = useState(false);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const { name, value } = e.target;
Expand Down Expand Up @@ -75,6 +84,26 @@ export default function AuthForm(): JSX.Element {
}
};

const handleResetPassword = async (
e: React.FormEvent<HTMLFormElement>,
): Promise<void> => {
e.preventDefault();
if (!resetEmail) {
setError("Please enter your email address");
return;
}
setError("");
setLoading(true);
try {
await resetPasswordAction(resetEmail);
setIsResetSuccess(true);
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setLoading(false);
}
};

const toggleAuthMode = () => {
setIsLogin(!isLogin);
setError("");
Expand All @@ -86,6 +115,77 @@ export default function AuthForm(): JSX.Element {
});
};

const handleBackToLogin = () => {
setShowForgotPassword(false);
setIsResetSuccess(false);
setResetEmail("");
setError("");
};

if (showForgotPassword) {
return (
<Card className="w-full backdrop-blur-sm bg-gray-300/20">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl text-center">Reset Password</CardTitle>
<CardDescription className="text-center">
Enter your email address and we will send you a reset link
</CardDescription>
</CardHeader>
<CardContent>
{isResetSuccess ? (
<div className="space-y-4">
<Alert className="bg-green-50 text-green-800 border-green-200">
<AlertDescription>
If an account exists with this email, you will receive a
password reset link shortly. Please check your email - and
junk folder.
</AlertDescription>
</Alert>
<Button
variant="outline"
className="w-full"
onClick={handleBackToLogin}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Login
</Button>
</div>
) : (
<form onSubmit={handleResetPassword} className="space-y-4">
{error && (
<Alert variant="destructive" className="text-red-600 mb-4">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-2">
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-500" />
<Input
type="email"
placeholder="Email"
className="pl-10"
value={resetEmail}
onChange={(e) => setResetEmail(e.target.value)}
/>
</div>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Sending reset link..." : "Send reset link"}
</Button>
<Button
variant="link"
className="w-full"
onClick={handleBackToLogin}
>
Back to Login
</Button>
</form>
)}
</CardContent>
</Card>
);
}

return (
<Card className="w-full backdrop-blur-sm bg-gray-300/20">
<CardHeader className="space-y-1">
Expand Down Expand Up @@ -174,7 +274,12 @@ export default function AuthForm(): JSX.Element {
/>
<span className="text-sm">Remember me</span>
</label>
<Button variant="link" className="text-sm">
<Button
variant="link"
className="text-sm"
onClick={() => setShowForgotPassword(true)}
type="button"
>
Forgot password?
</Button>
</div>
Expand Down
20 changes: 12 additions & 8 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,43 +50,47 @@ git-push:
git push


# New commands for dev environment setup
.PHONY: dev-env dev-env-kill load-env docker-services backend frontend

# Load environment variables
# Export all variables
export

# Load environment variables and export them
load-env:
@if [ -f .env ]; then \
set -a; \
. .env; \
set +a; \
$(eval include .env) \
$(eval export $(shell sed 's/=.*//' .env)) \
else \
echo "Error: .env file not found"; \
exit 1; \
fi

# Start Docker services
docker-services:
docker-services: load-env
@echo "Starting Docker services..."
docker-compose up -d

# Start backend services
backend:
backend: load-env
@echo "Starting backend services..."
cd gridwalk-backend && \
echo "$$AWS_PASS" | aws-vault exec gridw -- cargo run
aws-vault exec gridw -- cargo run

# Start frontend services
frontend:
frontend: load-env
@echo "Starting frontend services..."
cd gridwalk-ui && \
npm run dev

# Main command to set up development environment
dev-env:
dev-env: load-env
@echo "Setting up development environment..."
tmux new-session -d -s gridwalk
tmux rename-window -t gridwalk:0 'GRIDWALK DEV ENVIRONMENT'
tmux send-keys -t gridwalk:0 'docker-compose up -d' C-m
tmux send-keys -t gridwalk:0 'make docker-services' C-m
tmux send-keys -t gridwalk:0 'cd gridwalk-ui && npm run dev' C-m
tmux split-window -h -t gridwalk:0
tmux send-keys -t gridwalk:0.1 'cd gridwalk-backend && echo $$AWS_PASS | aws-vault exec gridw -- cargo run' C-m
Expand Down

0 comments on commit 58ec8ef

Please sign in to comment.