feat: Initial commit of backend services and AGENTS.md

This commit is contained in:
William Valentin
2025-10-29 13:12:30 -07:00
commit 999d37babb
25 changed files with 3881 additions and 0 deletions

67
AGENTS.md Normal file
View File

@@ -0,0 +1,67 @@
# Adopt-a-Street Agents
This document outlines the various agents (user roles and automated systems) within the Adopt-a-Street application, their capabilities, and how they interact with the platform.
## User Agents
### 1. Regular User
Regular users are the primary actors in the Adopt-a-Street ecosystem. They are community members who volunteer to maintain and improve their local streets.
**Capabilities:**
* **Authentication:** Register for a new account, log in, and log out.
* **Street Adoption:** View available streets on a map and adopt one or more streets to care for.
* **Task Management:** View and complete maintenance tasks associated with their adopted streets (e.g., trash pickup, graffiti removal).
* **Social Feed:** Share updates and photos of their work, view posts from other users, and engage with the community.
* **Events:** Participate in community cleanup events and other local initiatives.
* **Rewards:** Earn points and badges for completing tasks and participating in events, which can be redeemed for rewards.
* **Profile Management:** View their profile, including adopted streets, completed tasks, points, and badges.
### 2. Premium User
Premium users have all the capabilities of regular users, with potential access to additional features and benefits.
**Capabilities:**
* All capabilities of a Regular User.
* **Premium Features:** Access to exclusive rewards, advanced analytics, or other premium features (as defined by the application).
## Automated Agents
### 1. AI Agent
The AI agent provides intelligent features and support to users, helping to streamline the street adoption process and enhance the user experience.
**Functionality:**
* **Task Suggestions:** Recommends maintenance tasks based on street conditions, user activity, and other data.
* **Community Insights:** Provides analytics and insights into community engagement and environmental impact.
* **Gamification:** Manages the points, badges, and rewards system to encourage user participation.
## System Architecture
The Adopt-a-Street application is a full-stack web application with a React frontend and a Node.js/Express backend.
### Frontend Components
* **`MapView`:** Displays an interactive map of streets, allowing users to view available streets and adopt them.
* **`TaskList`:** Lists the maintenance tasks for a user's adopted streets.
* **`SocialFeed`:** A social media-style feed for users to share updates and connect with others.
* **`Profile`:** Displays user information, including adopted streets, points, and badges.
* **`Events`:** Shows upcoming community events and allows users to RSVP.
* **`Rewards`:** A marketplace where users can redeem points for rewards.
* **`Login` and `Register`:** User authentication components.
* **`AuthContext`:** Manages user authentication state throughout the application.
### Backend API Endpoints
* `/api/auth`: User registration and login.
* `/api/streets`: Get street data and adopt streets.
* `/api/tasks`: Manage maintenance tasks.
* `/api/posts`: Create and view posts in the social feed.
* `/api/events`: Manage community events.
* `/api/rewards`: Manage rewards and user points.
* `/api/reports`: User-submitted reports on street conditions.
* `/api/ai`: AI-powered suggestions and insights.
* `/api/payments`: Handle premium subscriptions and other payments.

2
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
.env

18
backend/eslint.config.js Normal file
View File

@@ -0,0 +1,18 @@
const globals = require("globals");
module.exports = [
{
languageOptions: {
ecmaVersion: "latest",
sourceType: "commonjs",
globals: {
...globals.browser,
...globals.node,
},
},
rules: {
"no-unused-vars": "warn",
"no-undef": "warn",
},
},
];

View File

@@ -0,0 +1,20 @@
const jwt = require("jsonwebtoken");
module.exports = function (req, res, next) {
// Get token from header
const token = req.header("x-auth-token");
// Check if not token
if (!token) {
return res.status(401).json({ msg: "No token, authorization denied" });
}
// Verify token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded.user;
next();
} catch (err) {
res.status(401).json({ msg: "Token is not valid" });
}
};

33
backend/models/Event.js Normal file
View File

@@ -0,0 +1,33 @@
const mongoose = require("mongoose");
const EventSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
date: {
type: Date,
required: true,
},
location: {
type: String,
required: true,
},
participants: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
],
},
{
timestamps: true,
},
);
module.exports = mongoose.model("Event", EventSchema);

29
backend/models/Post.js Normal file
View File

@@ -0,0 +1,29 @@
const mongoose = require("mongoose");
const PostSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
content: {
type: String,
required: true,
},
imageUrl: {
type: String,
},
likes: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
],
},
{
timestamps: true,
},
);
module.exports = mongoose.model("Post", PostSchema);

30
backend/models/Report.js Normal file
View File

@@ -0,0 +1,30 @@
const mongoose = require("mongoose");
const ReportSchema = new mongoose.Schema(
{
street: {
type: mongoose.Schema.Types.ObjectId,
ref: "Street",
required: true,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
issue: {
type: String,
required: true,
},
status: {
type: String,
enum: ["open", "resolved"],
default: "open",
},
},
{
timestamps: true,
},
);
module.exports = mongoose.model("Report", ReportSchema);

27
backend/models/Reward.js Normal file
View File

@@ -0,0 +1,27 @@
const mongoose = require("mongoose");
const RewardSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
cost: {
type: Number,
required: true,
},
isPremium: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
},
);
module.exports = mongoose.model("Reward", RewardSchema);

37
backend/models/Street.js Normal file
View File

@@ -0,0 +1,37 @@
const mongoose = require("mongoose");
const StreetSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
location: {
type: {
type: String,
enum: ["Point"],
required: true,
},
coordinates: {
type: [Number],
required: true,
},
},
adoptedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
status: {
type: String,
enum: ["available", "adopted"],
default: "available",
},
},
{
timestamps: true,
},
);
StreetSchema.index({ location: "2dsphere" });
module.exports = mongoose.model("Street", StreetSchema);

29
backend/models/Task.js Normal file
View File

@@ -0,0 +1,29 @@
const mongoose = require("mongoose");
const TaskSchema = new mongoose.Schema(
{
street: {
type: mongoose.Schema.Types.ObjectId,
ref: "Street",
required: true,
},
description: {
type: String,
required: true,
},
completedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
status: {
type: String,
enum: ["pending", "completed"],
default: "pending",
},
},
{
timestamps: true,
},
);
module.exports = mongoose.model("Task", TaskSchema);

45
backend/models/User.js Normal file
View File

@@ -0,0 +1,45 @@
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
isPremium: {
type: Boolean,
default: false,
},
points: {
type: Number,
default: 0,
},
adoptedStreets: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Street",
},
],
completedTasks: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Task",
},
],
badges: [String],
},
{
timestamps: true,
},
);
module.exports = mongoose.model("User", UserSchema);

2891
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
backend/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "adopt-a-street",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.8.3",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"globals": "^16.4.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.12.1",
"multer": "^1.4.5-lts.1",
"socket.io": "^4.8.1",
"stripe": "^17.7.0"
},
"devDependencies": {
"eslint": "^9.38.0"
}
}

23
backend/routes/ai.js Normal file
View File

@@ -0,0 +1,23 @@
const express = require("express");
const auth = require("../middleware/auth");
const router = express.Router();
// Get AI task suggestions
router.get("/task-suggestions", auth, async (req, res) => {
try {
// In a real application, you would use a more sophisticated AI model to generate task suggestions.
// For this example, we'll just return some mock data.
const suggestions = [
{ street: "Main St", description: "Clean up litter" },
{ street: "Elm St", description: "Remove graffiti" },
{ street: "Oak Ave", description: "Mow the lawn" },
];
res.json(suggestions);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
module.exports = router;

98
backend/routes/auth.js Normal file
View File

@@ -0,0 +1,98 @@
const express = require("express");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const auth = require("../middleware/auth");
const router = express.Router();
// Get user
router.get("/", auth, async (req, res) => {
try {
const user = await User.findById(req.user.id).select("-password");
res.json(user);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Register
router.post("/register", async (req, res) => {
const { name, email, password } = req.body;
try {
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: "User already exists" });
}
user = new User({
name,
email,
password,
});
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
await user.save();
const payload = {
user: {
id: user.id,
},
};
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: 3600 },
(err, token) => {
if (err) throw err;
res.json({ token });
},
);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Login
router.post("/login", async (req, res) => {
const { email, password } = req.body;
try {
let user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ msg: "Invalid credentials" });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ msg: "Invalid credentials" });
}
const payload = {
user: {
id: user.id,
},
};
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: 3600 },
(err, token) => {
if (err) throw err;
res.json({ token });
},
);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
module.exports = router;

66
backend/routes/events.js Normal file
View File

@@ -0,0 +1,66 @@
const express = require("express");
const Event = require("../models/Event");
const auth = require("../middleware/auth");
const router = express.Router();
// Get all events
router.get("/", async (req, res) => {
try {
const events = await Event.find();
res.json(events);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Create an event
router.post("/", auth, async (req, res) => {
const { title, description, date, location } = req.body;
try {
const newEvent = new Event({
title,
description,
date,
location,
});
const event = await newEvent.save();
res.json(event);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// RSVP to an event
router.put("/rsvp/:id", auth, async (req, res) => {
try {
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ msg: "Event not found" });
}
// Check if the user has already RSVPed
if (
event.participants.filter(
(participant) => participant.toString() === req.user.id,
).length > 0
) {
return res.status(400).json({ msg: "Already RSVPed" });
}
event.participants.unshift(req.user.id);
await event.save();
res.json(event.participants);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
module.exports = router;

View File

@@ -0,0 +1,27 @@
const express = require("express");
const auth = require("../middleware/auth");
const User = require("../models/User");
const router = express.Router();
// Handle premium subscription
router.post("/subscribe", auth, async (req, res) => {
try {
// In a real application, you would integrate with a payment gateway like Stripe.
// For this example, we'll just mock a successful payment.
const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ msg: "User not found" });
}
user.isPremium = true;
await user.save();
res.json({ msg: "Subscription successful" });
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
module.exports = router;

63
backend/routes/posts.js Normal file
View File

@@ -0,0 +1,63 @@
const express = require("express");
const Post = require("../models/Post");
const auth = require("../middleware/auth");
const router = express.Router();
// Get all posts
router.get("/", async (req, res) => {
try {
const posts = await Post.find().populate("user", ["name"]);
res.json(posts);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Create a post
router.post("/", auth, async (req, res) => {
const { content, imageUrl } = req.body;
try {
const newPost = new Post({
user: req.user.id,
content,
imageUrl,
});
const post = await newPost.save();
res.json(post);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Like a post
router.put("/like/:id", auth, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ msg: "Post not found" });
}
// Check if the post has already been liked by this user
if (
post.likes.filter((like) => like.toString() === req.user.id).length > 0
) {
return res.status(400).json({ msg: "Post already liked" });
}
post.likes.unshift(req.user.id);
await post.save();
res.json(post.likes);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
module.exports = router;

58
backend/routes/reports.js Normal file
View File

@@ -0,0 +1,58 @@
const express = require("express");
const Report = require("../models/Report");
const auth = require("../middleware/auth");
const router = express.Router();
// Get all reports
router.get("/", async (req, res) => {
try {
const reports = await Report.find()
.populate("street", ["name"])
.populate("user", ["name"]);
res.json(reports);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Create a report
router.post("/", auth, async (req, res) => {
const { street, issue } = req.body;
try {
const newReport = new Report({
street,
user: req.user.id,
issue,
});
const report = await newReport.save();
res.json(report);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Resolve a report
router.put("/:id", auth, async (req, res) => {
try {
const report = await Report.findById(req.params.id);
if (!report) {
return res.status(404).json({ msg: "Report not found" });
}
report.status = "resolved";
await report.save();
res.json(report);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
module.exports = router;

70
backend/routes/rewards.js Normal file
View File

@@ -0,0 +1,70 @@
const express = require("express");
const Reward = require("../models/Reward");
const User = require("../models/User");
const auth = require("../middleware/auth");
const router = express.Router();
// Get all rewards
router.get("/", async (req, res) => {
try {
const rewards = await Reward.find();
res.json(rewards);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Create a reward
router.post("/", auth, async (req, res) => {
const { name, description, cost, isPremium } = req.body;
try {
const newReward = new Reward({
name,
description,
cost,
isPremium,
});
const reward = await newReward.save();
res.json(reward);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Redeem a reward
router.post("/redeem/:id", auth, async (req, res) => {
try {
const reward = await Reward.findById(req.params.id);
if (!reward) {
return res.status(404).json({ msg: "Reward not found" });
}
const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ msg: "User not found" });
}
if (user.points < reward.cost) {
return res.status(400).json({ msg: "Not enough points" });
}
if (reward.isPremium && !user.isPremium) {
return res.status(403).json({ msg: "Premium reward not available" });
}
user.points -= reward.cost;
await user.save();
res.json({ msg: "Reward redeemed successfully" });
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
module.exports = router;

74
backend/routes/streets.js Normal file
View File

@@ -0,0 +1,74 @@
const express = require("express");
const Street = require("../models/Street");
const auth = require("../middleware/auth");
const router = express.Router();
// Get all streets
router.get("/", async (req, res) => {
try {
const streets = await Street.find();
res.json(streets);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Get single street
router.get("/:id", async (req, res) => {
try {
const street = await Street.findById(req.params.id);
if (!street) {
return res.status(404).json({ msg: "Street not found" });
}
res.json(street);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Create a street
router.post("/", auth, async (req, res) => {
const { name, location } = req.body;
try {
const newStreet = new Street({
name,
location,
});
const street = await newStreet.save();
res.json(street);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Adopt a street
router.put("/adopt/:id", auth, async (req, res) => {
try {
const street = await Street.findById(req.params.id);
if (!street) {
return res.status(404).json({ msg: "Street not found" });
}
if (street.status === "adopted") {
return res.status(400).json({ msg: "Street already adopted" });
}
street.adoptedBy = req.user.id;
street.status = "adopted";
await street.save();
res.json(street);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
module.exports = router;

56
backend/routes/tasks.js Normal file
View File

@@ -0,0 +1,56 @@
const express = require("express");
const Task = require("../models/Task");
const auth = require("../middleware/auth");
const router = express.Router();
// Get all tasks for user
router.get("/", auth, async (req, res) => {
try {
const tasks = await Task.find({ completedBy: req.user.id });
res.json(tasks);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Create a task
router.post("/", auth, async (req, res) => {
const { street, description } = req.body;
try {
const newTask = new Task({
street,
description,
});
const task = await newTask.save();
res.json(task);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
// Complete a task
router.put("/:id", auth, async (req, res) => {
try {
const task = await Task.findById(req.params.id);
if (!task) {
return res.status(404).json({ msg: "Task not found" });
}
task.completedBy = req.user.id;
task.status = "completed";
await task.save();
res.json(task);
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
}
});
module.exports = router;

21
backend/routes/users.js Normal file
View File

@@ -0,0 +1,21 @@
const express = require('express');
const User = require('../models/User');
const auth = require('../middleware/auth');
const router = express.Router();
// Get user by ID
router.get('/:id', auth, async (req, res) => {
try {
const user = await User.findById(req.params.id).populate('adoptedStreets');
if (!user) {
return res.status(404).json({ msg: 'User not found' });
}
res.json(user);
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
});
module.exports = router;

68
backend/server.js Normal file
View File

@@ -0,0 +1,68 @@
require("dotenv").config();
const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
const http = require("http");
const socketio = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = socketio(server);
const port = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());
// MongoDB Connection
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log("MongoDB connected"))
.catch((err) => console.log("MongoDB connection error:", err));
// Socket.IO Setup
io.on("connection", (socket) => {
console.log("New client connected");
socket.on("joinEvent", (eventId) => {
socket.join(eventId);
});
socket.on("eventUpdate", (data) => {
io.to(data.eventId).emit("update", data.message);
});
socket.on("disconnect", () => {
console.log("Client disconnected");
});
});
// Routes
const authRoutes = require("./routes/auth");
const streetRoutes = require("./routes/streets");
const taskRoutes = require("./routes/tasks");
const postRoutes = require("./routes/posts");
const eventRoutes = require("./routes/events");
const rewardRoutes = require("./routes/rewards");
const reportRoutes = require("./routes/reports");
const aiRoutes = require("./routes/ai");
const paymentRoutes = require("./routes/payments");
const userRoutes = require("./routes/users");
app.use("/api/auth", authRoutes);
app.use("/api/streets", streetRoutes);
app.use("/api/tasks", taskRoutes);
app.use("/api/posts", postRoutes);
app.use("/api/events", eventRoutes);
app.use("/api/rewards", rewardRoutes);
app.use("/api/reports", reportRoutes);
app.use("/api/ai", aiRoutes);
app.use("/api/payments", paymentRoutes);
app.use("/api/users", userRoutes);
app.get("/", (req, res) => {
res.send("Street Adoption App Backend");
});
server.listen(port, () => {
console.log(`Server running on port ${port}`);
});

1
frontend Submodule

Submodule frontend added at 2b98fcfd52