18 Commits

Author SHA1 Message Date
boris
709596eea2 i finally committed i guess
Signed-off-by: boris <boris@borishub.co.uk>
2025-03-15 01:59:16 +00:00
boris
8de2b7f29e (airhead): ive no idea no lie
Signed-off-by: boris <boris@borishub.co.uk>
2024-12-04 21:13:46 +00:00
boris
5b0d04b702 (feat): added captcha for invalid login 2024-12-04 01:37:17 +00:00
boris
cafe0b58d0 (fix): added empty get header redirect 2024-12-04 00:25:37 +00:00
boris
a1711afecc (fix): holy shit filters and searching actually works. fix before was completely schizo i think i was just zynbrained 2024-12-04 00:21:29 +00:00
boris
86f778b7ac (feat)(in-progress): added modal for creation 2024-12-03 22:21:22 +00:00
boris
574dcbb119 (fix): i fixed it !! filter workidge extravaganza headsplosion i just put in 3 zyns 2024-12-03 21:59:08 +00:00
boris
f9d625e905 (feat): filter and direction dropdown 2024-12-03 20:05:15 +00:00
boris
214bfe20ac added functionality for CRUD 2024-12-02 20:33:27 +00:00
boris
00a29b9db7 images 2024-12-01 18:15:49 +00:00
boris
6430dc6904 idk 2024-12-01 18:15:35 +00:00
boris
c650dbce01 changed <button> paginators to <a> paginators. 2024-11-29 21:47:38 +00:00
boris
e3a42f4ba3 commit before branching 2024-11-29 21:34:21 +00:00
boris
b7b5fb545b completely flarched it 2024-11-29 17:19:08 +00:00
boris
4005328979 erm did a small amount
output database to view
added basic filters
improve bootstrap theming and positioning
added pagination
2024-11-28 23:40:06 +00:00
boris
5c24228c20 add template files and compose file, fix syntax errors in template 2024-11-21 12:33:55 +00:00
boris
e1b1455ac0 add template files and compose file, fix syntax errors in template 2024-11-21 12:33:26 +00:00
boris
b64c6d835e add template files and compose file, fix syntax errors in template 2024-11-21 12:33:19 +00:00
98 changed files with 38017 additions and 8308 deletions

Binary file not shown.

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# JWT Configuration
JWT_SECRET_KEY=your-secret-key-here
JWT_TOKEN_EXPIRY=3600 # 1 hour in seconds
# Database Configuration
DB_HOST=localhost
DB_NAME=your_database_name
DB_USER=your_database_user
DB_PASS=your_database_password

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# Environment variables
.env
.env.local
.env.*.local
# IDE files
.idea/
.vscode/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

0
.idea/.gitignore generated vendored Normal file → Executable file
View File

1
.idea/.name generated
View File

@@ -1 +0,0 @@
MVCtemplate

1
.idea/Ecobuddy.iml generated Normal file → Executable file
View File

@@ -4,5 +4,6 @@
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="leaflet" level="application" />
</component>
</module>

9
.idea/MVCtemplate.iml generated
View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="jquery" level="application" />
</component>
</module>

32
.idea/dataSources.xml generated Normal file → Executable file
View File

@@ -1,11 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="ecobuddy.sqlite" uuid="8da0d10f-48a6-4aa3-9200-f806440fb4ed">
<data-source source="LOCAL" name="ecobuddynew.sqlite" uuid="6566010b-b220-4baf-bb3e-99178c3287f0">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/ecobuddy.sqlite</jdbc-url>
<jdbc-url>jdbc:sqlite:Databases/ecobuddynew.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="ecobuddynew" uuid="b5d0338c-4f7c-4008-ba23-032fa68749c1">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/Databases/ecobuddynew.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
<data-source source="LOCAL" name="ecobuddy.sqlite" uuid="5216c958-85d2-48a7-b57e-256771f5c73c">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:Databases/ecobuddy.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>

0
.idea/encodings.xml generated Normal file → Executable file
View File

6
.idea/jsLibraryMappings.xml generated Executable file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{leaflet}" />
</component>
</project>

0
.idea/misc.xml generated Normal file → Executable file
View File

3
.idea/modules.xml generated Normal file → Executable file
View File

@@ -2,8 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/MVCtemplate.iml" filepath="$PROJECT_DIR$/.idea/MVCtemplate.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/Ecobuddy.iml" filepath="$PROJECT_DIR$/.idea/Ecobuddy.iml" />
</modules>
</component>
</project>

8
.idea/php.xml generated Normal file → Executable file
View File

@@ -10,5 +10,11 @@
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="5.5.0" />
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

0
.idea/scopes/scope_settings.xml generated Normal file → Executable file
View File

6
.idea/sqldialects.xml generated Executable file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

0
.idea/vcs.xml generated Normal file → Executable file
View File

View File

@@ -1 +0,0 @@
,boris,boris-ThinkPad-T480,07.11.2024 12:14,file:///home/boris/.config/libreoffice/4;

Binary file not shown.

232
Databases/add_facilities.py Normal file
View File

@@ -0,0 +1,232 @@
import sqlite3
import random
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# Check if we need to add any new categories
cursor.execute("SELECT id FROM ecoCategories")
existing_categories = [row[0] for row in cursor.fetchall()]
# Add two new categories
new_categories = [
(16, "Urban Farms"),
(17, "Rainwater Harvesting Systems")
]
for category in new_categories:
if category[0] not in existing_categories:
cursor.execute("INSERT INTO ecoCategories (id, name) VALUES (?, ?)", category)
print(f"Added new category: {category[1]}")
# Get list of user IDs for contributors
cursor.execute("SELECT id FROM ecoUser")
user_ids = [row[0] for row in cursor.fetchall()]
# Define 10 new ecological facilities in the UK with accurate location data
new_facilities = [
{
"title": "Community Garden Hackney",
"category": 12, # Pollinator Gardens
"description": "Urban garden with native plants to support local pollinators",
"houseNumber": "45",
"streetName": "Dalston Lane",
"county": "Greater London",
"town": "London",
"postcode": "E8 3AH",
"lng": -0.0612,
"lat": 51.5476,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded with new wildflower section",
"Volunteer days every Saturday"
]
},
{
"title": "Rooftop Solar Farm",
"category": 8, # Green Roofs
"description": "Combined green roof and solar panel installation on commercial building",
"houseNumber": "120",
"streetName": "Deansgate",
"county": "Greater Manchester",
"town": "Manchester",
"postcode": "M3 2QJ",
"lng": -2.2484,
"lat": 53.4808,
"contributor": random.choice(user_ids),
"status_comments": [
"Generates power for the entire building"
]
},
{
"title": "Edinburgh Tool Library",
"category": 15, # Community Tool Libraries
"description": "Borrow tools instead of buying them - reducing waste and consumption",
"houseNumber": "25",
"streetName": "Leith Walk",
"county": "Edinburgh",
"town": "Edinburgh",
"postcode": "EH6 8LN",
"lng": -3.1752,
"lat": 55.9677,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Cardiff Bay Water Refill Station",
"category": 9, # Public Water Refill Stations
"description": "Free water refill station to reduce plastic bottle usage",
"houseNumber": "3",
"streetName": "Mermaid Quay",
"county": "Cardiff",
"town": "Cardiff",
"postcode": "CF10 5BZ",
"lng": -3.1644,
"lat": 51.4644,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently cleaned and maintained",
"High usage during summer months"
]
},
{
"title": "Bristol Urban Farm",
"category": 16, # Urban Farms (new category)
"description": "Community-run urban farm providing local produce and education",
"houseNumber": "18",
"streetName": "Stapleton Road",
"county": "Bristol",
"town": "Bristol",
"postcode": "BS5 0RA",
"lng": -2.5677,
"lat": 51.4635,
"contributor": random.choice(user_ids),
"status_comments": [
"Open for volunteers Tuesday-Sunday"
]
},
{
"title": "Newcastle Rainwater Collection System",
"category": 17, # Rainwater Harvesting Systems (new category)
"description": "Public demonstration of rainwater harvesting for garden irrigation",
"houseNumber": "55",
"streetName": "Northumberland Street",
"county": "Tyne and Wear",
"town": "Newcastle upon Tyne",
"postcode": "NE1 7DH",
"lng": -1.6178,
"lat": 54.9783,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Brighton Beach Solar Bench",
"category": 7, # Solar-Powered Benches
"description": "Solar-powered bench with USB charging ports and WiFi",
"houseNumber": "",
"streetName": "Kings Road",
"county": "East Sussex",
"town": "Brighton",
"postcode": "BN1 2FN",
"lng": -0.1426,
"lat": 50.8214,
"contributor": random.choice(user_ids),
"status_comments": [
"Popular spot for tourists",
"One USB port currently not working"
]
},
{
"title": "Leeds Community Compost Hub",
"category": 6, # Community Compost Bins
"description": "Large-scale community composting facility for local residents",
"houseNumber": "78",
"streetName": "Woodhouse Lane",
"county": "West Yorkshire",
"town": "Leeds",
"postcode": "LS2 9JT",
"lng": -1.5491,
"lat": 53.8067,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded capacity"
]
},
{
"title": "Glasgow EV Charging Hub",
"category": 4, # Public EV Charging Stations
"description": "Multi-vehicle EV charging station with fast chargers",
"houseNumber": "42",
"streetName": "Buchanan Street",
"county": "Glasgow",
"town": "Glasgow",
"postcode": "G1 3JX",
"lng": -4.2526,
"lat": 55.8621,
"contributor": random.choice(user_ids),
"status_comments": [
"6 charging points available",
"24/7 access"
]
},
{
"title": "Oxford E-Waste Collection Center",
"category": 13, # E-Waste Collection Bins
"description": "Dedicated facility for proper disposal and recycling of electronic waste",
"houseNumber": "15",
"streetName": "St Aldate's",
"county": "Oxfordshire",
"town": "Oxford",
"postcode": "OX1 1BX",
"lng": -1.2577,
"lat": 51.7520,
"contributor": random.choice(user_ids),
"status_comments": []
}
]
# Insert facilities into the database
for facility in new_facilities:
cursor.execute("""
INSERT INTO ecoFacilities
(title, category, description, houseNumber, streetName, county, town, postcode, lng, lat, contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
facility["title"],
facility["category"],
facility["description"],
facility["houseNumber"],
facility["streetName"],
facility["county"],
facility["town"],
facility["postcode"],
facility["lng"],
facility["lat"],
facility["contributor"]
))
# Get the ID of the inserted facility
facility_id = cursor.lastrowid
# Add status comments if any
for comment in facility["status_comments"]:
cursor.execute("""
INSERT INTO ecoFacilityStatus (facilityId, statusComment)
VALUES (?, ?)
""", (facility_id, comment))
print(f"Added facility: {facility['title']} in {facility['town']}")
# Commit the changes and close the connection
conn.commit()
conn.close()
print("\nSuccessfully added 10 new ecological facilities to the database.")
Using the test data in the ecoCategories, ecoFacilities, and ecoFacilityStatus, please create 10 new ecological facilities with the following constraints:
Must be located in the United Kingdom (Names of areas, towns, cities, counties)
Postcode, Latitude and Longitude must be fairly accurate to the location you have generated for the area.
More categories may be made, it is up to you, but make sure foreign keys are respected.
Contributor may be of any user in the database.
Not all facilities must have statusComments, however facilities may have multiple statusComments.

BIN
Databases/ecobuddy.sqlite Normal file

Binary file not shown.

BIN
Databases/ecobuddynew.sqlite Executable file

Binary file not shown.

View File

@@ -0,0 +1,66 @@
Starting facility generation at 2025-03-14 22:20:38.302675
Target: 1000 new facilities
Generating facilities...
Generated 100 facilities so far...
Generated 200 facilities so far...
Generated 300 facilities so far...
Generated 400 facilities so far...
Generated 500 facilities so far...
Generated 600 facilities so far...
Generated 700 facilities so far...
Generated 800 facilities so far...
Generated 900 facilities so far...
Generated 1000 facilities so far...
Inserting facilities into database...
Inserted batch 1/20
Inserted batch 2/20
Inserted batch 3/20
Inserted batch 4/20
Inserted batch 5/20
Inserted batch 6/20
Inserted batch 7/20
Inserted batch 8/20
Inserted batch 9/20
Inserted batch 10/20
Inserted batch 11/20
Inserted batch 12/20
Inserted batch 13/20
Inserted batch 14/20
Inserted batch 15/20
Inserted batch 16/20
Inserted batch 17/20
Inserted batch 18/20
Inserted batch 19/20
Inserted batch 20/20
Inserting status comments...
Inserted comment batch 1/23
Inserted comment batch 2/23
Inserted comment batch 3/23
Inserted comment batch 4/23
Inserted comment batch 5/23
Inserted comment batch 6/23
Inserted comment batch 7/23
Inserted comment batch 8/23
Inserted comment batch 9/23
Inserted comment batch 10/23
Inserted comment batch 11/23
Inserted comment batch 12/23
Inserted comment batch 13/23
Inserted comment batch 14/23
Inserted comment batch 15/23
Inserted comment batch 16/23
Inserted comment batch 17/23
Inserted comment batch 18/23
Inserted comment batch 19/23
Inserted comment batch 20/23
Inserted comment batch 21/23
Inserted comment batch 22/23
Inserted comment batch 23/23
Generation complete at 2025-03-14 22:20:38.336715
Total facilities in database: 1030
Total status comments in database: 1147
Generated facilities saved to generated_facilities.csv for reference

View File

@@ -0,0 +1,568 @@
import sqlite3
import random
import csv
import os
from datetime import datetime
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# Get current max facility ID
cursor.execute("SELECT MAX(id) FROM ecoFacilities")
max_facility_id = cursor.fetchone()[0] or 0
# Get list of user IDs for contributors
cursor.execute("SELECT id FROM ecoUser")
user_ids = [row[0] for row in cursor.fetchall()]
# Get list of categories
cursor.execute("SELECT id, name FROM ecoCategories")
categories = {row[0]: row[1] for row in cursor.fetchall()}
# UK Cities and Towns with their counties and approximate coordinates
uk_locations = [
# Format: Town/City, County, Latitude, Longitude, Postcode Area
("London", "Greater London", 51.5074, -0.1278, "EC"),
("Birmingham", "West Midlands", 52.4862, -1.8904, "B"),
("Manchester", "Greater Manchester", 53.4808, -2.2426, "M"),
("Glasgow", "Glasgow", 55.8642, -4.2518, "G"),
("Liverpool", "Merseyside", 53.4084, -2.9916, "L"),
("Bristol", "Bristol", 51.4545, -2.5879, "BS"),
("Edinburgh", "Edinburgh", 55.9533, -3.1883, "EH"),
("Leeds", "West Yorkshire", 53.8008, -1.5491, "LS"),
("Sheffield", "South Yorkshire", 53.3811, -1.4701, "S"),
("Newcastle upon Tyne", "Tyne and Wear", 54.9783, -1.6178, "NE"),
("Nottingham", "Nottinghamshire", 52.9548, -1.1581, "NG"),
("Cardiff", "Cardiff", 51.4816, -3.1791, "CF"),
("Belfast", "Belfast", 54.5973, -5.9301, "BT"),
("Brighton", "East Sussex", 50.8225, -0.1372, "BN"),
("Leicester", "Leicestershire", 52.6369, -1.1398, "LE"),
("Aberdeen", "Aberdeen", 57.1497, -2.0943, "AB"),
("Portsmouth", "Hampshire", 50.8198, -1.0880, "PO"),
("York", "North Yorkshire", 53.9599, -1.0873, "YO"),
("Swansea", "Swansea", 51.6214, -3.9436, "SA"),
("Oxford", "Oxfordshire", 51.7520, -1.2577, "OX"),
("Cambridge", "Cambridgeshire", 52.2053, 0.1218, "CB"),
("Exeter", "Devon", 50.7184, -3.5339, "EX"),
("Bath", "Somerset", 51.3751, -2.3617, "BA"),
("Reading", "Berkshire", 51.4543, -0.9781, "RG"),
("Preston", "Lancashire", 53.7632, -2.7031, "PR"),
("Coventry", "West Midlands", 52.4068, -1.5197, "CV"),
("Hull", "East Yorkshire", 53.7676, -0.3274, "HU"),
("Stoke-on-Trent", "Staffordshire", 53.0027, -2.1794, "ST"),
("Wolverhampton", "West Midlands", 52.5870, -2.1288, "WV"),
("Plymouth", "Devon", 50.3755, -4.1427, "PL"),
("Derby", "Derbyshire", 52.9225, -1.4746, "DE"),
("Sunderland", "Tyne and Wear", 54.9069, -1.3830, "SR"),
("Southampton", "Hampshire", 50.9097, -1.4044, "SO"),
("Norwich", "Norfolk", 52.6309, 1.2974, "NR"),
("Bournemouth", "Dorset", 50.7192, -1.8808, "BH"),
("Middlesbrough", "North Yorkshire", 54.5742, -1.2350, "TS"),
("Blackpool", "Lancashire", 53.8175, -3.0357, "FY"),
("Bolton", "Greater Manchester", 53.5785, -2.4299, "BL"),
("Ipswich", "Suffolk", 52.0567, 1.1482, "IP"),
("Telford", "Shropshire", 52.6784, -2.4453, "TF"),
("Dundee", "Dundee", 56.4620, -2.9707, "DD"),
("Peterborough", "Cambridgeshire", 52.5695, -0.2405, "PE"),
("Huddersfield", "West Yorkshire", 53.6458, -1.7850, "HD"),
("Luton", "Bedfordshire", 51.8787, -0.4200, "LU"),
("Warrington", "Cheshire", 53.3900, -2.5970, "WA"),
("Southend-on-Sea", "Essex", 51.5459, 0.7077, "SS"),
("Swindon", "Wiltshire", 51.5557, -1.7797, "SN"),
("Slough", "Berkshire", 51.5105, -0.5950, "SL"),
("Watford", "Hertfordshire", 51.6565, -0.3903, "WD"),
("Carlisle", "Cumbria", 54.8952, -2.9335, "CA")
]
# Street name components for generating realistic street names
street_prefixes = ["High", "Main", "Church", "Park", "Mill", "Station", "London", "Victoria", "Queen", "King", "North", "South", "East", "West", "New", "Old", "Castle", "Bridge", "Green", "Market", "School", "Manor", "Abbey", "Priory", "Cathedral", "University", "College", "Hospital", "Railway", "Canal", "River", "Forest", "Wood", "Hill", "Mount", "Valley", "Meadow", "Field", "Farm", "Garden", "Orchard", "Vineyard", "Grange", "Lodge", "Court", "Hall", "House", "Cottage", "Barn", "Mill", "Windmill", "Watermill", "Forge", "Quarry", "Mine", "Pit", "Well", "Spring", "Brook", "Stream", "Lake", "Pond", "Pool", "Reservoir", "Bay", "Cove", "Beach", "Cliff", "Rock", "Stone", "Granite", "Marble", "Slate", "Clay", "Sand", "Gravel", "Chalk", "Flint", "Coal", "Iron", "Steel", "Copper", "Silver", "Gold", "Tin", "Lead", "Zinc", "Brass", "Bronze", "Pewter", "Nickel", "Cobalt", "Chromium", "Titanium", "Aluminium", "Silicon", "Carbon", "Oxygen", "Hydrogen", "Nitrogen", "Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon"]
street_suffixes = ["Street", "Road", "Lane", "Avenue", "Drive", "Boulevard", "Way", "Place", "Square", "Court", "Terrace", "Close", "Crescent", "Gardens", "Grove", "Mews", "Alley", "Walk", "Path", "Trail", "Hill", "Rise", "View", "Heights", "Park", "Green", "Meadow", "Field", "Common", "Heath", "Moor", "Down", "Fell", "Pike", "Tor", "Crag", "Cliff", "Ridge", "Edge", "Top", "Bottom", "Side", "End", "Corner", "Junction", "Cross", "Gate", "Bridge", "Ford", "Ferry", "Wharf", "Quay", "Dock", "Harbor", "Port", "Bay", "Cove", "Beach", "Shore", "Bank", "Strand", "Esplanade", "Parade", "Promenade", "Embankment", "Causeway", "Viaduct", "Tunnel", "Passage", "Arcade", "Gallery", "Mall", "Market", "Bazaar", "Fair", "Exchange", "Mart", "Emporium", "Center", "Circle", "Oval", "Triangle", "Pentagon", "Hexagon", "Octagon", "Circus", "Ring", "Loop", "Bend", "Curve", "Turn", "Twist", "Spiral", "Coil", "Helix", "Maze", "Labyrinth"]
# Facility descriptions by category
category_descriptions = {
1: [ # Recycling Bins
"Public recycling point for paper, glass, plastic, and metal",
"Community recycling station with separate bins for different materials",
"Recycling center with facilities for household waste separation",
"Public access recycling bins for common household recyclables",
"Multi-material recycling point with clear instructions for proper sorting"
],
2: [ # e-Scooters
"Dockless e-scooter rental station with multiple vehicles available",
"E-scooter parking and charging zone for public use",
"Designated e-scooter pickup and drop-off point",
"E-scooter sharing station with app-based rental system",
"Electric scooter hub with maintenance and charging facilities"
],
3: [ # Bike Share Stations
"Public bicycle sharing station with multiple bikes available",
"Bike rental hub with secure docking stations",
"Community bike share point with regular and electric bicycles",
"Cycle hire station with self-service rental system",
"Bike sharing facility with maintenance and repair services"
],
4: [ # Public EV Charging Stations
"Electric vehicle charging point with multiple connectors",
"Fast-charging station for electric vehicles",
"Public EV charging facility with covered waiting area",
"Multi-vehicle electric charging hub with different power options",
"EV charging station with renewable energy source"
],
5: [ # Battery Recycling Points
"Dedicated collection point for used batteries of all sizes",
"Battery recycling bin with separate compartments for different types",
"Safe disposal facility for household and small electronics batteries",
"Battery collection point with educational information about recycling",
"Secure battery recycling station to prevent environmental contamination"
],
6: [ # Community Compost Bins
"Neighborhood composting facility for food and garden waste",
"Community compost bins with educational signage",
"Public composting station with separate sections for different stages",
"Shared compost facility managed by local volunteers",
"Urban composting hub turning food waste into valuable soil"
],
7: [ # Solar-Powered Benches
"Solar bench with USB charging ports and WiFi connectivity",
"Public seating with integrated solar panels and device charging",
"Smart bench powered by solar energy with digital information display",
"Solar-powered rest area with phone charging capabilities",
"Eco-friendly bench with solar panels and LED lighting"
],
8: [ # Green Roofs
"Building with extensive green roof system visible from public areas",
"Accessible green roof garden with native plant species",
"Public building showcasing sustainable rooftop vegetation",
"Green roof installation with educational tours available",
"Biodiverse roof garden with insect habitats and rainwater collection"
],
9: [ # Public Water Refill Stations
"Free water refill station to reduce plastic bottle usage",
"Public drinking fountain with bottle filling capability",
"Water refill point with filtered water options",
"Accessible water station encouraging reusable bottles",
"Community water dispenser with usage counter display"
],
10: [ # Waste Oil Collection Points
"Cooking oil recycling point for residential use",
"Used oil collection facility with secure containers",
"Waste oil drop-off point for conversion to biodiesel",
"Community oil recycling station with spill prevention measures",
"Cooking oil collection facility with educational information"
],
11: [ # Book Swap Stations
"Community book exchange point with weatherproof shelving",
"Public book sharing library in repurposed phone box",
"Free book swap station encouraging reading and reuse",
"Neighborhood book exchange with rotating collection",
"Little free library with take-one-leave-one system"
],
12: [ # Pollinator Gardens
"Public garden designed to support bees and butterflies",
"Pollinator-friendly planting area with native flowering species",
"Community garden dedicated to supporting local insect populations",
"Bee-friendly garden with educational signage about pollinators",
"Urban wildflower meadow supporting biodiversity"
],
13: [ # E-Waste Collection Bins
"Secure collection point for electronic waste and small appliances",
"E-waste recycling bin for phones, computers, and small electronics",
"Electronic waste drop-off point with data security assurance",
"Community e-waste collection facility with regular collection schedule",
"Dedicated bin for responsible disposal of electronic items"
],
14: [ # Clothing Donation Bins
"Textile recycling point for clothes and household fabrics",
"Clothing donation bin supporting local charities",
"Secure collection point for reusable clothing and textiles",
"Community clothing recycling bin with regular collection",
"Textile donation point preventing landfill waste"
],
15: [ # Community Tool Libraries
"Tool lending library for community use and sharing",
"Shared equipment facility reducing need for individual ownership",
"Community resource center for borrowing tools and equipment",
"Tool sharing hub with membership system and workshops",
"Public tool library with wide range of equipment available"
],
16: [ # Urban Farms
"Community-run urban farm providing local produce",
"City farming project with volunteer opportunities",
"Urban agriculture site with educational programs",
"Local food growing initiative in repurposed urban space",
"Community garden with vegetable plots and fruit trees"
],
17: [ # Rainwater Harvesting Systems
"Public demonstration of rainwater collection for irrigation",
"Rainwater harvesting system with educational displays",
"Community rainwater collection facility for shared gardens",
"Visible rainwater storage and filtration system",
"Urban water conservation project with storage tanks"
]
}
# Status comments by category
status_comments = {
1: [ # Recycling Bins
"Recently emptied and cleaned",
"Some bins are nearly full",
"All bins in good condition",
"Paper bin is currently full",
"New signage installed to improve sorting"
],
2: [ # e-Scooters
"All scooters fully charged",
"Three scooters currently available",
"Maintenance scheduled for next week",
"New scooters added to this location",
"High usage area, scooters frequently unavailable"
],
3: [ # Bike Share Stations
"All docking stations operational",
"Five bikes currently available",
"Some bikes need maintenance",
"New electric bikes added",
"Popular station with high turnover"
],
4: [ # Public EV Charging Stations
"All charging points operational",
"Fast charger currently under repair",
"Peak usage during business hours",
"New charging point added last month",
"Payment system recently upgraded"
],
5: [ # Battery Recycling Points
"Collection bin recently emptied",
"Secure container in good condition",
"New signage explaining battery types",
"High usage from local businesses",
"Additional capacity added"
],
6: [ # Community Compost Bins
"Compost ready for collection",
"Needs more brown material",
"Recently turned and aerated",
"New bins added to increase capacity",
"Volunteer day scheduled for maintenance"
],
7: [ # Solar-Powered Benches
"All charging ports working",
"Solar panels recently cleaned",
"WiFi currently unavailable",
"LED lights need replacement",
"High usage during lunch hours"
],
8: [ # Green Roofs
"Plants thriving after recent rain",
"Maintenance scheduled next month",
"New species added to increase biodiversity",
"Irrigation system working well",
"Open for public tours on weekends"
],
9: [ # Public Water Refill Stations
"Water quality tested weekly",
"Fountain cleaned daily",
"Bottle filler counter shows high usage",
"New filter installed recently",
"Popular during summer months"
],
10: [ # Waste Oil Collection Points
"Container recently emptied",
"Secure lid in good condition",
"New funnel system installed",
"Collection schedule posted",
"Area kept clean and tidy"
],
11: [ # Book Swap Stations
"Good selection currently available",
"Children's books needed",
"Recently reorganized by volunteers",
"Weatherproof cover working well",
"High turnover of popular titles"
],
12: [ # Pollinator Gardens
"Plants in full bloom",
"Many bees and butterflies observed",
"New native species planted",
"Volunteer day for maintenance scheduled",
"Educational tours available"
],
13: [ # E-Waste Collection Bins
"Bin recently emptied",
"Secure deposit system working",
"Collection schedule posted",
"New items accepted now include small appliances",
"Data destruction guaranteed"
],
14: [ # Clothing Donation Bins
"Bin recently emptied",
"Clean and well-maintained",
"High quality donations appreciated",
"Winter clothing especially needed",
"Please bag items before donating"
],
15: [ # Community Tool Libraries
"New inventory system implemented",
"Popular tools often unavailable on weekends",
"Tool maintenance workshop scheduled",
"New donations recently added to collection",
"Extended hours during summer"
],
16: [ # Urban Farms
"Seasonal produce currently available",
"Volunteer opportunities posted",
"Educational workshops on weekends",
"New growing area being developed",
"Composting system recently expanded"
],
17: [ # Rainwater Harvesting Systems
"System working efficiently after recent rainfall",
"Water quality monitoring in place",
"Educational tours available by appointment",
"System capacity recently expanded",
"Used for irrigation of nearby community garden"
]
}
# Generate a realistic UK postcode based on area code
def generate_postcode(area_code):
# Format: Area + District + Space + Sector + Unit
# e.g., M1 1AA or SW1A 1AA
district = random.randint(1, 99)
sector = random.randint(1, 9)
unit = ''.join(random.choices('ABCDEFGHJKLMNPQRSTUVWXYZ', k=2)) # Excluding I and O as they're not used
if len(area_code) == 1:
return f"{area_code}{district} {sector}{unit}"
else:
return f"{area_code}{district} {sector}{unit}"
# Generate a realistic street name
def generate_street_name():
prefix = random.choice(street_prefixes)
suffix = random.choice(street_suffixes)
return f"{prefix} {suffix}"
# Generate a realistic house number
def generate_house_number():
# 80% chance of a simple number, 20% chance of a letter suffix or unit
if random.random() < 0.8:
return str(random.randint(1, 200))
else:
options = [
f"{random.randint(1, 200)}{random.choice('ABCDEFG')}", # e.g., 42A
f"Unit {random.randint(1, 20)}",
f"Flat {random.randint(1, 50)}",
f"Suite {random.randint(1, 10)}"
]
return random.choice(options)
# Add small random variation to coordinates to avoid facilities at exact same location
def vary_coordinates(lat, lng):
# Add variation of up to ~500 meters
lat_variation = random.uniform(-0.004, 0.004)
lng_variation = random.uniform(-0.006, 0.006)
return lat + lat_variation, lng + lng_variation
# Generate facility title based on category and location
def generate_title(category_name, location_name, street_name):
templates = [
f"{location_name} {category_name}",
f"{category_name} at {street_name}",
f"{street_name} {category_name}",
f"Community {category_name} {location_name}",
f"{location_name} Central {category_name}",
f"{location_name} {street_name} {category_name}"
]
return random.choice(templates)
# Create a log file to track progress
log_file = open("facility_generation_log.txt", "w")
log_file.write(f"Starting facility generation at {datetime.now()}\n")
log_file.write(f"Target: 1000 new facilities\n\n")
# Create a CSV file to store all generated facilities for reference
csv_file = open("generated_facilities.csv", "w", newline='')
csv_writer = csv.writer(csv_file)
csv_writer.writerow(["ID", "Title", "Category", "Description", "Address", "Postcode", "Latitude", "Longitude", "Contributor"])
# Prepare for batch insertion to improve performance
facilities_to_insert = []
status_comments_to_insert = []
# Track unique titles to avoid duplicates
existing_titles = set()
cursor.execute("SELECT title FROM ecoFacilities")
for row in cursor.fetchall():
existing_titles.add(row[0])
# Generate 1000 facilities
num_facilities = 1000
facilities_created = 0
log_file.write("Generating facilities...\n")
while facilities_created < num_facilities:
# Select a random location
location = random.choice(uk_locations)
location_name, county, base_lat, base_lng, postcode_area = location
# Generate 5-25 facilities per location to create clusters
facilities_per_location = min(random.randint(5, 25), num_facilities - facilities_created)
for _ in range(facilities_per_location):
# Select a random category
category_id = random.choice(list(categories.keys()))
category_name = categories[category_id]
# Generate address components
street_name = generate_street_name()
house_number = generate_house_number()
lat, lng = vary_coordinates(base_lat, base_lng)
postcode = generate_postcode(postcode_area)
# Generate title
title_base = generate_title(category_name, location_name, street_name)
title = title_base
# Ensure title is unique by adding a suffix if needed
suffix = 2
while title in existing_titles:
title = f"{title_base} {suffix}"
suffix += 1
existing_titles.add(title)
# Select description
description = random.choice(category_descriptions[category_id])
# Select contributor
contributor_id = random.choice(user_ids)
# Add to batch for insertion
facilities_to_insert.append((
title,
category_id,
description,
house_number,
street_name,
county,
location_name,
postcode,
lng,
lat,
contributor_id
))
# Log progress periodically
facilities_created += 1
if facilities_created % 100 == 0:
log_message = f"Generated {facilities_created} facilities so far..."
print(log_message)
log_file.write(log_message + "\n")
if facilities_created >= num_facilities:
break
# Insert facilities in batches for better performance
log_file.write("\nInserting facilities into database...\n")
print("Inserting facilities into database...")
batch_size = 50
for i in range(0, len(facilities_to_insert), batch_size):
batch = facilities_to_insert[i:i+batch_size]
cursor.executemany("""
INSERT INTO ecoFacilities
(title, category, description, houseNumber, streetName, county, town, postcode, lng, lat, contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", batch)
# Get the IDs of the inserted facilities
cursor.execute("SELECT last_insert_rowid()")
last_id = cursor.fetchone()[0]
first_id_in_batch = last_id - len(batch) + 1
# Generate status comments for each facility
for j, facility in enumerate(batch):
facility_id = first_id_in_batch + j
category_id = facility[1] # Category ID is the second element
# Write to CSV for reference
csv_writer.writerow([
facility_id,
facility[0], # title
categories[category_id], # category name
facility[2], # description
f"{facility[3]} {facility[4]}, {facility[6]}, {facility[5]}", # address
facility[7], # postcode
facility[9], # lat
facility[8], # lng
facility[10] # contributor
])
# Decide how many status comments to add (0-3)
num_comments = random.choices([0, 1, 2, 3], weights=[30, 40, 20, 10])[0]
if num_comments > 0:
# Get relevant comments for this category
relevant_comments = status_comments.get(category_id, status_comments[1]) # Default to recycling bin comments
# Select random comments without repetition
selected_comments = random.sample(relevant_comments, min(num_comments, len(relevant_comments)))
# Add to batch for insertion
for comment in selected_comments:
status_comments_to_insert.append((facility_id, comment))
# Commit after each batch
conn.commit()
log_message = f"Inserted batch {i//batch_size + 1}/{(len(facilities_to_insert)-1)//batch_size + 1}"
print(log_message)
log_file.write(log_message + "\n")
# Insert status comments in batches
if status_comments_to_insert:
log_file.write("\nInserting status comments...\n")
print("Inserting status comments...")
for i in range(0, len(status_comments_to_insert), batch_size):
batch = status_comments_to_insert[i:i+batch_size]
cursor.executemany("""
INSERT INTO ecoFacilityStatus (facilityId, statusComment)
VALUES (?, ?)
""", batch)
conn.commit()
log_message = f"Inserted comment batch {i//batch_size + 1}/{(len(status_comments_to_insert)-1)//batch_size + 1}"
print(log_message)
log_file.write(log_message + "\n")
# Get final counts
cursor.execute("SELECT COUNT(*) FROM ecoFacilities")
total_facilities = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM ecoFacilityStatus")
total_comments = cursor.fetchone()[0]
# Log completion
completion_message = f"\nGeneration complete at {datetime.now()}"
print(completion_message)
log_file.write(completion_message + "\n")
summary = f"Total facilities in database: {total_facilities}\n"
summary += f"Total status comments in database: {total_comments}\n"
summary += f"Generated facilities saved to generated_facilities.csv for reference"
print(summary)
log_file.write(summary)
# Close connections
log_file.close()
csv_file.close()
conn.commit()
conn.close()
print("\nSuccessfully added 1000 new ecological facilities to the database.")
print("A detailed log and CSV export have been created for reference.")

View File

@@ -0,0 +1,79 @@
<?php
// Connect to the SQLite database
$db = new PDO('sqlite:Databases/ecobuddy.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// List of real first names for usernames
$firstNames = [
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles',
'Christopher', 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Paul', 'Andrew', 'Joshua',
'Kenneth', 'Kevin', 'Brian', 'George', 'Timothy', 'Ronald', 'Edward', 'Jason', 'Jeffrey', 'Ryan',
'Jacob', 'Gary', 'Nicholas', 'Eric', 'Jonathan', 'Stephen', 'Larry', 'Justin', 'Scott', 'Brandon',
'Benjamin', 'Samuel', 'Gregory', 'Alexander', 'Frank', 'Patrick', 'Raymond', 'Jack', 'Dennis', 'Jerry',
'Tyler', 'Aaron', 'Jose', 'Adam', 'Nathan', 'Henry', 'Douglas', 'Zachary', 'Peter', 'Kyle',
'Ethan', 'Walter', 'Noah', 'Jeremy', 'Christian', 'Keith', 'Roger', 'Terry', 'Gerald', 'Harold',
'Sean', 'Austin', 'Carl', 'Arthur', 'Lawrence', 'Dylan', 'Jesse', 'Jordan', 'Bryan', 'Billy',
'Joe', 'Bruce', 'Gabriel', 'Logan', 'Albert', 'Willie', 'Alan', 'Juan', 'Wayne', 'Elijah',
'Randy', 'Roy', 'Vincent', 'Ralph', 'Eugene', 'Russell', 'Bobby', 'Mason', 'Philip', 'Louis'
];
// List of common words for password generation
$words = [
'Apple', 'Banana', 'Cherry', 'Dragon', 'Eagle', 'Forest', 'Garden', 'Harbor', 'Island', 'Jungle',
'Kingdom', 'Lemon', 'Mountain', 'Nature', 'Ocean', 'Planet', 'Queen', 'River', 'Summer', 'Tiger',
'Universe', 'Volcano', 'Winter', 'Yellow', 'Zebra', 'Castle', 'Diamond', 'Emerald', 'Flower', 'Galaxy',
'Horizon', 'Iceberg', 'Journey', 'Knight', 'Legend', 'Meadow', 'Nebula', 'Oasis', 'Palace', 'Quasar',
'Rainbow', 'Sapphire', 'Thunder', 'Unicorn', 'Victory', 'Whisper', 'Xylophone', 'Yacht', 'Zephyr', 'Autumn',
'Breeze', 'Cascade', 'Dolphin', 'Eclipse', 'Falcon', 'Glacier', 'Harmony', 'Infinity', 'Jasmine', 'Kaleidoscope',
'Lighthouse', 'Mirage', 'Nightfall', 'Orchard', 'Phoenix', 'Quicksilver', 'Radiance', 'Serenity', 'Twilight', 'Umbrella'
];
// Check if we already have users with these names
$existingUsers = [];
$stmt = $db->query("SELECT username FROM ecoUser");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$existingUsers[] = $row['username'];
}
// Prepare the SQL statement for inserting users
$insertStmt = $db->prepare("INSERT INTO ecoUser (username, password, userType) VALUES (?, ?, ?)");
// Create a file to store usernames and passwords
$file = fopen("user_credentials.txt", "w");
fwrite($file, "Username,Password\n");
// Generate 50 users
$usersAdded = 0;
$usedNames = [];
shuffle($firstNames);
foreach ($firstNames as $name) {
if ($usersAdded >= 50) break;
// Skip if the name is already in the database
if (in_array($name, $existingUsers) || in_array($name, $usedNames)) {
continue;
}
// Generate password in format (Word)(Word)(Word)(Digit)
$password = $words[array_rand($words)] . $words[array_rand($words)] . $words[array_rand($words)] . rand(0, 9);
// Hash the password using SHA-256
$hashedPassword = hash('sha256', $password);
// Insert the user into the database (userType 2 is for standard users)
$insertStmt->execute([$name, $hashedPassword, 2]);
// Write the credentials to the file
fwrite($file, "$name,$password\n");
$usedNames[] = $name;
$usersAdded++;
echo "Added user: $name\n";
}
fclose($file);
echo "\nSuccessfully added $usersAdded users to the database.\n";
echo "Usernames and passwords have been saved to user_credentials.txt\n";

View File

@@ -0,0 +1,79 @@
import sqlite3
import random
import hashlib
import os
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# List of real first names for usernames
first_names = [
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles',
'Christopher', 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Paul', 'Andrew', 'Joshua',
'Kenneth', 'Kevin', 'Brian', 'George', 'Timothy', 'Ronald', 'Edward', 'Jason', 'Jeffrey', 'Ryan',
'Jacob', 'Gary', 'Nicholas', 'Eric', 'Jonathan', 'Stephen', 'Larry', 'Justin', 'Scott', 'Brandon',
'Benjamin', 'Samuel', 'Gregory', 'Alexander', 'Frank', 'Patrick', 'Raymond', 'Jack', 'Dennis', 'Jerry',
'Tyler', 'Aaron', 'Jose', 'Adam', 'Nathan', 'Henry', 'Douglas', 'Zachary', 'Peter', 'Kyle',
'Ethan', 'Walter', 'Noah', 'Jeremy', 'Christian', 'Keith', 'Roger', 'Terry', 'Gerald', 'Harold',
'Sean', 'Austin', 'Carl', 'Arthur', 'Lawrence', 'Dylan', 'Jesse', 'Jordan', 'Bryan', 'Billy',
'Joe', 'Bruce', 'Gabriel', 'Logan', 'Albert', 'Willie', 'Alan', 'Juan', 'Wayne', 'Elijah',
'Randy', 'Roy', 'Vincent', 'Ralph', 'Eugene', 'Russell', 'Bobby', 'Mason', 'Philip', 'Louis'
]
# List of common words for password generation
words = [
'Apple', 'Banana', 'Cherry', 'Dragon', 'Eagle', 'Forest', 'Garden', 'Harbor', 'Island', 'Jungle',
'Kingdom', 'Lemon', 'Mountain', 'Nature', 'Ocean', 'Planet', 'Queen', 'River', 'Summer', 'Tiger',
'Universe', 'Volcano', 'Winter', 'Yellow', 'Zebra', 'Castle', 'Diamond', 'Emerald', 'Flower', 'Galaxy',
'Horizon', 'Iceberg', 'Journey', 'Knight', 'Legend', 'Meadow', 'Nebula', 'Oasis', 'Palace', 'Quasar',
'Rainbow', 'Sapphire', 'Thunder', 'Unicorn', 'Victory', 'Whisper', 'Xylophone', 'Yacht', 'Zephyr', 'Autumn',
'Breeze', 'Cascade', 'Dolphin', 'Eclipse', 'Falcon', 'Glacier', 'Harmony', 'Infinity', 'Jasmine', 'Kaleidoscope',
'Lighthouse', 'Mirage', 'Nightfall', 'Orchard', 'Phoenix', 'Quicksilver', 'Radiance', 'Serenity', 'Twilight', 'Umbrella'
]
# Check if we already have users with these names
cursor.execute("SELECT username FROM ecoUser")
existing_users = [row[0] for row in cursor.fetchall()]
# Create a file to store usernames and passwords
with open("user_credentials.txt", "w") as file:
file.write("Username,Password\n")
# Generate 50 users
users_added = 0
used_names = []
random.shuffle(first_names)
for name in first_names:
if users_added >= 50:
break
# Skip if the name is already in the database
if name in existing_users or name in used_names:
continue
# Generate password in format (Word)(Word)(Word)(Digit)
password = random.choice(words) + random.choice(words) + random.choice(words) + str(random.randint(0, 9))
# Hash the password using SHA-256
hashed_password = hashlib.sha256(password.encode()).hexdigest()
# Insert the user into the database (userType 2 is for standard users)
cursor.execute("INSERT INTO ecoUser (username, password, userType) VALUES (?, ?, ?)",
(name, hashed_password, 2))
# Write the credentials to the file
file.write(f"{name},{password}\n")
used_names.append(name)
users_added += 1
print(f"Added user: {name}")
# Commit the changes and close the connection
conn.commit()
conn.close()
print(f"\nSuccessfully added {users_added} users to the database.")
print("Usernames and passwords have been saved to user_credentials.txt")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
Username,Password
Dylan,MirageAutumnUmbrella7
Lawrence,MirageVictoryKingdom5
Bryan,EmeraldOrchardLegend3
Alan,ThunderOceanDiamond2
Frank,XylophoneVictoryHarmony3
Logan,NatureSerenityXylophone5
Jerry,LighthousePhoenixYellow8
Harold,IcebergGlacierTiger8
Keith,YachtJourneyGarden4
Arthur,VictoryOrchardFlower0
Louis,FlowerNebulaNature3
Paul,LegendLegendYacht4
Aaron,RiverCascadeApple3
George,MeadowBreezePalace4
Carl,RadianceMirageQuasar2
Kenneth,IcebergPlanetHorizon8
Daniel,BreezeXylophoneGalaxy5
Ronald,PhoenixThunderZephyr2
Benjamin,NatureVolcanoNebula8
Joe,UnicornSapphireHorizon1
Larry,IslandZebraApple6
Zachary,SapphireUnicornJasmine0
Willie,UmbrellaVictoryHorizon8
Anthony,QueenZebraNebula9
Michael,TigerMountainNightfall8
Vincent,InfinityHorizonQuicksilver8
Roger,WinterXylophonePalace8
Kyle,IcebergDolphinDragon9
Henry,HarmonyKnightPalace7
Eugene,JourneyThunderPalace0
Billy,ThunderThunderQuicksilver9
Peter,OrchardJasmineVictory4
Christopher,QueenThunderAutumn5
Adam,SummerUnicornThunder7
Nathan,SerenityOrchardThunder2
Edward,MeadowGalaxyYellow6
Eric,DolphinNebulaYacht8
Brian,CastleFlowerGlacier4
Alexander,ForestSapphireZebra4
Andrew,GalaxyLemonApple0
Brandon,HarmonyTigerHarmony2
Russell,CherryZebraQuicksilver8
Jack,OrchardZephyrSapphire8
Jose,BananaJungleSerenity8
Jacob,KaleidoscopeEmeraldJasmine5
Dennis,KnightFlowerRainbow2
Donald,WhisperQuicksilverCastle1
William,ApplePalaceSummer6
Patrick,CastleInfinityPhoenix9
Timothy,YellowEagleSummer0

Binary file not shown.

112
Models/AuthExample.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
/**
* Example controller showing how to use the simplified authentication
*
* This file demonstrates how to use the User::checkAuth() and User::checkAdmin()
* methods to protect routes without using middleware.
*/
require_once('Models/User.php');
/**
* Example of a protected endpoint that requires authentication
*/
function protectedEndpoint() {
// Check if user is authenticated
$auth = User::checkAuth();
if (!$auth) {
// The checkAuth method already sent the error response
return;
}
// User is authenticated, proceed with the endpoint logic
$response = [
'status' => 'success',
'message' => 'You are authenticated',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username']
]
];
// Send response
header('Content-Type: application/json');
echo json_encode($response);
}
/**
* Example of an admin-only endpoint
*/
function adminEndpoint() {
// Check if user is an admin
$auth = User::checkAdmin();
if (!$auth) {
// The checkAdmin method already sent the error response
return;
}
// User is an admin, proceed with the admin-only logic
$response = [
'status' => 'success',
'message' => 'You have admin access',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username']
]
];
// Send response
header('Content-Type: application/json');
echo json_encode($response);
}
/**
* Example of a public endpoint that doesn't require authentication
* but can still use authentication data if available
*/
function publicEndpoint() {
// Check if user is authenticated, but don't require it
$auth = User::checkAuth(false);
$response = [
'status' => 'success',
'message' => 'This is a public endpoint'
];
// Add user info if authenticated
if ($auth) {
$response['user'] = [
'id' => $auth['uid'],
'username' => $auth['username']
];
} else {
$response['user'] = 'Guest';
}
// Send response
header('Content-Type: application/json');
echo json_encode($response);
}
/**
* Example of how to use these functions in a simple router
*/
function handleRequest() {
$route = $_GET['route'] ?? 'public';
switch ($route) {
case 'protected':
protectedEndpoint();
break;
case 'admin':
adminEndpoint();
break;
case 'public':
default:
publicEndpoint();
break;
}
}
// Call the router function
handleRequest();

199
Models/AuthService.php Normal file
View File

@@ -0,0 +1,199 @@
<?php
require_once('UserDataSet.php');
/**
* Authentication service for handling JWT-based authentication
*/
class AuthService {
private string $secretKey;
private int $tokenExpiry;
/**
* Initialises the authentication service
* Loads configuration from environment variables
* @throws Exception if OpenSSL extension is not loaded
*/
public function __construct() {
// Load environment variables from .env file
$envFile = __DIR__ . '/../.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// Skip comments
if (strpos($line, '#') === 0) continue;
// Parse environment variable
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!empty($name)) {
putenv(sprintf('%s=%s', $name, $value));
}
}
}
// Set configuration from environment variables with defaults
$this->secretKey = getenv('JWT_SECRET_KEY') ?: 'your-256-bit-secret';
$this->tokenExpiry = (int)(getenv('JWT_TOKEN_EXPIRY') ?: 3600);
// Verify OpenSSL extension is available
if (!extension_loaded('openssl')) {
throw new Exception('OpenSSL extension is required for JWT');
}
}
/**
* Generates a JWT token for a user
* @param array $userData User information to include in token
* @return string The generated JWT token
*/
public function generateToken(array $userData): string {
$issuedAt = time();
$expire = $issuedAt + $this->tokenExpiry;
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
'uid' => $userData['id'],
'username' => $userData['username'],
'accessLevel' => $userData['userType']
];
return $this->encodeJWT($payload);
}
/**
* Validates a JWT token
* @param string $token The JWT token to validate
* @return array|null The decoded payload if valid, null otherwise
*/
public function validateToken(string $token): ?array {
try {
$payload = $this->decodeJWT($token);
// Check if token is expired
if ($payload === null || !isset($payload['exp']) || $payload['exp'] < time()) {
return null;
}
return $payload;
} catch (Exception $e) {
return null;
}
}
/**
* Encodes data into a JWT token
* @param array $payload The data to encode
* @return string The encoded JWT token
*/
private function encodeJWT(array $payload): string {
// Create and encode header
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
$header = $this->base64UrlEncode($header);
// Create and encode payload
$payload = json_encode($payload);
$payload = $this->base64UrlEncode($payload);
// Create and encode signature
$signature = hash_hmac('sha256', "$header.$payload", $this->secretKey, true);
$signature = $this->base64UrlEncode($signature);
return "$header.$payload.$signature";
}
/**
* Decodes a JWT token
* @param string $token The JWT token to decode
* @return array|null The decoded payload if valid, null otherwise
*/
private function decodeJWT(string $token): ?array {
// Split token into components
$parts = explode('.', $token);
if (count($parts) !== 3) {
return null;
}
[$header, $payload, $signature] = $parts;
// Verify signature
$validSignature = $this->base64UrlEncode(
hash_hmac('sha256', "$header.$payload", $this->secretKey, true)
);
if ($signature !== $validSignature) {
return null;
}
// Decode and return payload
return json_decode($this->base64UrlDecode($payload), true);
}
/**
* Encodes data using base64url encoding
* @param string $data The data to encode
* @return string The encoded data
*/
private function base64UrlEncode(string $data): string {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* Decodes base64url encoded data
* @param string $data The data to decode
* @return string The decoded data
*/
private function base64UrlDecode(string $data): string {
return base64_decode(strtr($data, '-_', '+/') . str_repeat('=', 3 - (3 + strlen($data)) % 4));
}
/**
* Generates a refresh token for a user
* @param array $userData User information to include in token
* @return string The generated refresh token
*/
public function generateRefreshToken(array $userData): string {
$issuedAt = time();
$expire = $issuedAt + ($this->tokenExpiry * 24); // Refresh token lasts 24 times longer than access token
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
'uid' => $userData['id'],
'username' => $userData['username'],
'type' => 'refresh'
];
return $this->encodeJWT($payload);
}
/**
* Refreshes an access token using a refresh token
* @param string $refreshToken The refresh token
* @return string|null The new access token if valid, null otherwise
*/
public function refreshToken(string $refreshToken): ?string {
try {
$payload = $this->decodeJWT($refreshToken);
// Check if token is expired or not a refresh token
if ($payload === null || !isset($payload['exp']) || $payload['exp'] < time() ||
!isset($payload['type']) || $payload['type'] !== 'refresh') {
return null;
}
// Generate a new access token
$userData = [
'id' => $payload['uid'],
'username' => $payload['username'],
'userType' => isset($payload['accessLevel']) ? $payload['accessLevel'] : 0
];
return $this->generateToken($userData);
} catch (Exception $e) {
return null;
}
}
}

70
Models/Database.php Executable file
View File

@@ -0,0 +1,70 @@
<?php
/**
* Database connection handler using Singleton pattern
*/
class Database {
/**
* @var Database|null The singleton instance
*/
protected static $_dbInstance = null;
/**
* @var PDO The database connection handle
*/
protected $_dbHandle;
/**
* Gets the database connection handle
* @return PDO The database connection
*/
public function getDbConnection(): PDO
{
return $this->_dbHandle;
}
/**
* Gets the singleton instance of the Database class
* @return Database The database instance
*/
public static function getInstance(): ?Database
{
if(self::$_dbInstance == null) {
self::$_dbInstance = new self();
}
return self::$_dbInstance;
}
/**
* Private constructor to prevent direct instantiation
* Initialises the database connection
* @throws PDOException if connection fails
*/
private function __construct() {
try {
// Create PDO connection with error handling
$this->_dbHandle = new PDO("sqlite:Databases/ecobuddy.sqlite");
// Configure PDO for better error handling and performance
$this->_dbHandle->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
$this->_dbHandle->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
// SQLite3 sometimes just forgets foreign keys exist i guess (https://stackoverflow.com/questions/15301643/sqlite3-forgets-to-use-foreign-keys)
$this->_dbHandle->exec('PRAGMA foreign_keys = ON;');
// Set transaction timeout to 5 seconds, just stops the app from hanging when the db is busy
$this->_dbHandle->exec('PRAGMA busy_timeout = 5000;');
}
catch (PDOException $e) {
// Log the error and rethrow
error_log("Database connection error: " . $e->getMessage());
throw $e;
}
}
/**
* Destructor to clean up database connection
*/
public function __destruct() {
$this->_dbHandle = null;
}
}

217
Models/FacilityData.php Executable file
View File

@@ -0,0 +1,217 @@
<?php
/**
* Represents a facility in the EcoBuddy system
*
* This class serves as a data model for facilities, encapsulating all
* the properties and behaviours of a single facility. It follows the
* Data Transfer Object (DTO) pattern that I learned about in my
* software architecture module.
*
* Each facility has location data, descriptive information, and metadata
* about who contributed it. This class provides a clean interface for
* accessing this data throughout the application.
*/
class FacilityData {
/**
* Facility properties
*
* @var int $_id - Unique identifier for the facility
* @var string $_title - Name of the facility
* @var string $_category - Category/type of the facility
* @var string $_status - Current status of the facility
* @var string $_description - Detailed description of the facility
* @var string $_houseNumber - Building number or name
* @var string $_streetName - Street name
* @var string $_county - County
* @var string $_town - Town or city
* @var string $_postcode - Postal code
* @var float $_lng - Longitude coordinate
* @var float $_lat - Latitude coordinate
* @var string $_contributor - Username of the person who added the facility
*/
protected $_id;
protected $_title;
protected $_category;
protected $_status;
protected $_description;
protected $_houseNumber;
protected $_streetName;
protected $_county;
protected $_town;
protected $_postcode;
protected $_lng;
protected $_lat;
protected $_contributor;
/**
* Initialises a new facility with data from the database
* @param array $dbRow Database row containing facility data
*/
public function __construct($dbRow) {
$this->_id = $dbRow['id'];
$this->_title = $dbRow['title'];
$this->_category = $dbRow['category'];
$this->_status = $dbRow['status'];
$this->_description = $dbRow['description'];
$this->_houseNumber = $dbRow['houseNumber'];
$this->_streetName = $dbRow['streetName'];
$this->_county = $dbRow['county'];
$this->_town = $dbRow['town'];
$this->_postcode = $dbRow['postcode'];
$this->_lng = $dbRow['lng'];
$this->_lat = $dbRow['lat'];
$this->_contributor = $dbRow['contributor'];
}
/**
* Gets the facility's unique identifier
*
* This ID is used throughout the application to reference this specific
* facility, particularly in database operations and API requests.
*
* @return int The facility ID
*/
public function getId() {
return $this->_id;
}
/**
* Gets the facility's title
*
* The title is the primary name or label for the facility that
* is displayed to users in the interface.
*
* @return string The facility title
*/
public function getTitle() {
return $this->_title;
}
/**
* Gets the facility's category
*
* The category helps classify facilities by type, such as
* recycling centre, community garden, etc.
*
* @return string The facility category
*/
public function getCategory() {
return $this->_category;
}
/**
* Gets the facility's current status
*
* The status indicates whether the facility is operational,
* under maintenance, closed, etc.
*
* @return string The facility status
*/
public function getStatus() {
return $this->_status;
}
/**
* Gets the facility's description
*
* The description provides detailed information about the facility,
* its purpose, services offered, etc.
*
* @return string The facility description
*/
public function getDescription() {
return $this->_description;
}
/**
* Gets the facility's house/building number
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The house/building number
*/
public function getHouseNumber() {
return $this->_houseNumber;
}
/**
* Gets the facility's street name
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The street name
*/
public function getStreetName() {
return $this->_streetName;
}
/**
* Gets the facility's county
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The county
*/
public function getCounty() {
return $this->_county;
}
/**
* Gets the facility's town or city
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The town or city
*/
public function getTown() {
return $this->_town;
}
/**
* Gets the facility's postcode
*
* This is part of the facility's address and helps locate it physically.
* It's also useful for searching facilities by location.
*
* @return string The postcode
*/
public function getPostcode() {
return $this->_postcode;
}
/**
* Gets the facility's longitude coordinate
*
* This is used for displaying the facility on a map and
* for calculating distances between facilities.
*
* @return float The longitude coordinate
*/
public function getLng() {
return $this->_lng;
}
/**
* Gets the facility's latitude coordinate
*
* This is used for displaying the facility on a map and
* for calculating distances between facilities.
*
* @return float The latitude coordinate
*/
public function getLat() {
return $this->_lat;
}
/**
* Gets the username of the facility's contributor
*
* This tracks who added the facility to the system,
* which is useful for auditing and attribution.
*
* @return string The contributor's username
*/
public function getContributor() {
return $this->_contributor;
}
}

462
Models/FacilityDataSet.php Executable file
View File

@@ -0,0 +1,462 @@
<?php
require_once ('Database.php');
require_once ('FacilityData.php');
class FacilityDataSet
{
protected $_dbHandle, $_dbInstance;
public function __construct()
{
$this->_dbInstance = Database::getInstance();
$this->_dbHandle = $this->_dbInstance->getDbConnection();
}
/**
* @param $id
* @return bool
* Deletes Facility Records being passed a facility id.
*/
public function deleteFacility($id): bool
{
try {
// Start transaction
$this->_dbHandle->beginTransaction();
// Delete related status records first
$statusQuery = "DELETE FROM ecoFacilityStatus WHERE facilityid = :id;";
$statusStmt = $this->_dbHandle->prepare($statusQuery);
$statusStmt->bindValue(':id', (int)$id, \PDO::PARAM_INT);
$statusStmt->execute();
// Delete the facility
$facilityQuery = "DELETE FROM ecoFacilities WHERE id = :id;";
$facilityStmt = $this->_dbHandle->prepare($facilityQuery);
$facilityStmt->bindValue(':id', (int)$id, \PDO::PARAM_INT);
$facilityStmt->execute();
// Commit transaction
$this->_dbHandle->commit();
return $facilityStmt->rowCount() > 0;
} catch (PDOException $e) {
// Rollback on error
$this->_dbHandle->rollBack();
error_log("Error deleting facility: " . $e->getMessage());
return false;
}
}
/**
* @return array|false Returns array of facilities or false on error
* Fetch all facility records with related data
*/
public function fetchAll(): array|false
{
try {
error_log('Starting fetchAll...');
$query = "
SELECT DISTINCT ecoFacilities.id,
ecoFacilities.title,
COALESCE(GROUP_CONCAT(ecoFacilityStatus.statusComment, '; '), '') AS status,
ecoCategories.name AS category,
ecoFacilities.description,
ecoFacilities.houseNumber,
ecoFacilities.streetName,
ecoFacilities.county,
ecoFacilities.town,
ecoFacilities.postcode,
ecoFacilities.lng,
ecoFacilities.lat,
COALESCE(ecoUser.username, 'Unknown') AS contributor
FROM ecoFacilities
LEFT JOIN ecoCategories ON ecoCategories.id = ecoFacilities.category
LEFT JOIN ecoUser ON ecoUser.id = ecoFacilities.contributor
LEFT JOIN ecoFacilityStatus ON ecoFacilityStatus.facilityid = ecoFacilities.id
GROUP BY ecoFacilities.id, ecoFacilities.title, ecoCategories.name,
ecoFacilities.description, ecoFacilities.streetName,
ecoFacilities.county, ecoFacilities.town, ecoFacilities.postcode,
ecoUser.username
ORDER BY ecoFacilities.id ASC;
";
error_log('Preparing query...');
$dataStmt = $this->_dbHandle->prepare($query);
error_log('Executing query...');
$dataStmt->execute();
error_log('Fetching results...');
$results = $dataStmt->fetchAll(PDO::FETCH_ASSOC);
if ($results === false) {
error_log('Query returned false');
return false;
}
error_log('Query successful. Row count: ' . count($results));
return $results;
} catch (PDOException $e) {
error_log("Database error in fetchAll: " . $e->getMessage());
error_log("SQL State: " . $e->getCode());
error_log("Stack trace: " . $e->getTraceAsString());
return false;
} catch (Exception $e) {
error_log("General error in fetchAll: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
return false;
}
}
/**
* Creates a new facility in the database
* @param array $data Facility data
* @return array|false The created facility data or false on failure
*/
public function createFacility($data)
{
try {
$this->_dbHandle->beginTransaction();
// Validate coordinates
if (!is_numeric($data['lng']) || !is_numeric($data['lat']) ||
$data['lng'] < -180 || $data['lng'] > 180 ||
$data['lat'] < -90 || $data['lat'] > 90) {
throw new Exception('Invalid coordinates provided');
}
// Get contributor ID
$contributorId = $this->getContributorId($data['contributor']);
if (!$contributorId) {
throw new Exception('Invalid contributor name');
}
// Get category ID
$categoryId = $this->getCategoryId($data['category']);
if (!$categoryId) {
// If category doesn't exist, create it
$categoryId = $this->createCategory($data['category']);
if (!$categoryId) {
throw new Exception('Failed to create category: ' . $data['category']);
}
}
// Insert facility
$sql = "INSERT INTO ecoFacilities (title, category, description, houseNumber,
streetName, county, town, postcode, lng, lat, contributor)
VALUES (:title, :category, :description, :houseNumber,
:streetName, :county, :town, :postcode, :longitude, :latitude, :contributor)";
$stmt = $this->_dbHandle->prepare($sql);
$params = [
':title' => $data['title'],
':category' => $categoryId,
':description' => $data['description'],
':houseNumber' => $data['houseNumber'],
':streetName' => $data['streetName'],
':county' => $data['county'],
':town' => $data['town'],
':postcode' => $data['postcode'],
':longitude' => $data['lng'],
':latitude' => $data['lat'],
':contributor' => $contributorId
];
error_log("Executing SQL with params: " . print_r($params, true));
if (!$stmt->execute($params)) {
throw new Exception('Failed to insert facility: ' . implode(', ', $stmt->errorInfo()));
}
$facilityId = $this->_dbHandle->lastInsertId();
$this->_dbHandle->commit();
// Return the created facility
return $this->getFacilityById($facilityId);
} catch (Exception $e) {
$this->_dbHandle->rollBack();
error_log("Error in createFacility: " . $e->getMessage());
throw $e;
}
}
private function createCategory($categoryName)
{
try {
$sql = "INSERT INTO ecoCategories (name) VALUES (:name)";
$stmt = $this->_dbHandle->prepare($sql);
$stmt->execute([':name' => $categoryName]);
return $this->_dbHandle->lastInsertId();
} catch (Exception $e) {
error_log("Error creating category: " . $e->getMessage());
return false;
}
}
/**
* Updates an existing facility in the database
* @param int $id Facility ID
* @param array $data Updated facility data
* @return array|false The updated facility data or false on failure
*/
public function updateFacility($id, $data) {
try {
// Start transaction
$this->_dbHandle->beginTransaction();
// Validate coordinates
if (!is_numeric($data['lng']) || !is_numeric($data['lat']) ||
$data['lng'] < -180 || $data['lng'] > 180 ||
$data['lat'] < -90 || $data['lat'] > 90) {
throw new Exception('Invalid coordinates');
}
// Get Contributor ID
$query = "SELECT ecoUser.id FROM ecoUser WHERE ecoUser.username = :contributor;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':contributor', $data['contributor']);
$stmt->execute();
$contributorResult = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$contributorResult) {
throw new Exception('Invalid contributor username');
}
$contributorId = $contributorResult['id'];
// Get Category ID
$query = "SELECT ecoCategories.id FROM ecoCategories WHERE ecoCategories.name = :category;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':category', $data['category']);
$stmt->execute();
$categoryResult = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$categoryResult) {
throw new Exception('Invalid category name');
}
$categoryId = $categoryResult['id'];
// Update facility
$query = "
UPDATE ecoFacilities
SET title = :title,
category = :category,
description = :description,
houseNumber = :houseNumber,
streetName = :streetName,
county = :county,
town = :town,
postcode = :postcode,
lng = :lng,
lat = :lat,
contributor = :contributor
WHERE id = :id
";
$stmt = $this->_dbHandle->prepare($query);
$params = [
':title' => $data['title'],
':category' => $categoryId,
':description' => $data['description'],
':houseNumber' => $data['houseNumber'],
':streetName' => $data['streetName'],
':county' => $data['county'],
':town' => $data['town'],
':postcode' => $data['postcode'],
':lng' => $data['lng'],
':lat' => $data['lat'],
':contributor' => $contributorId,
':id' => $id
];
error_log("Executing update query with params: " . print_r($params, true));
if (!$stmt->execute($params)) {
throw new Exception('Failed to update facility: ' . implode(', ', $stmt->errorInfo()));
}
if ($stmt->rowCount() > 0) {
$this->_dbHandle->commit();
return $this->getFacilityById($id);
}
$this->_dbHandle->rollBack();
return false;
} catch (Exception $e) {
$this->_dbHandle->rollBack();
error_log("Error updating facility: " . $e->getMessage());
return false;
}
}
/**
* Gets a facility by its ID
* @param int $id Facility ID
* @return array|false The facility data or false if not found
*/
public function getFacilityById($id) {
try {
$query = "
SELECT DISTINCT ecoFacilities.id,
ecoFacilities.title,
COALESCE(GROUP_CONCAT(ecoFacilityStatus.statusComment, ';'), '') AS status,
ecoCategories.name AS category,
ecoFacilities.description,
ecoFacilities.houseNumber,
ecoFacilities.streetName,
ecoFacilities.county,
ecoFacilities.town,
ecoFacilities.postcode,
ecoFacilities.lng,
ecoFacilities.lat,
COALESCE(ecoUser.username, 'Unknown') AS contributor
FROM ecoFacilities
LEFT JOIN ecoCategories ON ecoCategories.id = ecoFacilities.category
LEFT JOIN ecoUser ON ecoUser.id = ecoFacilities.contributor
LEFT JOIN ecoFacilityStatus ON ecoFacilityStatus.facilityid = ecoFacilities.id
WHERE ecoFacilities.id = ?
GROUP BY ecoFacilities.id, ecoFacilities.title, ecoCategories.name,
ecoFacilities.description, ecoFacilities.streetName,
ecoFacilities.county, ecoFacilities.town, ecoFacilities.postcode,
ecoUser.username;
";
$stmt = $this->_dbHandle->prepare($query);
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Error getting facility: " . $e->getMessage());
return false;
}
}
private function getContributorId($username)
{
try {
$query = "SELECT ecoUser.id FROM ecoUser WHERE ecoUser.username = :username;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':username', $username);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? $result['id'] : false;
} catch (Exception $e) {
error_log("Error getting contributor ID: " . $e->getMessage());
return false;
}
}
private function getCategoryId($categoryName)
{
try {
$query = "SELECT ecoCategories.id FROM ecoCategories WHERE ecoCategories.name = :name;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':name', $categoryName);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? $result['id'] : false;
} catch (Exception $e) {
error_log("Error getting category ID: " . $e->getMessage());
return false;
}
}
/**
* Adds a new status comment to a facility
* @param int $facilityId The ID of the facility
* @param string $statusComment The status comment to add
* @return bool True if successful, false otherwise
*/
public function addFacilityStatus($facilityId, $statusComment)
{
try {
// Log input parameters
error_log("Adding facility status - Facility ID: " . $facilityId . ", Comment: " . $statusComment);
// Start transaction
$this->_dbHandle->beginTransaction();
// Insert new status comment
$query = "INSERT INTO ecoFacilityStatus (facilityId, statusComment) VALUES (:facilityId, :statusComment)";
$stmt = $this->_dbHandle->prepare($query);
// Log the prepared statement
error_log("Prepared statement: " . $query);
// Bind values and log them
$stmt->bindValue(':facilityId', (int)$facilityId, PDO::PARAM_INT);
$stmt->bindValue(':statusComment', $statusComment);
error_log("Bound values - Facility ID: " . (int)$facilityId . ", Comment: " . $statusComment);
if (!$stmt->execute()) {
$errorInfo = $stmt->errorInfo();
error_log("SQL Error: " . print_r($errorInfo, true));
throw new Exception('Failed to insert status comment: ' . implode(', ', $errorInfo));
}
$this->_dbHandle->commit();
error_log("Successfully added facility status");
return true;
} catch (Exception $e) {
$this->_dbHandle->rollBack();
error_log("Error adding facility status: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
return false;
}
}
/**
* Gets all status comments for a facility
* @param int $facilityId The ID of the facility
* @return array Array of status comments with their IDs
*/
public function getFacilityStatuses($facilityId)
{
try {
$query = "SELECT id, statusComment FROM ecoFacilityStatus WHERE facilityId = :facilityId ORDER BY id DESC";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':facilityId', (int)$facilityId, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("Error getting facility statuses: " . $e->getMessage());
return [];
}
}
/**
* Updates an existing status comment
* @param int $statusId The ID of the status comment
* @param string $statusComment The updated status comment
* @return bool True if successful, false otherwise
*/
public function updateFacilityStatus($statusId, $statusComment)
{
try {
$query = "UPDATE ecoFacilityStatus SET statusComment = :statusComment WHERE id = :statusId";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':statusId', (int)$statusId, PDO::PARAM_INT);
$stmt->bindValue(':statusComment', $statusComment);
return $stmt->execute();
} catch (Exception $e) {
error_log("Error updating facility status: " . $e->getMessage());
return false;
}
}
/**
* Deletes a specific status comment
* @param int $statusId The ID of the status comment to delete
* @return bool True if successful, false otherwise
*/
public function deleteFacilityStatus($statusId)
{
try {
$query = "DELETE FROM ecoFacilityStatus WHERE id = :statusId";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':statusId', (int)$statusId, PDO::PARAM_INT);
return $stmt->execute();
} catch (Exception $e) {
error_log("Error deleting facility status: " . $e->getMessage());
return false;
}
}
}

57
Models/Paginator.php Executable file
View File

@@ -0,0 +1,57 @@
<?php
require_once('FacilityDataSet.php');
class Paginator {
protected $_pages, $_totalPages, $_rowLimit, $_pageMatrix, $_rowCount;
public function __construct($rowLimit, $dataset) {
$this->_rowLimit = $rowLimit;
$this->_totalPages = $this->calculateTotalPages($dataset['count']);
$this->_rowCount = $dataset['count'];
$this->_pages = $dataset['dataset'];
$this->_pageMatrix = $this->Paginate();
}
public function getTotalPages() {
return $this->_totalPages;
}
private function calculateTotalPages(int $count): int {
return $count > 0 ? ceil($count / $this->_rowLimit) : 0;
}
public function Paginate(): array {
$pageMatrix = [];
for ($i = 0; $i < $this->_totalPages; $i++) {
$page = [];
$start = $i * $this->_rowLimit;
$end = min($start + $this->_rowLimit, $this->_rowCount); // Ensure within bounds
for ($j = $start; $j < $end; $j++) {
$page[] = $this->_pages[$j];
}
$pageMatrix[$i] = $page;
}
return $pageMatrix;
}
public function getPageFromUri(): int {
// Retrieve 'page' parameter and default to 0 if missing or invalid
return filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
'options' => ['default' => 0, 'min_range' => 0] // Default to 1 if invalid or missing
]);
}
public function getPage(int $pageNumber): array {
if ($pageNumber < 0 || $pageNumber >= $this->_totalPages) {
return []; // Return an empty array if the page number is invalid
}
return $this->_pageMatrix[$pageNumber];
}
public function countPageResults(int $pageNumber): int {
if ($pageNumber < 0 || $pageNumber >= $this->_totalPages) {
return 0; // Return 0 if the page number is invalid
}
return count($this->_pageMatrix[$pageNumber]);
}
}

219
Models/User.php Executable file
View File

@@ -0,0 +1,219 @@
<?php
require_once('UserDataSet.php');
require_once('AuthService.php');
/**
* User class - Handles user authentication and session management
*
* This class manages user authentication using JWT tokens and provides
* methods for logging in, logging out, and checking user permissions.
* I've implemented this based on JWT authentication
*/
class User {
/**
* Class properties
* @var string $_username - The user's username
* @var bool $_loggedIn - Whether the user is currently logged in
* @var string $_userId - The user's unique ID
* @var int $_accessLevel - The user's access level (admin = 1, regular user = 2)
* @var AuthService $_authService - Service for JWT token handling
*/
protected $_username, $_loggedIn, $_userId, $_accessLevel;
protected $_authService;
/**
* Gets the current user's username
*
* @return string The username of the current user
*/
public function getUsername() {
return $this->_username;
}
/**
* Gets the current user's ID
*
* @return string The ID of the current user
*/
public function getUserId() {
return $this->_userId;
}
/**
* Constructor - Initialises user from JWT token if available
*
* Checks for a JWT token in the Authorization header and validates it.
* If valid, sets user properties based on the token payload.
* Also starts a session if needed for CAPTCHA verification during registration.
*/
public function __construct() {
// Initialise default values
$this->_username = "None";
$this->_loggedIn = false;
$this->_userId = "0";
$this->_accessLevel = null;
$this->_authService = new AuthService();
// Check for JWT token in Authorization header
$headers = getallheaders();
$token = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : null;
// Validate token if it exists
if ($token) {
$payload = $this->_authService->validateToken($token);
if ($payload) {
$this->_username = $payload['username'];
$this->_userId = $payload['uid'];
$this->_accessLevel = $payload['accessLevel'];
$this->_loggedIn = true;
}
}
// Start session only if needed for CAPTCHA
if (session_status() === PHP_SESSION_NONE && isset($_GET['page']) && $_GET['page'] === 'register') {
session_start();
}
}
/**
* Sets the user's access level
*
* @param int $level The access level to set (admin = 1, regular user = 2)
* @return void
*/
private function setAccessLevel($level) {
$this->_accessLevel = $level;
}
/**
* Gets the user's access level
*
* @return int|null The user's access level (admin = 1, regular user = 2) or null if not set
*/
public function getAccessLevel() {
return $this->_accessLevel;
}
/**
* Authenticates a user using username and password
*
* Checks credentials against the database and generates a JWT token if valid.
* Sets user properties if authentication is successful.
*
* @param string $username The username to authenticate
* @param string $password The password to verify
* @return string|bool JWT token if authentication was successful, false otherwise
*/
public function Authenticate($username, $password)
{
$users = new UserDataSet();
$userDataSet = $users->checkUserCredentials($username, $password);
if(count($userDataSet) > 0) {
$userData = $userDataSet[0];
$accessLevel = $users->checkAccessLevel($username);
// Generate JWT token
$token = $this->_authService->generateToken([
'id' => $userData->getId(),
'username' => $userData->getUsername(),
'userType' => $accessLevel
]);
// Set user properties
$this->_loggedIn = true;
$this->_username = $username;
$this->_userId = $userData->getId();
$this->_accessLevel = $accessLevel;
return $token;
}
else {
$this->_loggedIn = false;
return false;
}
}
/**
* Logs the user out
*
* Resets all user properties to their default values.
* Note: This doesn't invalidate the JWT token - handled client-side
* by removing the token from storage.
*
* @return void
*/
public function logout() {
// Reset user properties
$this->_loggedIn = false;
$this->_username = "None";
$this->_userId = "0";
$this->_accessLevel = null;
}
/**
* Checks if the user is currently logged in
*
* @return bool True if the user is logged in, false otherwise
*/
public function isLoggedIn(): bool
{
return $this->_loggedIn;
}
/**
* Static method to check if a request is authenticated
*
* This method can be called from any controller to check if the request
* has a valid JWT token. It returns the payload if authenticated or
* sends an error response and returns false if not.
*
* @param bool $required Whether authentication is required (defaults to true)
* @return array|false The payload if authenticated, false otherwise
*/
public static function checkAuth(bool $required = true)
{
$authService = new AuthService();
// Get the token from the Authorization header
$headers = getallheaders();
$token = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : null;
// Validate the token
$payload = $token ? $authService->validateToken($token) : null;
// If authentication is required and no valid token, return error
if ($required && !$payload) {
header('Content-Type: application/json');
http_response_code(401);
echo json_encode(['error' => 'Authentication required']);
return false;
}
return $payload;
}
/**
* Static method to check if a request is from an admin
*
* This method can be called from any controller to check if the request
* has a valid JWT token with admin access level. It returns the payload
* if authenticated as admin or sends an error response and returns false if not.
*
* @return array|false The payload if authenticated as admin, false otherwise
*/
public static function checkAdmin()
{
$payload = self::checkAuth(true);
if ($payload && isset($payload['accessLevel']) && $payload['accessLevel'] == 1) {
return $payload;
}
header('Content-Type: application/json');
http_response_code(403);
echo json_encode(['error' => 'Admin access required']);
return false;
}
}

20
Models/UserData.php Executable file
View File

@@ -0,0 +1,20 @@
<?php
class UserData {
protected $_id, $_username, $_name, $_password, $_usertype;
public function __construct($dbRow) {
$this->_id = $dbRow['id'];
$this->_username = $dbRow['username'];
$this->_password = $dbRow['password'];
$this->_usertype = $dbRow['userType'];
}
public function getId() {
return $this->_id;
}
public function getUsername() {
return $this->_username;
}
}

47
Models/UserDataSet.php Executable file
View File

@@ -0,0 +1,47 @@
<?php
require_once ('Database.php');
require_once ('UserData.php');
class UserDataSet {
protected $_dbHandle, $_dbInstance;
public function __construct() {
$this->_dbInstance = Database::getInstance();
$this->_dbHandle = $this->_dbInstance->getDbConnection();
}
/**
* @param $username
* @return mixed
* Query access level of a username, and return their usertype
*/
public function checkAccessLevel($username) {
$sqlQuery = "SELECT ecoUser.userType FROM ecoUser
LEFT JOIN ecoUsertypes ON ecoUser.userType = ecoUsertypes.userType
WHERE ecoUser.username = ?";
$statement = $this->_dbHandle->prepare($sqlQuery);
$statement->bindValue(1, $username);
$statement->execute();
return $statement->fetch(PDO::FETCH_ASSOC)['userType'];
}
/**
* @param $username
* @param $password
* @return array
* Authenticate user with query, and return their details
*/
public function checkUserCredentials($username, $password): array
{
$sqlQuery = 'SELECT * FROM ecoUser WHERE username = ? AND password = ?;';
$statement = $this->_dbHandle->prepare($sqlQuery);
$statement->bindParam(1, $username);
$statement->bindParam(2, $password);
$statement->execute();
$dataSet = [];
while ($row = $statement->fetch()) {
$dataSet[] = new UserData($row);
}
return $dataSet;
}
}

View File

109
Views/index.phtml Normal file → Executable file
View File

@@ -1,11 +1,104 @@
<?php require('template/header.phtml') ?>
<?php
/**
* Main index view for the EcoBuddy application
*
* This file serves as the main view for the application, displaying
* a table of facilities with various actions depending on the user's
* access level. It includes modals for creating, updating, deleting,
* and viewing statuses of facilities.
*
* The table is populated dynamically using JavaScript, with the data
* stored in sessionStorage.
*/
require('template/header.phtml')
?>
<h3>Welcome to the web-site </h3>
<div class="row">
<div class="col-12 p-0" id="facilityContent">
<!-- Main content -->
<div class="card shadow-sm border-0 rounded-3">
<!-- Title and add button (admins only) -->
<div class="card-header bg-light py-3">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<h5 class="mb-0 fw-bold text-primary">
<i class="bi bi-geo-alt-fill me-2 text-success"></i>Facilities
</h5>
<!-- Badge showing the number of facilities -->
<span class="badge bg-success rounded-pill ms-2" id="facilityCount"></span>
</div>
<?php if($view->user->getAccessLevel() == 1): ?>
<!-- Add new facility button (admin only) -->
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#createModal">
<i class="bi bi-plus-circle me-1"></i>Add New Facility
</button>
<?php endif; ?>
</div>
</div>
<!-- Pagination controls -->
<div class="card-footer bg-white py-2">
<?php require('template/pagination.phtml');?>
</div>
<!-- Facilities table -->
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="facilityTable">
<thead class="table-light">
<tr>
<?php if($view->user->getAccessLevel() == 1): ?>
<th class="fw-semibold" style="width: 40px;">ID</th>
<?php else: ?>
<th class="d-none">ID</th>
<?php endif; ?>
<th class="fw-semibold" style="width: 15%;">Title</th>
<th class="fw-semibold text-center" style="width: 10%;">Category</th>
<th class="fw-semibold" style="width: 25%;">Description</th>
<th class="fw-semibold" style="width: 20%;">Address</th>
<th class="fw-semibold text-center" style="width: 8%;" hidden>Postcode</th>
<th class="fw-semibold text-center" style="width: 12%;">Coordinates</th>
<th class="fw-semibold text-center" style="width: 8%;">Contributor</th>
<th class="fw-semibold text-center" style="width: 10%;">Actions</th>
</tr>
</thead>
<tbody class="border-top-0">
<!-- Table content will be dynamically populated by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<h3>A template for web-site development using the <i>Model-View-Controller</i> design pattern and <a href="http://www.getbootstrap.com/css"><i>Bootstrap</i></a>.</h3>
<p>The <i>Views/template</i> directory contains a <i>header.phtl</i> and a <i>footer.phtml</i> which should be included on every new page generated.
To add additional pages just edit the file <i>header.phtml</i> to add the extra link and then add a new <i>Controller (pageN.php)</i> and a new <i>View (pageN.phtml)</i>, for each page required.</p>
<p>The <i>Model</i> code files are placed in the <i>Models</i> directory.</p>
<p>Do not change any of the css files in the <i>css</> directory!<p>
<!-- Include modal templates -->
<?php require('template/createModal.phtml') ?>
<?php require('template/updateModal.phtml') ?>
<?php require('template/deleteModal.phtml') ?>
<?php require('template/statusModal.phtml') ?>
<!-- Script to update the facility count badge -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Update facility count badge based on data in sessionStorage
const updateFacilityCount = () => {
const facilityData = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
const countBadge = document.getElementById('facilityCount');
if (countBadge) {
countBadge.textContent = `${facilityData.length} facilities`;
}
};
// Initial count update when the page loads
updateFacilityCount();
// Listen for changes in facility data to update the count
window.addEventListener('storage', function(e) {
if (e.key === 'facilityData') {
updateFacilityCount();
}
});
});
</script>
<?php require('template/footer.phtml');?>
<?php require('template/footer.phtml') ?>

View File

@@ -1,5 +0,0 @@
<?php require('template/header.phtml') ?>
<h3>Welcome to Page1</h3>
<?php require('template/footer.phtml') ?>

View File

@@ -1,5 +0,0 @@
<?php require('template/header.phtml') ?>
<h3>Welcome to Page2</h3>
<?php require('template/footer.phtml') ?>

145
Views/template/createModal.phtml Executable file
View File

@@ -0,0 +1,145 @@
<?php if($view->user->getAccessLevel() == 1): ?>
<!-- Create Facility Modal -->
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="createModalLabel">
<i class="bi bi-plus-circle-fill text-success me-2"></i>Add New Facility
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<form id="createForm">
<input type="hidden" name="action" value="create">
<div class="mb-3">
<label for="titlCreate" class="form-label">Title</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-tag text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="titlCreate" name="titlCreate" placeholder="Enter facility title" required>
</div>
</div>
<div class="mb-3">
<label for="cateCreate" class="form-label">Category</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-bookmark text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="cateCreate" name="cateCreate" placeholder="Enter facility category" required>
</div>
</div>
<div class="mb-3">
<label for="descCreate" class="form-label">Description</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-card-text text-success"></i>
</span>
<textarea class="form-control border-start-0" id="descCreate" name="descCreate" placeholder="Enter facility description" rows="3" required></textarea>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="hnumCreate" class="form-label">House/Building Number</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-house text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="hnumCreate" name="hnumCreate" placeholder="Enter number" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="strtCreate" class="form-label">Street Name</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-signpost text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="strtCreate" name="strtCreate" placeholder="Enter street name" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="townCreate" class="form-label">Town/City</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-building text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="townCreate" name="townCreate" placeholder="Enter town/city" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="cntyCreate" class="form-label">County</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-map text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="cntyCreate" name="cntyCreate" placeholder="Enter county" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="postCreate" class="form-label">Postcode</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-mailbox text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="postCreate" name="postCreate" placeholder="Enter postcode" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="latCreate" class="form-label">Latitude</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-geo-alt text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="latCreate" name="latCreate" placeholder="Enter latitude" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="lngCreate" class="form-label">Longitude</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-geo-alt text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="lngCreate" name="lngCreate" placeholder="Enter longitude" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="contCreate" class="form-label">Contributor</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-person text-success"></i>
</span>
<input type="text" class="form-control border-start-0 bg-light" id="contCreate" name="contCreate" placeholder="Auto-filled with your username" disabled required>
</div>
<small class="text-muted">This will be automatically filled with your username</small>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="createForm" class="btn btn-success">
<i class="bi bi-plus-circle me-1"></i>Create Facility
</button>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>

View File

@@ -0,0 +1,37 @@
<!-- Delete Facility Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="deleteModalLabel">
<i class="bi bi-trash text-danger me-2"></i>Delete Facility
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<form id="deleteForm">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="idDelete" value="">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<span>Are you sure you want to delete this facility record? This action cannot be undone.</span>
</div>
<div class="mt-3">
<p class="mb-1 fw-bold">Facility to be deleted:</p>
<p id="deleteConfirmationText" class="text-danger mb-0"></p>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="deleteForm" class="btn btn-danger">
<i class="bi bi-trash me-1"></i>Delete Permanently
</button>
</div>
</div>
</div>
</div>
</div>

98
Views/template/footer.phtml Normal file → Executable file
View File

@@ -1,15 +1,99 @@
</div>
<div class="row">
<div id="footer" class="col-xs-12">
<p>Web-site development using the MVC design pattern and the Bootstrap CSS framework</p>
<div class="site-footer mt-auto">
<!-- Footer Content -->
<div class="row">
<div id="footer" class="col-xs-12">
<p class="m-0">George Wilkinson @2024</p>
<p class="m-0">Powered by Bootstrap</p>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Application JavaScript -->
<!-- Note: simpleAuth.js is already included in the header -->
<!-- Note: facilityData.js is already included in the header -->
<script src="/public/js/comments.js"></script>
<!-- Initialize components -->
<script>
// Only run initialization if not already done
if (!window.initializationComplete) {
document.addEventListener('DOMContentLoaded', function() {
// Initialize auth service
const loginButton = document.querySelector('[data-bs-toggle="modal"]');
const loginModal = document.getElementById('loginModal');
// Initialize all modals
try {
const modalElements = document.querySelectorAll('.modal');
modalElements.forEach(modalElement => {
if (modalElement) {
const modalInstance = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true
});
// Add click handler for modal triggers
const triggers = document.querySelectorAll(`[data-bs-target="#${modalElement.id}"]`);
triggers.forEach(trigger => {
trigger.addEventListener('click', (e) => {
e.preventDefault();
modalInstance.show();
});
});
}
});
} catch (error) {
console.error('Error initializing modals:', error);
}
// Initialize auth form handlers
const loginForm = document.querySelector('#loginModal form');
const loginError = document.querySelector('#loginError');
const captchaContainer = document.querySelector('.captcha-container');
if (loginForm) {
// Show CAPTCHA if needed
if (simpleAuth.needsCaptcha() && captchaContainer) {
captchaContainer.style.display = 'flex';
}
}
// Handle logout button
const logoutButton = document.querySelector('button[name="logoutButton"]');
if (logoutButton) {
logoutButton.addEventListener('click', async (e) => {
e.preventDefault();
await simpleAuth.logout();
});
}
// Validate token if authenticated
if (simpleAuth.isAuthenticated()) {
simpleAuth.validateToken().then(valid => {
if (!valid) {
if (!localStorage.getItem('validationAttempted')) {
localStorage.setItem('validationAttempted', 'true');
window.location.reload();
} else {
localStorage.removeItem('validationAttempted');
}
} else {
localStorage.removeItem('validationAttempted');
}
});
}
// Mark initialization as complete
window.initializationComplete = true;
});
}
</script>
</body>
</html>

336
Views/template/header.phtml Normal file → Executable file
View File

@@ -4,65 +4,317 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="description" content="EcoBuddy - Sustainable facilities management platform">
<meta name="author" content="">
<link rel="icon" type="image/x-icon" href="/images/ecoBuddy_x32.png">
<!-- Bootstrap core CSS from CDN for faster loading -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- Bootstrap core CSS -->
<link href="/css/bootstrap.css" rel="stylesheet">
<!-- Bootstrap theme -->
<link href="/css/bootstrap-theme.css" rel="stylesheet">
<link href="/css/my-style.css" rel="stylesheet">
<!-- CSS theme -->
<link href="/public/css/my-style.css" rel="stylesheet">
<title>Server-Side Programming - <?php echo $view->pageTitle; ?></title>
<!-- Bootstrap Icons -->
<link href="/public/css/bootstrap-icons.css" rel="stylesheet">
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Dynamic page title based on the current page -->
<title>Ecobuddy - <?php echo $view->pageTitle; ?></title>
<!-- Load simplified authentication helper -->
<script src="/public/js/simpleAuth.js"></script>
<!-- Load API client -->
<script src="/public/js/apiClient.js"></script>
<!-- Load facility data script -->
<script src="/public/js/facilityData.js"></script>
<!-- Initialise facility data from PHP server-side data -->
<script>
<?php if (isset($view->facilityDataSet) && is_array($view->facilityDataSet)): ?>
try {
// Convert PHP data to JavaScript object with proper encoding
// Using JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE for proper character handling
const initialData = <?php echo json_encode($view->facilityDataSet,
JSON_UNESCAPED_SLASHES |
JSON_UNESCAPED_UNICODE |
JSON_PARTIAL_OUTPUT_ON_ERROR
); ?>;
// Validate and store data in sessionStorage for use across the application
if (Array.isArray(initialData) && initialData.length > 0) {
sessionStorage.setItem('facilityData', JSON.stringify(initialData));
// Initialize based on DOM state to ensure scripts run at the right time
if (document.readyState === 'complete' || document.readyState === 'interactive') {
if (typeof initialiseFacilityData === 'function') {
initialiseFacilityData(initialData);
}
} else {
document.addEventListener('DOMContentLoaded', function() {
if (typeof initialiseFacilityData === 'function') {
initialiseFacilityData(initialData);
}
});
}
}
// Add client-side authentication check to update UI
document.addEventListener('DOMContentLoaded', function() {
// Check if user is authenticated on the client side
if (window.auth && window.auth.isAuthenticated()) {
console.log('User is authenticated on client side');
// Get user data
const user = window.auth.getUser();
if (user) {
console.log('User data:', user);
// Hide login button if it exists
const loginButton = document.getElementById('loginButton');
if (loginButton) {
loginButton.style.display = 'none';
}
// Hide login modal if it exists
const loginModal = document.getElementById('loginModal');
if (loginModal) {
loginModal.style.display = 'none';
}
// Show user menu
const userMenuContainer = document.createElement('div');
userMenuContainer.className = 'user-menu';
userMenuContainer.innerHTML = `
<div class="user-avatar">
<i class="bi bi-person-fill text-success"></i>
</div>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle" type="button" id="userMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
${user.username}
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userMenuButton">
<li><a class="dropdown-item" href="#"><i class="bi bi-person me-2"></i>Profile</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-gear me-2"></i>Settings</a></li>
<li><hr class="dropdown-divider"></li>
<li><button class="dropdown-item text-danger" id="logoutButton"><i class="bi bi-box-arrow-right me-2"></i>Logout</button></li>
</ul>
</div>
`;
// Replace login button with user menu
if (loginButton) {
loginButton.parentNode.replaceChild(userMenuContainer, loginButton);
}
// Add logout button handler
const logoutButton = document.getElementById('logoutButton');
if (logoutButton) {
logoutButton.addEventListener('click', async function() {
await window.auth.logout();
window.location.reload();
});
}
}
}
});
} catch (error) {
console.error('Error processing facility data:', error);
}
<?php endif; ?>
</script>
</head>
<body role="document">
<!-- Navigation bar -->
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm sticky-top">
<div class="container-fluid px-3">
<!-- Brand logo and name -->
<a class="navbar-brand d-flex align-items-center" href="/index.php">
<img src="/images/ecoBuddy_x64.png" alt="EcoBuddy Logo" width="48" height="48" class="me-2">
<span class="fw-bold text-success">EcoBuddy</span>
</a>
<div class="container">
<div class="row">
<div id="title" class="col-xs-12">
<img src="/images/new_uos_logo.jpg" alt="Salford University" />
<div class="pull-right"> <h1><?php echo $view->pageTitle ?> </h1></div>
<!-- Mobile menu toggle -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent"
aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Navigation content -->
<div class="collapse navbar-collapse" id="navbarContent">
<!-- Main navigation links -->
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/index.php">
<i class="bi bi-house-fill me-1"></i>Home
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/map.php">
<i class="bi bi-map-fill me-1"></i>Map
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about.php">
<i class="bi bi-info-circle-fill me-1"></i>About
</a>
</li>
</ul>
<!-- Search and filter controls -->
<div class="d-flex flex-column flex-lg-row search-controls mx-auto">
<form class="d-flex flex-column flex-lg-row gap-2 w-100" role="search" action="" method="POST">
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-sort-alpha-down text-success"></i>
</span>
<select name="sort" class="form-select border-start-0 filter-control" id="sort">
<option value="title">Title</option>
<option value="category">Category</option>
<option value="status">Status</option>
<option value="description">Description</option>
<option value="streetName">Street</option>
<option value="county">County</option>
<option value="town">Town</option>
<option value="postcode">Postcode</option>
<option value="contributor">Contributor</option>
</select>
<select class="form-select sort-control" name="dir" id="dir" data-order="asc">
<option value="asc">Asc</option>
<option value="desc">Desc</option>
</select>
</div>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-filter-circle-fill text-success"></i>
</span>
<select name="filterCat" class="form-select border-start-0 filter-control" id="filterCat">
<option value="">All</option>
<option value="title">Title</option>
<option value="category">Category</option>
<option value="status">Status</option>
<option value="description">Description</option>
<option value="streetName">Street</option>
<option value="county">County</option>
<option value="town">Town</option>
<option value="postcode">Postcode</option>
<option value="contributor">Contributor</option>
</select>
</div>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-search text-success"></i>
</span>
<input class="form-control border-start-0" id="searchInput" type="search" name="filter" placeholder="Search..." aria-label="Search">
</div>
</form>
</div>
<!-- User account section -->
<div class="ms-lg-3 mt-3 mt-lg-0">
<?php if(!$view->user->isLoggedIn()): ?>
<button type="button" class="btn btn-success" id="loginButton" data-bs-toggle="modal" data-bs-target="#loginModal">
<i class="bi bi-box-arrow-in-right me-1"></i>Login
</button>
<?php else: ?>
<div class="user-menu">
<div class="user-avatar">
<i class="bi bi-person-fill text-success"></i>
</div>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle" type="button" id="userMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo $view->user->getUsername(); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userMenuButton">
<li><a class="dropdown-item" href="#"><i class="bi bi-person me-2"></i>Profile</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-gear me-2"></i>Settings</a></li>
<li><hr class="dropdown-divider"></li>
<li><button class="dropdown-item text-danger" id="logoutButton"><i class="bi bi-box-arrow-right me-2"></i>Logout</button></li>
</ul>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</nav>
<!-- Login Modal -->
<?php if(!$view->user->isLoggedIn()): ?>
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="loginModalLabel">
<i class="bi bi-box-arrow-in-right text-success me-2"></i>Login to EcoBuddy
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-person text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="username" name="username" placeholder="Enter your username" required>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-lock text-success"></i>
</span>
<input type="password" class="form-control border-start-0" id="password" name="password" placeholder="Enter your password" required>
</div>
</div>
<div class="row">
<div id="menu" class="col-xs-6 col-sm-3 col-md-2">
<ul class="nav navbar_default nav-stacked">
<div id="loginError" class="alert alert-danger" style="display: none;"></div>
<div class="row captcha-container" style="display: none;">
<!-- CAPTCHA Display -->
<div class="col-md-6 mb-3">
<label for="captchaCode" class="form-label">CAPTCHA Code</label>
<input type="text" class="form-control bg-light" id="captchaCode" name="generatedCaptcha" value="" readonly>
</div>
<?php
<!-- CAPTCHA Input -->
<div class="col-md-6 mb-3">
<label for="captchaInput" class="form-label">Enter CAPTCHA</label>
<input type="text" class="form-control" id="captchaInput" name="captchaInput" placeholder="Enter code">
</div>
</div>
if (!isset($_SESSION["login"])) {
echo '
<form method="post" action="" class="form text-primary">
<label for="username">Username</label>
<input type="text" name="username">
<label for="password">Password</label>
<input type="text" name="password">
<input type="submit" name="loginbutton" value="Login">
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-success">
<i class="bi bi-box-arrow-in-right me-2"></i>Login
</button>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between align-items-center">
<small class="text-muted">Don't have an account? <a href="" onclick="alert('Please contact the administrator to create an account.');" class="text-success">Register</a></small>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
</form>';
}
else
{
echo '
<form method="post" action="" class="form text-primary">
<input type="submit" name="logoutbutton" value="Logout">
</form>';
}
?>
<li><a href="/index.php">Home</a></li>
<li><a href="/page1.php">Page1</a></li>
<li><a href="/page2.php">Page2</a></li>
</ul>
</div>
<div id="content" class="col-xs-6 col-sm-9 col-md-10">
<!-- Main content container -->
<div class="container-fluid py-4 px-3">
<div class="row" id="content">

18
Views/template/loginError.phtml Executable file
View File

@@ -0,0 +1,18 @@
<span class="ms-5 me-5 row alert alert-danger" role="alert"><?= $view->loginError ?></span>
<div class="row captcha-container">
<!-- CAPTCHA Display -->
<div class="form-floating mb-3 col">
<input type="text" class="form-control" id="captchaCode" value="<?php
// Generate a simple 5-character CAPTCHA
$captcha = substr(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"), 0, 5);
echo $captcha;
?>" readonly>
<label for="captchaCode">CAPTCHA Code</label>
</div>
<!-- CAPTCHA Input -->
<div class="form-floating mb-3 col">
<input type="text" class="form-control" id="captchaInput" name="captchaInput" placeholder="Enter CAPTCHA" required>
<label for="captchaInput">Enter CAPTCHA</label>
</div>
</div>

63
Views/template/loginModal.phtml Executable file
View File

@@ -0,0 +1,63 @@
<!-- Login Modal -->
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="loginModalLabel">
<i class="bi bi-box-arrow-in-right text-success me-2"></i>Login to EcoBuddy
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-person text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="username" name="username" placeholder="Enter your username" required>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-lock text-success"></i>
</span>
<input type="password" class="form-control border-start-0" id="password" name="password" placeholder="Enter your password" required>
</div>
</div>
<div id="loginError" class="alert alert-danger" style="display: none;"></div>
<div class="row captcha-container" style="display: none;">
<!-- CAPTCHA Display -->
<div class="col-md-6 mb-3">
<label for="captchaCode" class="form-label">CAPTCHA Code</label>
<input type="text" class="form-control bg-light" id="captchaCode" name="generatedCaptcha" value="" readonly>
</div>
<!-- CAPTCHA Input -->
<div class="col-md-6 mb-3">
<label for="captchaInput" class="form-label">Enter CAPTCHA</label>
<input type="text" class="form-control" id="captchaInput" name="captchaInput" placeholder="Enter code">
</div>
</div>
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-success">
<i class="bi bi-box-arrow-in-right me-2"></i>Login
</button>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between align-items-center">
<small class="text-muted">Don't have an account? <a href="" onclick="alert('Please contact the administrator to create an account.');" class="text-success">Register</a></small>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,2 @@
<?php echo "<p class='text-center bg-light border-0 rounded mb-1' style='color: black;'>" . $user->getUsername() . "<span class='bi bi-person-fill'></span></p>"?>
<button class="btn bg-danger btn-outline-danger text-light" type="button" id="logoutButton">Logout</button>

78
Views/template/pagination.phtml Executable file
View File

@@ -0,0 +1,78 @@
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center gap-2">
<div class="text-muted small">
<span id="paginationInfo" class="d-flex align-items-center">
<i class="bi bi-info-circle me-2 text-success"></i>
<span>Showing facilities</span>
</span>
</div>
<!-- Pagination controls -->
<nav aria-label="Facility table pagination">
<ul class="pagination pagination-sm mb-0" id="paginationControls">
<!-- First page button -->
<li class="page-item">
<a class="page-link border-0 text-success" href="#" aria-label="First" id="firstPage">
<i class="bi bi-chevron-double-left"></i>
</a>
</li>
<!-- Previous page button -->
<li class="page-item">
<a class="page-link border-0 text-success" href="#" aria-label="Previous" id="prevPage">
<i class="bi bi-chevron-left"></i>
</a>
</li>
<!-- Dynamic page numbers will be inserted here as list items -->
<!-- Next page button -->
<li class="page-item">
<a class="page-link border-0 text-success" href="#" aria-label="Next" id="nextPage">
<i class="bi bi-chevron-right"></i>
</a>
</li>
<!-- Last page button -->
<li class="page-item">
<a class="page-link border-0 text-success" href="#" aria-label="Last" id="lastPage">
<i class="bi bi-chevron-double-right"></i>
</a>
</li>
</ul>
</nav>
<!-- Items per page selector -->
<div class="d-flex align-items-center">
<label for="itemsPerPage" class="form-label text-muted small mb-0 me-2">Items per page:</label>
<select class="form-select form-select-sm" id="itemsPerPage" style="width: 70px;">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Set up items per page selector
const itemsPerPageSelect = document.getElementById('itemsPerPage');
if (itemsPerPageSelect) {
itemsPerPageSelect.addEventListener('change', function() {
// Update items per page in the pagination system
if (typeof itemsPerPage !== 'undefined') {
itemsPerPage = parseInt(this.value);
currentPage = 1; // Reset to first page
// Recalculate total pages
if (typeof filteredData !== 'undefined' && typeof totalPages !== 'undefined') {
totalPages = Math.ceil(filteredData.length / itemsPerPage);
// Update table with new pagination
if (typeof updateTableWithPagination === 'function') {
updateTableWithPagination();
}
}
}
});
}
});
</script>

107
Views/template/statusModal.phtml Executable file
View File

@@ -0,0 +1,107 @@
<!-- Facility Comments Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="statusModalLabel">
<i class="bi bi-chat-square-text text-primary me-2"></i>Facility Comments
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<!-- Add Comment Form (Only shown to logged in users) -->
<?php if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && $_SESSION['access'] >= 1): ?>
<div class="mb-4 border-bottom pb-4">
<h6 class="fw-bold mb-3">
<i class="bi bi-plus-circle text-success me-2"></i>Add New Comment
</h6>
<form id="commentForm">
<input type="hidden" name="action" value="status">
<input type="hidden" name="facilityId" id="commentFacilityId" value="">
<div class="mb-3">
<label for="commentText" class="form-label">Your Comment</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-pencil text-primary"></i>
</span>
<textarea class="form-control border-start-0" id="commentText" name="commentText" rows="3" placeholder="Share your thoughts about this facility..." required></textarea>
</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
<i class="bi bi-send me-1"></i>Post Comment
</button>
</div>
</form>
</div>
<?php endif; ?>
<!-- Existing Comments Section -->
<div>
<h6 class="fw-bold mb-3">
<i class="bi bi-chat-square-dots text-primary me-2"></i>Comments
</h6>
<div id="commentsContainer" class="comments-container">
<!-- Comments will be loaded here dynamically -->
<div class="text-center py-4 text-muted" id="noCommentsMessage">
<i class="bi bi-chat-square-text fs-4 d-block mb-2"></i>
<p>No comments yet. Be the first to share your thoughts!</p>
</div>
</div>
<!-- Login Prompt for Non-Authenticated Users -->
<?php if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true): ?>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle me-2"></i>
<span>Please <a href="#" data-bs-toggle="modal" data-bs-target="#loginModal" data-bs-dismiss="modal">log in</a> to add your comments.</span>
</div>
<?php endif; ?>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Edit Comment Modal -->
<div class="modal fade" id="editCommentModal" tabindex="-1" aria-labelledby="editCommentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="editCommentModalLabel">
<i class="bi bi-pencil-square text-primary me-2"></i>Edit Comment
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<form id="editCommentForm">
<input type="hidden" name="action" value="editComment">
<input type="hidden" name="commentId" id="editCommentId" value="">
<div class="mb-3">
<label for="editCommentText" class="form-label">Edit Your Comment</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-pencil text-primary"></i>
</span>
<textarea class="form-control border-start-0" id="editCommentText" name="editCommentText" rows="4" placeholder="Update your comment..." required></textarea>
</div>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="editCommentForm" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i>Save Changes
</button>
</div>
</div>
</div>
</div>
</div>

144
Views/template/updateModal.phtml Executable file
View File

@@ -0,0 +1,144 @@
<!-- Update Facility Modal -->
<div class="modal fade" id="updateModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="updateModalLabel">
<i class="bi bi-pencil-square text-success me-2"></i>Update Facility
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<form id="updateForm">
<input type="hidden" name="action" value="update">
<input type="hidden" name="idUpdate" value="">
<div class="mb-3">
<label for="titlUpdate" class="form-label">Title</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-tag text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="titlUpdate" name="titlUpdate" placeholder="Enter facility title" required>
</div>
</div>
<div class="mb-3">
<label for="cateUpdate" class="form-label">Category</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-bookmark text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="cateUpdate" name="cateUpdate" placeholder="Enter facility category" required>
</div>
</div>
<div class="mb-3">
<label for="descUpdate" class="form-label">Description</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-card-text text-success"></i>
</span>
<textarea class="form-control border-start-0" id="descUpdate" name="descUpdate" placeholder="Enter facility description" rows="3" required></textarea>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="hnumUpdate" class="form-label">House/Building Number</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-house text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="hnumUpdate" name="hnumUpdate" placeholder="Enter number" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="strtUpdate" class="form-label">Street Name</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-signpost text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="strtUpdate" name="strtUpdate" placeholder="Enter street name" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="townUpdate" class="form-label">Town/City</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-building text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="townUpdate" name="townUpdate" placeholder="Enter town/city" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="cntyUpdate" class="form-label">County</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-map text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="cntyUpdate" name="cntyUpdate" placeholder="Enter county" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="postUpdate" class="form-label">Postcode</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-mailbox text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="postUpdate" name="postUpdate" placeholder="Enter postcode" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="latUpdate" class="form-label">Latitude</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-geo-alt text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="latUpdate" name="latUpdate" placeholder="Enter latitude" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="lngUpdate" class="form-label">Longitude</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-geo-alt text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="lngUpdate" name="lngUpdate" placeholder="Enter longitude" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="contUpdate" class="form-label">Contributor</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-person text-success"></i>
</span>
<input type="text" class="form-control border-start-0 bg-light" id="contUpdate" name="contUpdate" placeholder="Original contributor" readonly required>
</div>
<small class="text-muted">Original contributor of this facility</small>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="updateForm" class="btn btn-success">
<i class="bi bi-check-circle me-1"></i>Update Facility
</button>
</div>
</div>
</div>
</div>
</div>

225
add_facilities.py Normal file
View File

@@ -0,0 +1,225 @@
import sqlite3
import random
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# Check if we need to add any new categories
cursor.execute("SELECT id FROM ecoCategories")
existing_categories = [row[0] for row in cursor.fetchall()]
# Add two new categories
new_categories = [
(16, "Urban Farms"),
(17, "Rainwater Harvesting Systems")
]
for category in new_categories:
if category[0] not in existing_categories:
cursor.execute("INSERT INTO ecoCategories (id, name) VALUES (?, ?)", category)
print(f"Added new category: {category[1]}")
# Get list of user IDs for contributors
cursor.execute("SELECT id FROM ecoUser")
user_ids = [row[0] for row in cursor.fetchall()]
# Define 10 new ecological facilities in the UK with accurate location data
new_facilities = [
{
"title": "Community Garden Hackney",
"category": 12, # Pollinator Gardens
"description": "Urban garden with native plants to support local pollinators",
"houseNumber": "45",
"streetName": "Dalston Lane",
"county": "Greater London",
"town": "London",
"postcode": "E8 3AH",
"lng": -0.0612,
"lat": 51.5476,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded with new wildflower section",
"Volunteer days every Saturday"
]
},
{
"title": "Rooftop Solar Farm",
"category": 8, # Green Roofs
"description": "Combined green roof and solar panel installation on commercial building",
"houseNumber": "120",
"streetName": "Deansgate",
"county": "Greater Manchester",
"town": "Manchester",
"postcode": "M3 2QJ",
"lng": -2.2484,
"lat": 53.4808,
"contributor": random.choice(user_ids),
"status_comments": [
"Generates power for the entire building"
]
},
{
"title": "Edinburgh Tool Library",
"category": 15, # Community Tool Libraries
"description": "Borrow tools instead of buying them - reducing waste and consumption",
"houseNumber": "25",
"streetName": "Leith Walk",
"county": "Edinburgh",
"town": "Edinburgh",
"postcode": "EH6 8LN",
"lng": -3.1752,
"lat": 55.9677,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Cardiff Bay Water Refill Station",
"category": 9, # Public Water Refill Stations
"description": "Free water refill station to reduce plastic bottle usage",
"houseNumber": "3",
"streetName": "Mermaid Quay",
"county": "Cardiff",
"town": "Cardiff",
"postcode": "CF10 5BZ",
"lng": -3.1644,
"lat": 51.4644,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently cleaned and maintained",
"High usage during summer months"
]
},
{
"title": "Bristol Urban Farm",
"category": 16, # Urban Farms (new category)
"description": "Community-run urban farm providing local produce and education",
"houseNumber": "18",
"streetName": "Stapleton Road",
"county": "Bristol",
"town": "Bristol",
"postcode": "BS5 0RA",
"lng": -2.5677,
"lat": 51.4635,
"contributor": random.choice(user_ids),
"status_comments": [
"Open for volunteers Tuesday-Sunday"
]
},
{
"title": "Newcastle Rainwater Collection System",
"category": 17, # Rainwater Harvesting Systems (new category)
"description": "Public demonstration of rainwater harvesting for garden irrigation",
"houseNumber": "55",
"streetName": "Northumberland Street",
"county": "Tyne and Wear",
"town": "Newcastle upon Tyne",
"postcode": "NE1 7DH",
"lng": -1.6178,
"lat": 54.9783,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Brighton Beach Solar Bench",
"category": 7, # Solar-Powered Benches
"description": "Solar-powered bench with USB charging ports and WiFi",
"houseNumber": "",
"streetName": "Kings Road",
"county": "East Sussex",
"town": "Brighton",
"postcode": "BN1 2FN",
"lng": -0.1426,
"lat": 50.8214,
"contributor": random.choice(user_ids),
"status_comments": [
"Popular spot for tourists",
"One USB port currently not working"
]
},
{
"title": "Leeds Community Compost Hub",
"category": 6, # Community Compost Bins
"description": "Large-scale community composting facility for local residents",
"houseNumber": "78",
"streetName": "Woodhouse Lane",
"county": "West Yorkshire",
"town": "Leeds",
"postcode": "LS2 9JT",
"lng": -1.5491,
"lat": 53.8067,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded capacity"
]
},
{
"title": "Glasgow EV Charging Hub",
"category": 4, # Public EV Charging Stations
"description": "Multi-vehicle EV charging station with fast chargers",
"houseNumber": "42",
"streetName": "Buchanan Street",
"county": "Glasgow",
"town": "Glasgow",
"postcode": "G1 3JX",
"lng": -4.2526,
"lat": 55.8621,
"contributor": random.choice(user_ids),
"status_comments": [
"6 charging points available",
"24/7 access"
]
},
{
"title": "Oxford E-Waste Collection Center",
"category": 13, # E-Waste Collection Bins
"description": "Dedicated facility for proper disposal and recycling of electronic waste",
"houseNumber": "15",
"streetName": "St Aldate's",
"county": "Oxfordshire",
"town": "Oxford",
"postcode": "OX1 1BX",
"lng": -1.2577,
"lat": 51.7520,
"contributor": random.choice(user_ids),
"status_comments": []
}
]
# Insert facilities into the database
for facility in new_facilities:
cursor.execute("""
INSERT INTO ecoFacilities
(title, category, description, houseNumber, streetName, county, town, postcode, lng, lat, contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
facility["title"],
facility["category"],
facility["description"],
facility["houseNumber"],
facility["streetName"],
facility["county"],
facility["town"],
facility["postcode"],
facility["lng"],
facility["lat"],
facility["contributor"]
))
# Get the ID of the inserted facility
facility_id = cursor.lastrowid
# Add status comments if any
for comment in facility["status_comments"]:
cursor.execute("""
INSERT INTO ecoFacilityStatus (facilityId, statusComment)
VALUES (?, ?)
""", (facility_id, comment))
print(f"Added facility: {facility['title']} in {facility['town']}")
# Commit the changes and close the connection
conn.commit()
conn.close()
print("\nSuccessfully added 10 new ecological facilities to the database.")

33
api/admin.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
/**
* Admin-only API endpoint example
*
* This endpoint demonstrates how to protect an API route for admin users only
* using our simplified authentication approach.
*/
require_once('../Models/User.php');
// Set content type to JSON
header('Content-Type: application/json');
// Check if user is an admin
$auth = User::checkAdmin();
if (!$auth) {
// The checkAdmin method already sent the error response
exit;
}
// User is an admin, proceed with the admin-only logic
$response = [
'status' => 'success',
'message' => 'You have access to this admin-only endpoint',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username'],
'accessLevel' => $auth['accessLevel']
]
];
// Send response
echo json_encode($response);

51
api/login.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
/**
* Login API endpoint
*
* This endpoint handles user authentication and returns a JWT token
* if the credentials are valid.
*/
require_once('../Models/User.php');
// Set content type to JSON
header('Content-Type: application/json');
// Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
// Get JSON request body
$json = file_get_contents('php://input');
$data = json_decode($json, true);
// Validate request data
if (!$data || !isset($data['username']) || !isset($data['password'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid request data']);
exit;
}
// Authenticate user
$user = new User();
$token = $user->Authenticate($data['username'], $data['password']);
if ($token) {
// Authentication successful
echo json_encode([
'success' => true,
'token' => $token,
'user' => [
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
]
]);
} else {
// Authentication failed
http_response_code(401);
echo json_encode(['error' => 'Invalid credentials']);
}

33
api/protected.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
/**
* Protected API endpoint example
*
* This endpoint demonstrates how to protect an API route using
* our simplified authentication approach.
*/
require_once('../Models/User.php');
// Set content type to JSON
header('Content-Type: application/json');
// Check if user is authenticated
$auth = User::checkAuth();
if (!$auth) {
// The checkAuth method already sent the error response
exit;
}
// User is authenticated, proceed with the endpoint logic
$response = [
'status' => 'success',
'message' => 'You have access to this protected endpoint',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username'],
'accessLevel' => $auth['accessLevel']
]
];
// Send response
echo json_encode($response);

113
auth.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
require_once('Models/AuthService.php');
require_once('Models/UserDataSet.php');
require_once('Models/User.php');
// Enable CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Content-Type: application/json');
// Handle OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
try {
$auth = new AuthService();
$userDataSet = new UserDataSet();
// Handle POST request for login
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
// Handle token refresh
if (isset($data['action']) && $data['action'] === 'refresh') {
if (!isset($data['refreshToken'])) {
http_response_code(400);
echo json_encode(['error' => 'Refresh token is required']);
exit;
}
$refreshToken = $data['refreshToken'];
$newToken = $auth->refreshToken($refreshToken);
if (!$newToken) {
http_response_code(401);
echo json_encode(['error' => 'Invalid or expired refresh token']);
exit;
}
echo json_encode([
'success' => true,
'token' => $newToken
]);
exit;
}
// Handle login
if (!isset($data['username']) || !isset($data['password'])) {
http_response_code(400);
echo json_encode(['error' => 'Username and password are required']);
exit;
}
// Authenticate user
$user = new User();
$token = $user->Authenticate($data['username'], $data['password']);
if ($token) {
// Generate refresh token
$refreshToken = $auth->generateRefreshToken([
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
]);
echo json_encode([
'success' => true,
'token' => $token,
'refreshToken' => $refreshToken,
'user' => [
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
]
]);
} else {
http_response_code(401);
echo json_encode(['error' => 'Invalid credentials']);
}
exit;
}
// Handle GET request for token validation
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$auth = User::checkAuth(false);
if ($auth) {
echo json_encode([
'valid' => true,
'user' => [
'id' => $auth['uid'],
'username' => $auth['username'],
'accessLevel' => $auth['accessLevel']
]
]);
} else {
http_response_code(401);
echo json_encode(['valid' => false, 'error' => 'Invalid or expired token']);
}
exit;
}
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
} catch (Exception $e) {
error_log('Auth error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Server error', 'message' => $e->getMessage()]);
}

View File

@@ -1,347 +0,0 @@
/*!
* Bootstrap v3.1.1 (http://getbootstrap.com)
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn:active,
.btn.active {
background-image: none;
}
.btn-default {
text-shadow: 0 1px 0 #fff;
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #dbdbdb;
border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus {
background-color: #e0e0e0;
background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
background-color: #e0e0e0;
border-color: #dbdbdb;
}
.btn-primary {
background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #2b669a;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #2d6ca2;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #2d6ca2;
border-color: #2b669a;
}
.btn-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #3e8f3e;
}
.btn-success:hover,
.btn-success:focus {
background-color: #419641;
background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
background-color: #419641;
border-color: #3e8f3e;
}
.btn-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #28a4c9;
}
.btn-info:hover,
.btn-info:focus {
background-color: #2aabd2;
background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
background-color: #2aabd2;
border-color: #28a4c9;
}
.btn-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #e38d13;
}
.btn-warning:hover,
.btn-warning:focus {
background-color: #eb9316;
background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
background-color: #eb9316;
border-color: #e38d13;
}
.btn-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #b92c28;
}
.btn-danger:hover,
.btn-danger:focus {
background-color: #c12e2a;
background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
background-color: #c12e2a;
border-color: #b92c28;
}
.thumbnail,
.img-thumbnail {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e8e8e8;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
background-color: #357ebd;
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
background-repeat: repeat-x;
}
.navbar-default {
background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
}
.navbar-default .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
}
.navbar-brand,
.navbar-nav > li > a {
text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
}
.navbar-inverse {
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
}
.navbar-inverse .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);
background-image: linear-gradient(to bottom, #222 0%, #282828 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav > li > a {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
}
.navbar-static-top,
.navbar-fixed-top,
.navbar-fixed-bottom {
border-radius: 0;
}
.alert {
text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
}
.alert-success {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
background-repeat: repeat-x;
border-color: #b2dba1;
}
.alert-info {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
background-repeat: repeat-x;
border-color: #9acfea;
}
.alert-warning {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
background-repeat: repeat-x;
border-color: #f5e79e;
}
.alert-danger {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
background-repeat: repeat-x;
border-color: #dca7a7;
}
.progress {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar {
background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
background-repeat: repeat-x;
}
.list-group {
border-radius: 4px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
text-shadow: 0 -1px 0 #3071a9;
background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
background-repeat: repeat-x;
border-color: #3278b3;
}
.panel {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
}
.panel-default > .panel-heading {
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.panel-primary > .panel-heading {
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
background-repeat: repeat-x;
}
.panel-success > .panel-heading {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
background-repeat: repeat-x;
}
.panel-info > .panel-heading {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
background-repeat: repeat-x;
}
.panel-warning > .panel-heading {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
background-repeat: repeat-x;
}
.panel-danger > .panel-heading {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
background-repeat: repeat-x;
}
.well {
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
border-color: #dcdcdc;
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
}
/*# sourceMappingURL=bootstrap-theme.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5785
css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,38 +0,0 @@
#title {
margin-top: 12px;
background-color: #fff;
color: #000;
}
#menu {
border-top: solid 6px #000;
background-color: #fff;
color: #fff;
height: 400px;
}
#menu a {
background-color: #f00;
color: #fff;
text-decoration: none;
display: block;
}
#menu a:hover {
background-color: #f00;
color: #ddd;
text-decoration:underline;
display: block;
}
#content {
background-color: #fff;
border-top: solid 6px #f00;
}
#footer {
margin-top: 20px;
text-align: center;
background-color: #000;
color: #fff;
}

16988
debug.log Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
services:
nginx-web:
container_name: nginx-web
image: nginx
volumes:
- ./nginx/configs/:/etc/nginx/
- ./nginx/logs/:/var/log/nginx
#- /home/boris/OneDrive/CSCS-Y2/Client Server Systems/Ecobuddy/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./:/var/www/html
#- /home/boris/OneDrive/CSCS-Y2/Client Server Systems/Ecobuddy/access.log:/var/log/nginx/access.log
#- /home/boris/OneDrive/CSCS-Y2/Client Server Systems/Ecobuddy/error.log:/var/log/nginx/error.log
ports:
- "8088:80"
environment:
- NGINX_HOST=localhost
- NGINX_PORT=8088
links:
- php-fpm
depends_on:
- php-fpm
php-fpm:
container_name: php-fpm
image: php:8-fpm
volumes:
- ./:/var/www/
- /home/boris/OneDrive/CSCS-Y2/Client Server Systems/Ecobuddy/nginx/php/config/www.conf:/usr/local/etc/php-fpm.d/www.conf
- /home/boris/OneDrive/CSCS-Y2/Client Server Systems/Ecobuddy/nginx/php/fpm-php.www.log:/var/log/fpm-php.www.log

Binary file not shown.

Binary file not shown.

192
facilitycontroller.php Normal file
View File

@@ -0,0 +1,192 @@
<?php
require_once('Models/AuthService.php');
require_once('Models/FacilityDataSet.php');
require_once('Models/User.php');
// Enable CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Content-Type: application/json');
// Handle OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
try {
$facilityDataSet = new FacilityDataSet();
// Handle POST requests for CRUD operations
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// Set up request data
$request = [
'action' => $action,
'data' => $_POST
];
// Apply different authentication checks based on action
if ($action === 'read' || $action === 'getStatuses') {
// These actions don't require authentication
// No authentication check needed
} else if (in_array($action, ['create', 'update', 'delete', 'editStatus', 'deleteStatus'])) {
// These actions require admin privileges
$auth = User::checkAdmin();
if (!$auth) {
// The checkAdmin method already sent the error response
exit;
}
} else if ($action === 'status') {
// This action requires authentication but not admin privileges
$auth = User::checkAuth();
if (!$auth) {
// The checkAuth method already sent the error response
exit;
}
} else {
// Unknown action
http_response_code(400);
echo json_encode(['error' => 'Invalid action']);
exit;
}
// Process the action
switch ($action) {
case 'read':
$facilities = $facilityDataSet->fetchAll();
if ($facilities) {
echo json_encode(['success' => true, 'facilities' => $facilities]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to fetch facilities']);
}
break;
case 'create':
try {
$data = [
'title' => $_POST['title'],
'category' => $_POST['category'],
'description' => $_POST['description'],
'houseNumber' => $_POST['houseNumber'],
'streetName' => $_POST['streetName'],
'county' => $_POST['county'],
'town' => $_POST['town'],
'postcode' => $_POST['postcode'],
'lng' => $_POST['lng'],
'lat' => $_POST['lat'],
'contributor' => $auth['username']
];
$facility = $facilityDataSet->createFacility($data);
if ($facility) {
echo json_encode(['success' => true, 'facility' => $facility]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to create facility']);
}
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'update':
try {
$id = $_POST['id'];
$data = [
'title' => $_POST['title'],
'category' => $_POST['category'],
'description' => $_POST['description'],
'houseNumber' => $_POST['houseNumber'],
'streetName' => $_POST['streetName'],
'county' => $_POST['county'],
'town' => $_POST['town'],
'postcode' => $_POST['postcode'],
'lng' => $_POST['lng'],
'lat' => $_POST['lat'],
'contributor' => $auth['username']
];
$facility = $facilityDataSet->updateFacility($id, $data);
if ($facility) {
echo json_encode(['success' => true, 'facility' => $facility]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to update facility']);
}
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'delete':
$id = $_POST['id'];
if ($facilityDataSet->deleteFacility($id)) {
echo json_encode(['success' => true]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to delete facility']);
}
break;
case 'status':
$facilityId = $_POST['facilityId'];
$statusComment = $_POST['statusComment'];
if ($facilityDataSet->addFacilityStatus($facilityId, $statusComment)) {
echo json_encode(['success' => true]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to add status']);
}
break;
case 'getStatuses':
$facilityId = $_POST['facilityId'];
$statuses = $facilityDataSet->getFacilityStatuses($facilityId);
echo json_encode(['success' => true, 'statuses' => $statuses]);
break;
case 'editStatus':
$statusId = $_POST['statusId'];
$statusComment = $_POST['statusComment'];
if ($facilityDataSet->updateFacilityStatus($statusId, $statusComment)) {
echo json_encode(['success' => true]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to update status']);
}
break;
case 'deleteStatus':
$statusId = $_POST['statusId'];
if ($facilityDataSet->deleteFacilityStatus($statusId)) {
echo json_encode(['success' => true]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to delete status']);
}
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Invalid action']);
break;
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
} catch (Exception $e) {
error_log('Facility controller error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Server error', 'message' => $e->getMessage()]);
}

0
fonts/glyphicons-halflings-regular.eot Normal file → Executable file
View File

0
fonts/glyphicons-halflings-regular.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

0
fonts/glyphicons-halflings-regular.ttf Normal file → Executable file
View File

0
fonts/glyphicons-halflings-regular.woff Normal file → Executable file
View File

BIN
images/ecoBuddy_x128.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
images/ecoBuddy_x128.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
images/ecoBuddy_x32.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
images/ecoBuddy_x32.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
images/ecoBuddy_x64.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/ecoBuddy_x64.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

20
index.php Normal file → Executable file
View File

@@ -1,10 +1,26 @@
<?php
// load dataset
require_once('Models/UserDataSet.php');
require_once('Models/FacilityDataSet.php');
// make a view class
$view = new stdClass();
$view->pageTitle = 'Homepage';
$view->pageTitle = 'Home';
// initialise facility data
$facilityDataSet = new FacilityDataSet();
$view->facilityDataSet = $facilityDataSet->fetchAll();
// Log any critical errors
if ($view->facilityDataSet === false) {
error_log('Error fetching facility data');
}
// load login controller
require_once("logincontroller.php");
$view->user = new User();
// load main view
require_once('Views/index.phtml');

1951
js/bootstrap.js vendored

File diff suppressed because it is too large Load Diff

6
js/bootstrap.min.js vendored

File diff suppressed because one or more lines are too long

177
logincontroller.php Normal file → Executable file
View File

@@ -1,33 +1,158 @@
<?php
session_start();
var_dump($_SESSION);
require_once("Models/User.php");
require_once("Models/AuthService.php");
if (isset($_POST["loginbutton"])) {
$username = $_POST["username"];
$password = $_POST["password"];
echo $username;
echo $password;
// Enable CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($username == "a" && $password == "b") {
// better would be to check these variables against your database
// SELECT * FROM usertable WHERE username=$username AND password=$password
// if number of rows returned > 0 then the username and password matched
echo "You are logged in";
$_SESSION["login"] = $username;
}
else
{
echo "Error in username and password";
}
// Handle OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if (isset($_POST["logoutbutton"]))
{
echo "logout user";
unset($_SESSION["login"]);
session_destroy();
// Create service objects
$authService = new AuthService();
// Start session for CAPTCHA handling only
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Handle AJAX requests
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
header('Content-Type: application/json');
if (isset($_POST["action"])) {
switch ($_POST["action"]) {
case "login":
$username = $_POST["username"];
$password = hash("sha256", $_POST["password"]);
// Check if CAPTCHA is required (after 3 failed attempts)
$loginAttempts = isset($_SESSION['loginAttempts']) ? $_SESSION['loginAttempts'] : 0;
if ($loginAttempts >= 3) {
// Validate CAPTCHA
if (!isset($_POST["captchaInput"]) || !isset($_SESSION['captcha']) ||
$_POST["captchaInput"] !== $_SESSION['captcha']) {
// Generate new CAPTCHA for next attempt
$captcha = substr(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"), 0, 5);
$_SESSION['captcha'] = $captcha;
echo json_encode([
"error" => "Incorrect CAPTCHA.",
"captcha" => $captcha
]);
exit;
}
// Clear CAPTCHA after successful validation
unset($_SESSION['captcha']);
}
// Authenticate user
$user = new User();
$token = $user->Authenticate($username, $password);
if ($token) {
// Reset login attempts on successful login
$_SESSION['loginAttempts'] = 0;
// Generate refresh token
$refreshToken = $authService->generateRefreshToken([
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
]);
echo json_encode([
'success' => true,
'token' => $token,
'refreshToken' => $refreshToken,
'user' => [
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
],
'redirect' => '/'
]);
} else {
// Increment login attempts
$_SESSION['loginAttempts'] = $loginAttempts + 1;
// If this failure triggers CAPTCHA, generate it
if ($_SESSION['loginAttempts'] >= 3) {
$captcha = substr(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"), 0, 5);
$_SESSION['captcha'] = $captcha;
echo json_encode([
"error" => "Invalid username or password.",
"captcha" => $captcha
]);
} else {
echo json_encode(["error" => "Invalid username or password."]);
}
}
break;
case "logout":
// Clear session (only used for CAPTCHA)
session_unset();
session_destroy();
echo json_encode([
"success" => true,
"redirect" => '/'
]);
break;
case "refresh":
// Validate refresh token
if (!isset($_POST['refreshToken'])) {
echo json_encode([
"valid" => false,
"error" => "No refresh token provided"
]);
exit;
}
$refreshToken = $_POST['refreshToken'];
$newToken = $authService->refreshToken($refreshToken);
if ($newToken) {
echo json_encode([
"valid" => true,
"token" => $newToken
]);
} else {
echo json_encode([
"valid" => false,
"error" => "Invalid or expired refresh token"
]);
}
break;
case "validate":
// Validate JWT token using the simplified approach
$auth = User::checkAuth(false);
if ($auth) {
echo json_encode([
"valid" => true,
"user" => [
"id" => $auth['uid'],
"username" => $auth['username'],
"accessLevel" => $auth['accessLevel']
]
]);
} else {
echo json_encode([
"valid" => false
]);
}
break;
}
exit;
}
}

View File

@@ -1,10 +0,0 @@
<?php
$view = new stdClass();
$view->pageTitle = 'Page1';
//var_dump($_POST);
require_once("logincontroller.php");
require_once('Views/page1.phtml');

View File

@@ -1,10 +0,0 @@
<?php
$view = new stdClass();
$view->pageTitle = 'Page2';
//var_dump($_POST);
require_once("logincontroller.php");
require_once('Views/page2.phtml');

2078
public/css/bootstrap-icons.css vendored Executable file

File diff suppressed because it is too large Load Diff

2052
public/css/bootstrap-icons.json Executable file

File diff suppressed because it is too large Load Diff

5
public/css/bootstrap-icons.min.css vendored Executable file

File diff suppressed because one or more lines are too long

2090
public/css/bootstrap-icons.scss vendored Executable file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

290
public/css/my-style.css Executable file
View File

@@ -0,0 +1,290 @@
nav, #loginStatus, #filters {
background-color: #3cc471;
color: #111
}
#content.full-height {
/*height: calc(100vh - 413px);*/
flex: 1 0 auto;
}
.main {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.facilityContent {
overflow-y: auto;
}
#title {
margin-top: 12px;
background-color: #fff;
color: #000;
}
#menu {
border-top: solid 6px #000;
background-color: #fff;
color: #fff;
height: 400px;
}
#menu a {
/*background-color: #f00;*/
color: #fff;
text-decoration: none;
display: block;
}
#menu a:hover {
/*background-color: #f00;*/
color: #ddd;
text-decoration:underline;
display: block;
}
#content {
background-color: #fff;
/*border-top: solid 6px #f00;*/
}
#footer {
margin-top: 20px;
text-align: center;
background-color: #bbb;
color: #111;
}
.modal {
z-index: 1055
}
.modal-backdrop {
z-index: 1040;
}
.site-footer {
flex: 0 0 auto;
}
td { white-space:pre-line }
/* Enhanced Facility Table Styles */
#facilityTable {
border-collapse: separate;
border-spacing: 0;
font-size: 0.9rem;
table-layout: fixed;
width: 100%;
}
#facilityTable thead th {
border-bottom: 1px solid #dee2e6;
font-weight: 600;
color: #495057;
font-size: 0.85rem;
padding: 0.5rem;
position: relative;
overflow: hidden;
}
#facilityTable tbody tr {
transition: all 0.2s ease;
}
#facilityTable tbody tr:hover {
background-color: rgba(60, 196, 113, 0.05);
}
#facilityTable tbody td {
vertical-align: middle;
border-bottom: 1px solid #f0f0f0;
line-height: 1.3;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
padding: 0.5rem;
position: relative;
}
/* Prevent content overflow */
#facilityTable th,
#facilityTable td {
box-sizing: border-box;
overflow: hidden;
}
.facility-icon {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
.description-container {
position: relative;
max-width: 100%;
}
.description-container p {
line-height: 1.4;
color: #495057;
margin-bottom: 0;
word-wrap: break-word;
overflow-wrap: break-word;
}
.description-container p.expanded {
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
}
.show-more-btn {
color: #3cc471;
font-size: 0.75rem;
}
.show-more-btn:hover {
color: #2a9d55;
}
/* Badge styling */
.badge.bg-opacity-10 {
font-weight: 500;
letter-spacing: 0.3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-size: 0.75rem;
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
/* Action buttons */
.btn.rounded-circle {
width: 28px;
height: 28px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.btn.rounded-circle:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Ensure action buttons are properly aligned */
.d-flex.justify-content-center.gap-1 {
flex-wrap: nowrap;
}
/* Toggle button styling */
.toggle-content-btn {
display: block;
margin-top: 0.25rem;
font-size: 0.75rem;
color: #3cc471;
}
.toggle-content-btn:hover {
color: #2a9d55;
}
/* Pagination styling */
.pagination .page-link {
color: #3cc471;
border-color: #e9ecef;
}
.pagination .page-item.active .page-link {
background-color: #3cc471;
border-color: #3cc471;
color: white;
}
.pagination .page-link:hover {
background-color: #f8f9fa;
border-color: #e9ecef;
color: #2a9d55;
}
/* Card styling */
.card.shadow {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08) !important;
}
/* Text truncation with ellipsis */
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}
/* Cell content wrapping */
.cell-content {
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
max-height: 3.9em; /* Approximately 3 lines of text */
overflow: hidden;
position: relative;
width: 100%;
}
.address-content {
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
max-height: 2.6em; /* Approximately 2 lines of text */
overflow: hidden;
width: 100%;
}
/* Ensure title column has proper alignment */
.fw-medium .d-flex {
align-items: center;
width: 100%;
}
/* Fix for coordinates column */
.text-nowrap {
white-space: nowrap !important;
}
.navbar-brand img {
transition: transform 0.3s ease;
}
.navbar-brand:hover img {
transform: scale(1.05);
}
.search-controls {
max-width: 800px;
}
.form-control:focus, .form-select:focus {
border-color: #198754;
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);
}
.user-avatar {
width: 32px;
height: 32px;
background-color: #e9ecef;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
}
.user-menu {
display: flex;
align-items: center;
}
@media (max-width: 992px) {
.search-controls {
margin-top: 1rem;
margin-bottom: 1rem;
}
}

307
public/js/apiClient.js Normal file
View File

@@ -0,0 +1,307 @@
/**
* API Client for making authenticated requests to the server
*
* This class provides a wrapper around the Fetch API to handle
* authentication and common request patterns.
*
* The client uses JWT tokens for authentication, which are automatically
* included in requests via the authFetch function provided by the auth service.
*/
class ApiClient {
/**
* Constructor
*
* Initialises the API client and sets up the authenticated fetch function.
* Relies on the auth service being available in the global scope.
*/
constructor() {
// Ensure auth service is available
if (!window.auth) {
console.error('Auth service not available');
}
// Create authenticated fetch function if not already available
this.authFetch = window.authFetch || window.auth?.createAuthFetch() || fetch;
}
/**
* Makes a GET request to the API
*
* This method handles GET requests with query parameters.
* It automatically converts the params object to a query string
* and handles error responses.
*
* @param {string} endpoint - The API endpoint
* @param {Object} params - Query parameters
* @returns {Promise<Object>} The response data
*/
async get(endpoint, params = {}) {
// Build query string
const queryString = Object.keys(params).length > 0
? '?' + new URLSearchParams(params).toString()
: '';
try {
const response = await this.authFetch(`${endpoint}${queryString}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`GET request to ${endpoint} failed:`, error);
throw error;
}
}
/**
* Makes a POST request to the API
*
* This method handles POST requests with either JSON data or FormData.
* It automatically sets the appropriate headers and handles error responses.
*
* @param {string} endpoint - The API endpoint
* @param {Object|FormData} data - The data to send
* @returns {Promise<Object>} The response data
*/
async post(endpoint, data = {}) {
try {
// Prepare request options
const options = {
method: 'POST'
};
// Handle FormData or JSON
if (data instanceof FormData) {
options.body = data;
} else {
options.headers = {
'Content-Type': 'application/json'
};
options.body = JSON.stringify(data);
}
const response = await this.authFetch(endpoint, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`POST request to ${endpoint} failed:`, error);
throw error;
}
}
/**
* Makes a facility-related API request
*
* This is a helper method that simplifies making requests to the facility controller.
* It automatically creates a FormData object with the action and data parameters.
*
* @param {string} action - The action to perform
* @param {Object} data - The data to send
* @returns {Promise<Object>} The response data
*/
async facility(action, data = {}) {
// Create FormData
const formData = new FormData();
formData.append('action', action);
// Add all data to FormData
Object.entries(data).forEach(([key, value]) => {
formData.append(key, value);
});
return this.post('/facilitycontroller.php', formData);
}
/**
* Creates a new facility
*
* This method sends a request to create a new facility with the provided data.
*
* @param {Object} facilityData - The facility data
* @returns {Promise<Object>} The response data
*/
async createFacility(facilityData) {
return this.facility('create', facilityData);
}
/**
* Updates a facility
*
* This method sends a request to update an existing facility with the provided data.
*
* @param {Object} facilityData - The facility data
* @returns {Promise<Object>} The response data
*/
async updateFacility(facilityData) {
return this.facility('update', facilityData);
}
/**
* Deletes a facility
*
* This method sends a request to delete a facility with the specified ID.
*
* @param {number|string} id - The facility ID
* @returns {Promise<Object>} The response data
*/
async deleteFacility(id) {
return this.facility('delete', { id });
}
/**
* Gets a facility by ID
*
* This method retrieves a single facility with the specified ID.
*
* @param {number|string} id - The facility ID
* @returns {Promise<Object>} The response data
*/
async getFacility(id) {
return this.facility('read', { id });
}
/**
* Gets statuses for a facility
*
* This method retrieves all status updates for a facility with the specified ID.
*
* @param {number|string} facilityId - The facility ID
* @returns {Promise<Object>} The response data
*/
async getFacilityStatuses(facilityId) {
return this.facility('getStatuses', { facilityId });
}
/**
* Adds a status to a facility
*
* This method adds a new status update to a facility.
*
* @param {number|string} idStatus - The facility ID
* @param {string} updateStatus - The status comment
* @returns {Promise<Object>} The response data
*/
async addFacilityStatus(idStatus, updateStatus) {
return this.facility('status', { idStatus, updateStatus });
}
/**
* Updates a facility status
*
* This method updates an existing status for a facility.
*
* @param {number|string} statusId - The status ID
* @param {string} editStatus - The updated status comment
* @param {number|string} facilityId - The facility ID
* @returns {Promise<Object>} The response data
*/
async updateFacilityStatus(statusId, editStatus, facilityId) {
return this.facility('editStatus', { statusId, editStatus, facilityId });
}
/**
* Deletes a facility status
*
* This method deletes a status update from a facility.
*
* @param {number|string} statusId - The status ID
* @param {number|string} facilityId - The facility ID
* @returns {Promise<Object>} The response data
*/
async deleteFacilityStatus(statusId, facilityId) {
return this.facility('deleteStatus', { statusId, facilityId });
}
/**
* Authenticates a user
*
* This method sends a login request with the provided credentials.
* It uses a direct fetch call rather than authFetch since the user
* isn't authenticated yet.
*
* @param {string} username - The username
* @param {string} password - The password
* @param {string} captchaInput - The CAPTCHA input (optional)
* @returns {Promise<Object>} The response data
*/
async login(username, password, captchaInput = null) {
const formData = new FormData();
formData.append('action', 'login');
formData.append('username', username);
formData.append('password', password);
if (captchaInput) {
formData.append('captchaInput', captchaInput);
}
const response = await fetch('/logincontroller.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
return await response.json();
}
/**
* Refreshes the access token
*
* This method sends a request to refresh an expired JWT token
* using the provided refresh token.
*
* @param {string} refreshToken - The refresh token
* @returns {Promise<Object>} The response data
*/
async refreshToken(refreshToken) {
const formData = new FormData();
formData.append('action', 'refresh');
formData.append('refreshToken', refreshToken);
const response = await fetch('/logincontroller.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
return await response.json();
}
/**
* Logs out the current user
*
* This method sends a logout request to invalidate the session.
* Note that client-side token removal is handled separately.
*
* @returns {Promise<Object>} The response data
*/
async logout() {
const formData = new FormData();
formData.append('action', 'logout');
const response = await fetch('/logincontroller.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
return await response.json();
}
}
// Initialize API client
const api = new ApiClient();
// Export API client
window.api = api;

614
public/js/auth.js Normal file
View File

@@ -0,0 +1,614 @@
/**
* Authentication service for handling user login, logout, and token management
*
* This class provides a complete authentication solution using JWT tokens.
* It handles token storage, validation, automatic refresh, and authenticated
* API requests.
*/
class AuthService {
/**
* Initialises the authentication service
*
* Loads existing token and user data from localStorage and sets up
* automatic token refresh. This ensures the user stays logged in
* across page refreshes and that tokens are refreshed before they expire.
*/
constructor() {
this.token = localStorage.getItem('token');
this.refreshToken = localStorage.getItem('refreshToken');
this.user = JSON.parse(localStorage.getItem('user'));
this.isValidating = false;
this.loginAttempts = parseInt(localStorage.getItem('loginAttempts') || '0');
this.refreshing = false;
// Set up token refresh interval
this.setupTokenRefresh();
}
/**
* Sets up automatic token refresh
*
* Creates an interval that checks token validity every minute and
* refreshes it if needed. This helps prevent the user from being
* logged out due to token expiration during active use of the application.
*/
setupTokenRefresh() {
// Check token every minute
setInterval(() => {
this.checkAndRefreshToken();
}, 60000); // 1 minute
// Also check immediately
this.checkAndRefreshToken();
}
/**
* Checks if token needs refreshing and refreshes if needed
*
* This method examines the current token's expiry time and refreshes
* it if it's about to expire (within 5 minutes). This provides
* seamless authentication
*/
async checkAndRefreshToken() {
// Skip if already refreshing or no token exists
if (this.refreshing || !this.token || !this.refreshToken) return;
try {
this.refreshing = true;
// Check if token is about to expire (within 5 minutes)
const payload = this.parseJwt(this.token);
if (!payload || !payload.exp) return;
const expiryTime = payload.exp * 1000; // Convert to milliseconds
const currentTime = Date.now();
const timeToExpiry = expiryTime - currentTime;
// If token expires in less than 5 minutes, refresh it
if (timeToExpiry < 300000) { // 5 minutes in milliseconds
await this.refreshAccessToken();
}
} catch (error) {
console.error('Token refresh check failed:', error);
} finally {
this.refreshing = false;
}
}
/**
* Parses a JWT token to extract its payload
*
* This utility method decodes the JWT token without verifying
* its signature. It's used internally to check token expiry
* and extract user information.
*
* @param {string} token - The JWT token to parse
* @returns {Object|null} The decoded token payload or null if invalid
*/
parseJwt(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
} catch (error) {
return null;
}
}
/**
* Refreshes the access token using the refresh token
*
* This method sends the refresh token to the server to obtain
* a new access token when the current one is about to expire.
* It's a crucial part of maintaining persistent authentication.
*
* @returns {Promise<boolean>} True if refresh was successful, false otherwise
*/
async refreshAccessToken() {
if (!this.refreshToken) return false;
try {
console.log('Attempting to refresh access token...');
const formData = new FormData();
formData.append('action', 'refresh');
formData.append('refreshToken', this.refreshToken);
const response = await fetch('/logincontroller.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
console.log('Token refresh response status:', response.status);
const data = await response.json();
console.log('Token refresh response data:', data);
if (data.valid && data.token) {
console.log('Token refresh successful');
this.token = data.token;
localStorage.setItem('token', data.token);
return true;
}
// If refresh failed, log out
if (!data.valid) {
console.log('Token refresh failed, logging out');
this.logout();
}
return false;
} catch (error) {
console.error('Token refresh error details:', error);
error_log('Token refresh failed:', error);
return false;
}
}
/**
* Authenticates a user with the provided credentials
*
* This method sends the login credentials to the server,
* stores the returned tokens and user data if successful,
* and updates the login attempts counter for CAPTCHA handling.
*
* @param {FormData} formData - The form data containing login credentials
* @returns {Promise<Object>} The login result
*/
async login(formData) {
try {
console.log('Attempting to login with URL:', '/logincontroller.php');
const response = await fetch('/logincontroller.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
console.log('Login response status:', response.status);
const data = await response.json();
console.log('Login response data:', data);
if (data.error) {
// If server sends a new CAPTCHA, update it
if (data.captcha) {
const captchaCode = document.getElementById('captchaCode');
const captchaContainer = document.querySelector('.captcha-container');
if (captchaCode && captchaContainer) {
captchaCode.value = data.captcha;
captchaContainer.style.display = 'flex';
}
}
throw new Error(data.error);
}
if (data.success && data.token) {
this.token = data.token;
this.refreshToken = data.refreshToken;
this.user = data.user;
localStorage.setItem('token', data.token);
localStorage.setItem('refreshToken', data.refreshToken);
localStorage.setItem('user', JSON.stringify(data.user));
// Reset login attempts on successful login
localStorage.removeItem('loginAttempts');
// Handle redirect if provided
if (data.redirect) {
window.location.href = data.redirect;
}
return data.user;
}
throw new Error('Login failed');
} catch (error) {
console.error('Login error details:', error);
error_log('Login error:', error);
throw error;
}
}
/**
* Logs the user out
*
* This method clears all authentication data from localStorage
* and notifies the server about the logout. It also updates the
* UI to reflect the logged-out state.
*
* @returns {Promise<Object>} The logout result
*/
async logout() {
try {
const formData = new FormData();
formData.append('action', 'logout');
const response = await fetch('/logincontroller.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
const data = await response.json();
if (!data.success) {
throw new Error('Logout failed');
}
this.token = null;
this.refreshToken = null;
this.user = null;
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
// Handle redirect if provided
if (data.redirect) {
window.location.href = data.redirect;
}
} catch (error) {
error_log('Logout error:', error);
throw error;
}
}
/**
* Checks if the user is currently authenticated
*
* This is a simple helper method that returns true if
* the user object exists, indicating an authenticated user.
*
* @returns {boolean} True if authenticated, false otherwise
*/
isAuthenticated() {
return !!this.token;
}
/**
* Checks if the current user has admin privileges
*
* This helper method checks if the user is authenticated
* and has an access level of 1, which indicates admin status.
*
* @returns {boolean} True if the user is an admin, false otherwise
*/
isAdmin() {
return this.user && this.user.accessLevel === 1;
}
/**
* Checks if CAPTCHA is required for login
*
* This method determines if a CAPTCHA should be shown during login
* based on the number of failed login attempts. This helps prevent
* brute force attacks on the login system.
*
* @returns {boolean} True if CAPTCHA is required, false otherwise
*/
needsCaptcha() {
return this.loginAttempts >= 3;
}
/**
* Validates the current token with the server
*
* This method checks if the current token is still valid by
* making a request to the server. It's used to verify authentication
* status when the application loads.
*
* @returns {Promise<boolean>} True if token is valid, false otherwise
*/
async validateToken() {
// Skip validation if no token exists
if (!this.token) return false;
// Prevent multiple simultaneous validations
if (this.isValidating) return true;
try {
console.log('Validating token...');
this.isValidating = true;
const response = await fetch('/auth.php', {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
console.log('Token validation response status:', response.status);
const data = await response.json();
console.log('Token validation response data:', data);
// Handle invalid token
if (!response.ok || !data.valid) {
console.log('Token is invalid, attempting to refresh...');
// Try to refresh the token
if (this.refreshToken) {
console.log('Refresh token exists, attempting to refresh...');
const refreshed = await this.refreshAccessToken();
if (refreshed) {
console.log('Token refreshed successfully');
return true;
}
}
console.log('Token refresh failed, logging out');
this.logout();
return false;
}
console.log('Token is valid');
return true;
} catch (error) {
console.error('Token validation error details:', error);
error_log('Token validation error:', error);
return false;
} finally {
this.isValidating = false;
}
}
/**
* Gets the current user object
*
* This helper method returns the user object containing
* information about the authenticated user.
*
* @returns {Object|null} The user object or null if not authenticated
*/
getUser() {
console.log('Getting user from auth service:', this.user);
if (!this.user) {
// Try to get user from localStorage as a fallback
const localUser = localStorage.getItem('user');
console.log('User not found in auth service, checking localStorage:', localUser);
if (localUser) {
try {
this.user = JSON.parse(localUser);
} catch (e) {
console.error('Error parsing user from localStorage:', e);
}
}
}
return this.user;
}
/**
* Gets the current JWT token
*
* This helper method returns the current JWT token for
* use in authenticated API requests.
*
* @returns {string|null} The JWT token or null if not authenticated
*/
getToken() {
console.log('Getting token from auth service:', this.token);
if (!this.token) {
// Try to get token from localStorage as a fallback
const localToken = localStorage.getItem('token');
console.log('Token not found in auth service, checking localStorage:', localToken);
if (localToken) {
this.token = localToken;
}
}
return this.token;
}
/**
* Creates an authenticated fetch function
*
* This method returns a wrapper around the fetch API that
* automatically includes the JWT token in the Authorization header.
* It also handles token refresh if a request fails due to an expired token.
*
* @returns {Function} The authenticated fetch function
*/
createAuthFetch() {
return async (url, options = {}) => {
// Ensure options.headers exists
options.headers = options.headers || {};
// Add Authorization header if token exists
if (this.token) {
options.headers['Authorization'] = `Bearer ${this.token}`;
}
// Add X-Requested-With header for AJAX requests
options.headers['X-Requested-With'] = 'XMLHttpRequest';
try {
// Make the request
const response = await fetch(url, options);
// If unauthorized (401), try to refresh the token and retry
if (response.status === 401 && this.refreshToken) {
console.log('Received 401 response, attempting to refresh token...');
const refreshed = await this.refreshAccessToken();
if (refreshed) {
console.log('Token refreshed, retrying request...');
// Update Authorization header with new token
options.headers['Authorization'] = `Bearer ${this.token}`;
// Retry the request
return fetch(url, options);
}
console.log('Token refresh failed, returning 401 response');
}
return response;
} catch (error) {
console.error('Auth fetch error details:', error);
error_log('Auth fetch error:', error);
throw error;
}
};
}
/**
* Checks if the token is valid by parsing it
*
* This method checks if the token is valid by parsing it and checking
* if it has expired. It doesn't make a server request, so it's only
* a client-side check.
*
* @returns {boolean} True if the token is valid, false otherwise
*/
isTokenValid() {
const token = this.getToken();
if (!token) {
console.log('No token found');
return false;
}
try {
const payload = this.parseJwt(token);
console.log('Token payload:', payload);
if (!payload || !payload.exp) {
console.log('Token payload is invalid or missing expiry');
return false;
}
const now = Math.floor(Date.now() / 1000);
const isValid = payload.exp > now;
console.log('Token expiry:', new Date(payload.exp * 1000));
console.log('Current time:', new Date(now * 1000));
console.log('Token is valid:', isValid);
return isValid;
} catch (e) {
console.error('Error parsing token:', e);
return false;
}
}
}
// Initialize authentication service
const auth = new AuthService();
// Create authenticated fetch function
const authFetch = auth.createAuthFetch();
// Set up authentication handlers when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM loaded, setting up authentication handlers');
// Get modal elements
const loginButton = document.querySelector('[data-bs-toggle="modal"]');
const loginModal = document.getElementById('loginModal');
// Set up login form handlers
const loginForm = document.querySelector('#loginModal form');
const loginError = document.querySelector('#loginError');
const captchaContainer = document.querySelector('.captcha-container');
if (loginForm) {
// Handle form submission
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(loginForm);
formData.append('action', 'login');
try {
await auth.login(formData);
// Hide modal before reload
const modal = bootstrap.Modal.getInstance(loginModal);
if (modal) {
modal.hide();
}
// Refresh the page using replace to prevent form resubmission
window.location.replace(window.location.href);
} catch (error) {
if (loginError) {
loginError.textContent = error.message;
loginError.style.display = 'block';
}
// Show CAPTCHA after 3 failed attempts
if (auth.needsCaptcha() && captchaContainer) {
captchaContainer.style.display = 'flex';
}
}
});
}
// Set up logout button handler
const logoutButton = document.getElementById('logoutButton');
if (logoutButton) {
logoutButton.addEventListener('click', async (e) => {
e.preventDefault();
try {
await auth.logout();
window.location.reload();
} catch (error) {
console.error('Logout failed:', error);
}
});
}
// Validate token if authenticated
if (auth.isAuthenticated()) {
console.log('User is authenticated, validating token...');
// Check if we're already in a validation cycle
const validationCycle = sessionStorage.getItem('validationCycle');
if (validationCycle) {
console.log('Already in validation cycle, forcing logout');
// We've already tried to validate in this page load
// Force a proper logout
sessionStorage.removeItem('validationCycle');
auth.logout().finally(() => {
window.location.replace('/');
});
return;
}
// Set validation cycle flag
sessionStorage.setItem('validationCycle', 'true');
auth.validateToken().then(valid => {
console.log('Token validation result:', valid);
if (!valid) {
console.log('Token is invalid, redirecting to login page');
// Token is invalid, redirect to login page
window.location.replace('/');
} else {
console.log('Token is valid, clearing validation cycle flag');
// Clear validation cycle flag
sessionStorage.removeItem('validationCycle');
}
});
} else {
console.log('User is not authenticated');
}
});
// Export auth service and authenticated fetch
window.auth = auth;
window.authFetch = authFetch;
// Make parseJwt accessible from outside
window.auth.parseJwt = auth.parseJwt.bind(auth);
/**
* Custom error logging function
*
* This utility function provides a consistent way to log errors
* throughout the application. It includes both a message and the
* error object for better debugging.
*
* @param {string} message - The error message
* @param {Error} error - The error object
*/
function error_log(message, error) {
console.error(message, error);
}

6314
public/js/bootstrap.bundle.js vendored Normal file

File diff suppressed because it is too large Load Diff

471
public/js/comments.js Normal file
View File

@@ -0,0 +1,471 @@
/**
* Comments functionality for facility management
*/
document.addEventListener('DOMContentLoaded', function() {
console.log('Comments.js loaded');
// Initialize comment modal handlers
initializeCommentModals();
// Set up form handlers
setupCommentFormHandlers();
});
/**
* Initialize comment modals
*/
function initializeCommentModals() {
// Status modal (comments view)
const statusModal = document.getElementById('statusModal');
if (statusModal) {
statusModal.addEventListener('show.bs.modal', function(event) {
console.log('Comments modal is about to show');
// Get the button that triggered the modal
const button = event.relatedTarget;
// Get the facility ID from the data attribute
const facilityId = button.getAttribute('data-facility-id');
console.log('Facility ID for comments:', facilityId);
if (!facilityId) {
console.error('No facility ID found for comments');
return;
}
// Set the facility ID in the comment form
const commentForm = document.getElementById('commentForm');
if (commentForm) {
const facilityIdInput = commentForm.querySelector('#commentFacilityId');
if (facilityIdInput) {
facilityIdInput.value = facilityId;
}
}
// Load facility comments
loadFacilityComments(facilityId);
});
}
// Edit comment modal
const editCommentModal = document.getElementById('editCommentModal');
if (editCommentModal) {
editCommentModal.addEventListener('show.bs.modal', function(event) {
console.log('Edit comment modal is about to show');
// The button that triggered the modal will pass data via dataset
const button = event.relatedTarget;
const commentId = button.getAttribute('data-comment-id');
const commentText = button.getAttribute('data-comment-text');
console.log('Comment ID:', commentId, 'Comment text:', commentText);
// Set the comment ID and text in the form
const editForm = document.getElementById('editCommentForm');
if (editForm) {
const commentIdInput = editForm.querySelector('#editCommentId');
const commentTextArea = editForm.querySelector('#editCommentText');
if (commentIdInput && commentTextArea) {
commentIdInput.value = commentId;
commentTextArea.value = commentText;
}
}
});
}
}
/**
* Set up comment form handlers
*/
function setupCommentFormHandlers() {
// Comment form handler
const commentForm = document.getElementById('commentForm');
if (commentForm) {
setupCommentFormHandler(commentForm);
}
// Edit comment form handler
const editCommentForm = document.getElementById('editCommentForm');
if (editCommentForm) {
setupEditCommentFormHandler(editCommentForm);
}
}
/**
* Set up a single comment form handler
* @param {HTMLFormElement} commentForm - The comment form element
*/
function setupCommentFormHandler(commentForm) {
commentForm.addEventListener('submit', async function(e) {
e.preventDefault();
// Prevent duplicate submissions
if (this.submitting) {
return;
}
this.submitting = true;
// Check if user is authenticated
if (!isAuthenticated()) {
alert('You must be logged in to add comments');
this.submitting = false;
return;
}
const formData = new FormData(this);
// Get form data
const commentText = formData.get('commentText');
const facilityId = formData.get('facilityId');
console.log('Comment form data:', { facilityId, commentText });
try {
console.log('Sending comment request...');
// Use the API client to add a status comment
const data = await window.api.addFacilityStatus(facilityId, commentText);
console.log('Comment response:', data);
if (data.success) {
console.log('Comment added successfully');
// Reset the form
this.reset();
// Reload comments to show the new one
loadFacilityComments(facilityId);
} else {
console.error('Comment failed:', data.error);
alert(data.error || 'Failed to add comment');
}
} catch (error) {
console.error('Error adding comment:', error);
alert('Failed to add comment: ' + error.message);
} finally {
this.submitting = false;
}
});
}
/**
* Set up a single edit comment form handler
* @param {HTMLFormElement} editCommentForm - The edit comment form element
*/
function setupEditCommentFormHandler(editCommentForm) {
editCommentForm.addEventListener('submit', async function(e) {
e.preventDefault();
// Prevent duplicate submissions
if (this.submitting) {
return;
}
this.submitting = true;
// Check if user is authenticated
if (!isAuthenticated()) {
alert('You must be logged in to edit comments');
this.submitting = false;
return;
}
const formData = new FormData(this);
// Get form data
const commentText = formData.get('editCommentText');
const commentId = formData.get('commentId');
const facilityId = document.getElementById('commentFacilityId').value;
console.log('Edit comment form data:', { commentId, facilityId, commentText });
try {
console.log('Sending edit comment request...');
// Use the API client to update a status comment
const data = await window.api.updateFacilityStatus(commentId, commentText, facilityId);
console.log('Edit comment response:', data);
if (data.success) {
console.log('Comment edited successfully');
// Close the edit modal
const editModal = bootstrap.Modal.getInstance(document.getElementById('editCommentModal'));
if (editModal) {
editModal.hide();
}
// Reload comments to show the updated one
loadFacilityComments(facilityId);
} else {
console.error('Edit comment failed:', data.error);
alert(data.error || 'Failed to edit comment');
}
} catch (error) {
console.error('Error editing comment:', error);
alert('Failed to edit comment: ' + error.message);
} finally {
this.submitting = false;
}
});
}
/**
* Creates a comment form dynamically for authenticated users
* @param {string} facilityId - The facility ID
*/
function createCommentFormForAuthenticatedUser(facilityId) {
// Check if user is authenticated
if (!isAuthenticated()) {
return `
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle me-2"></i>
Please <a href="#" data-bs-toggle="modal" data-bs-target="#loginModal">login</a> to add comments.
</div>
`;
}
// Create the comment form
return `
<form id="commentForm" class="mt-3">
<input type="hidden" id="commentFacilityId" name="facilityId" value="${escapeHtml(facilityId)}">
<div class="mb-3">
<label for="commentText" class="form-label">Add a Comment</label>
<textarea class="form-control" id="commentText" name="commentText" rows="3" required></textarea>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-success">
<i class="bi bi-chat-dots-fill me-1"></i>
Add Comment
</button>
</div>
</form>
`;
}
/**
* Checks if the current user is authenticated
* @returns {boolean} True if authenticated, false otherwise
*/
function isAuthenticated() {
// Use the auth service to check authentication
return window.auth.isAuthenticated();
}
/**
* Loads facility comments from the server
* @param {string} facilityId - The facility ID
*/
async function loadFacilityComments(facilityId) {
try {
console.log('Loading comments for facility:', facilityId);
// Show loading indicator
const commentsContainer = document.getElementById('commentsContainer');
if (commentsContainer) {
commentsContainer.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading comments...</p>
</div>
`;
}
// Use the API client to get facility statuses
const data = await window.api.getFacilityStatuses(facilityId);
console.log('Comments loaded:', data);
if (data.success) {
// Render the comments
renderComments(data.statuses, facilityId);
} else {
console.error('Failed to load comments:', data.error);
if (commentsContainer) {
commentsContainer.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Failed to load comments: ${data.error || 'Unknown error'}
</div>
`;
}
}
} catch (error) {
console.error('Error loading comments:', error);
const commentsContainer = document.getElementById('commentsContainer');
if (commentsContainer) {
commentsContainer.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Error loading comments: ${error.message}
</div>
`;
}
}
}
/**
* Renders comments in the comments container
* @param {Array} comments - Array of comment objects
* @param {string} facilityId - The facility ID
*/
function renderComments(comments, facilityId) {
const commentsContainer = document.getElementById('commentsContainer');
if (!commentsContainer) return;
// Clear the container
commentsContainer.innerHTML = '';
// Add the comment form for authenticated users
commentsContainer.innerHTML += createCommentFormForAuthenticatedUser(facilityId);
// If no comments, show a message
if (!comments || comments.length === 0) {
commentsContainer.innerHTML += `
<div class="alert alert-light mt-3">
<i class="bi bi-chat-dots me-2"></i>
No comments yet. Be the first to add a comment!
</div>
`;
return;
}
// Create the comments list
const commentsList = document.createElement('div');
commentsList.className = 'comments-list mt-4';
// Add each comment
comments.forEach(comment => {
const commentElement = document.createElement('div');
commentElement.className = 'comment-item card mb-3 border-0 shadow-sm';
// Format the date
const date = new Date(comment.timestamp);
const formattedDate = date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// Check if the current user is the comment author or an admin
const canEdit = isAdmin() || isCurrentUser(comment.username);
commentElement.innerHTML = `
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="d-flex align-items-center">
<div class="comment-avatar bg-light rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
<i class="bi bi-person-fill text-secondary"></i>
</div>
<div>
<h6 class="mb-0 fw-bold">${escapeHtml(comment.username)}</h6>
<small class="text-muted">${formattedDate}</small>
</div>
</div>
${canEdit ? `
<div class="dropdown">
<button class="btn btn-sm btn-light" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#editCommentModal" data-comment-id="${comment.id}" data-comment-text="${escapeHtml(comment.comment)}">
<i class="bi bi-pencil me-2"></i>Edit
</button>
</li>
<li>
<button class="dropdown-item text-danger" type="button" onclick="deleteComment('${comment.id}', '${facilityId}')">
<i class="bi bi-trash me-2"></i>Delete
</button>
</li>
</ul>
</div>
` : ''}
</div>
<p class="mb-0">${escapeHtml(comment.comment)}</p>
</div>
`;
commentsList.appendChild(commentElement);
});
commentsContainer.appendChild(commentsList);
// Re-initialize the comment form handler
const commentForm = document.getElementById('commentForm');
if (commentForm) {
setupCommentFormHandler(commentForm);
}
}
/**
* Deletes a comment
* @param {string} commentId - The comment ID
* @param {string} facilityId - The facility ID
*/
async function deleteComment(commentId, facilityId) {
// Confirm deletion
if (!confirm('Are you sure you want to delete this comment?')) {
return;
}
try {
console.log('Deleting comment:', commentId, 'for facility:', facilityId);
// Use the API client to delete a status comment
const data = await window.api.deleteFacilityStatus(commentId, facilityId);
console.log('Delete comment response:', data);
if (data.success) {
console.log('Comment deleted successfully');
// Reload comments to reflect the deletion
loadFacilityComments(facilityId);
} else {
console.error('Delete comment failed:', data.error);
alert(data.error || 'Failed to delete comment');
}
} catch (error) {
console.error('Error deleting comment:', error);
alert('Failed to delete comment: ' + error.message);
}
}
/**
* Checks if the current user is an admin
* @returns {boolean} True if admin, false otherwise
*/
function isAdmin() {
// Use the auth service to check if user is admin
return window.auth.isAdmin();
}
/**
* Checks if the given username matches the current user
* @param {string} username - The username to check
* @returns {boolean} True if current user, false otherwise
*/
function isCurrentUser(username) {
const user = window.auth.getUser();
return user && user.username === username;
}
/**
* Safely escapes HTML special characters to prevent XSS attacks
* @param {*} unsafe - The value to escape
* @returns {string} The escaped string
*/
function escapeHtml(unsafe) {
if (unsafe === null || unsafe === undefined) {
return '';
}
return unsafe
.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

1197
public/js/facilityData.js Normal file

File diff suppressed because it is too large Load Diff

152
public/js/simpleAuth.js Normal file
View File

@@ -0,0 +1,152 @@
/**
* Simplified Authentication Helper
*
* This provides a streamlined authentication solution that works with
* the simplified server-side authentication approach.
*/
class SimpleAuth {
/**
* Initialize the authentication helper
*/
constructor() {
this.token = localStorage.getItem('token');
this.user = JSON.parse(localStorage.getItem('user') || 'null');
}
/**
* Parse a JWT token to extract its payload
* @param {string} token - The JWT token to parse
* @returns {object|null} The decoded payload or null if invalid
*/
parseJwt(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
} catch (e) {
console.error('Error parsing JWT token:', e);
return null;
}
}
/**
* Login a user
* @param {object} credentials - The user credentials (username, password)
* @returns {Promise<object>} The login result
*/
async login(credentials) {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Login failed');
}
// Store token and user data
this.token = data.token;
localStorage.setItem('token', data.token);
// Parse user data from token
const payload = this.parseJwt(data.token);
this.user = {
id: payload.uid,
username: payload.username,
accessLevel: payload.accessLevel
};
localStorage.setItem('user', JSON.stringify(this.user));
return {
success: true,
user: this.user
};
} catch (error) {
console.error('Login error:', error);
return {
success: false,
error: error.message
};
}
}
/**
* Logout the current user
*/
logout() {
this.token = null;
this.user = null;
localStorage.removeItem('token');
localStorage.removeItem('user');
// Redirect to home page
window.location.href = '/';
}
/**
* Check if the user is authenticated
* @returns {boolean} True if authenticated, false otherwise
*/
isAuthenticated() {
return !!this.token && !!this.user;
}
/**
* Check if the user is an admin
* @returns {boolean} True if admin, false otherwise
*/
isAdmin() {
return this.isAuthenticated() && this.user.accessLevel === 1;
}
/**
* Get the current user
* @returns {object|null} The current user or null if not authenticated
*/
getUser() {
return this.user;
}
/**
* Get the authentication token
* @returns {string|null} The token or null if not authenticated
*/
getToken() {
return this.token;
}
/**
* Make an authenticated API request
* @param {string} url - The URL to fetch
* @param {object} options - Fetch options
* @returns {Promise<Response>} The fetch response
*/
async fetchAuth(url, options = {}) {
if (!this.token) {
throw new Error('Not authenticated');
}
const headers = {
...options.headers,
'Authorization': `Bearer ${this.token}`
};
return fetch(url, {
...options,
headers
});
}
}
// Create a global instance
const auth = new SimpleAuth();