OHO-6(Entire Working MVP (Auth))
This commit is contained in:
parent
9a5603e896
commit
0558ae9fd7
18 changed files with 1286 additions and 207 deletions
60
.gitignore
vendored
60
.gitignore
vendored
|
|
@ -1,10 +1,64 @@
|
|||
# ----------------------
|
||||
# General ignores
|
||||
# ----------------------
|
||||
|
||||
# Ignore any environment files with .env extensions globally
|
||||
*.env
|
||||
|
||||
*.md
|
||||
# Ignore markdown files if you want (usually you keep .md, so exclude these lines if you want to track docs)
|
||||
# *.md
|
||||
|
||||
# ----------------------
|
||||
# Backend specific ignores
|
||||
# ----------------------
|
||||
|
||||
# Ignore dotenv config
|
||||
# Ignore backend env file explicitly
|
||||
Backend/.env
|
||||
|
||||
# Ignore Dart environment file
|
||||
# Node modules and logs (optional in root if only backend uses node)
|
||||
# If Backend/.gitignore already has node_modules, this can be omitted here
|
||||
# Backend/node_modules/
|
||||
# Backend/logs/
|
||||
|
||||
# ----------------------
|
||||
# Frontend specific ignores
|
||||
# ----------------------
|
||||
|
||||
# Dart/Flutter environment config to ignore
|
||||
Frontend/lib/other_pages/enviroment.dart
|
||||
|
||||
# Flutter build folders
|
||||
Frontend/.dart_tool/
|
||||
Frontend/build/
|
||||
Frontend/.packages
|
||||
Frontend/.flutter-plugins
|
||||
Frontend/.flutter-plugins-dependencies
|
||||
Frontend/.idea/
|
||||
Frontend/ios/Pods/
|
||||
Frontend/pubspec.lock
|
||||
|
||||
# Android specific (sometimes frontend has android/ folder)
|
||||
Frontend/android/.gradle/
|
||||
Frontend/android/app/build/
|
||||
|
||||
# ----------------------
|
||||
# IDE and OS files
|
||||
# ----------------------
|
||||
|
||||
# VSCode settings folder
|
||||
.vscode/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Linux
|
||||
*~
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# ----------------------
|
||||
# Logs
|
||||
# ----------------------
|
||||
*.log
|
||||
|
|
|
|||
89
Backend/.gitignore
vendored
Normal file
89
Backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Node modules - never commit dependencies
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov/
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output/
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt/
|
||||
|
||||
# Bower dependency directory
|
||||
bower_components/
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release/
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm/
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next/
|
||||
|
||||
# Parcel-bundler cache (https://parceljs.org/)
|
||||
.cache/
|
||||
|
||||
# SvelteKit build / cache directories
|
||||
.svelte-kit/
|
||||
|
||||
# Vuepress build output
|
||||
.vuepress/dist/
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# VS Code settings folder (if you want, override in root .gitignore instead)
|
||||
.vscode/
|
||||
|
||||
# npm package lock files (optional, you may keep these)
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
361
Backend/index.js
361
Backend/index.js
|
|
@ -1,179 +1,218 @@
|
|||
require('dotenv').config();
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const mongoose = require('mongoose');
|
||||
const axios = require('axios');
|
||||
const cors = require('cors');
|
||||
const passport = require('passport');
|
||||
const session = require('express-session');
|
||||
const GoogleStrategy = require('passport-google-oauth20').Strategy;
|
||||
const { OAuth2Client } = require('google-auth-library');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.json());
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || '*'
|
||||
origin: process.env.CORS_ORIGIN || '*',
|
||||
credentials: true
|
||||
}));
|
||||
// Prediction route
|
||||
const predictRoute = require('./predict');
|
||||
app.use('/api/predict', predictRoute);
|
||||
|
||||
/* ---------- MODELS ---------- */
|
||||
const userSchema = new mongoose.Schema({
|
||||
googleId: { type: String, required: true, unique: true },
|
||||
displayName: String,
|
||||
email: String,
|
||||
photo: String,
|
||||
accessToken: String,
|
||||
refreshToken: String,
|
||||
createdAt: { type: Date, default: Date.now }
|
||||
});
|
||||
const User = mongoose.model('User', userSchema);
|
||||
|
||||
const transactionSchema = new mongoose.Schema({
|
||||
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
description: String,
|
||||
amount: Number,
|
||||
category: String,
|
||||
date: { type: Date, default: Date.now },
|
||||
});
|
||||
const Transaction = mongoose.model('Transaction', transactionSchema);
|
||||
|
||||
/* ---------- SESSION ---------- */
|
||||
const sessionOptions = {
|
||||
secret: process.env.JWT_SECRET || 'change_this_secret',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax'
|
||||
}
|
||||
};
|
||||
app.use(session(sessionOptions));
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
/* ---------- PASSPORT ---------- */
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user._id);
|
||||
});
|
||||
passport.deserializeUser(async (id, done) => {
|
||||
try {
|
||||
const user = await User.findById(id).lean();
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
|
||||
passport.use(new GoogleStrategy({
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: process.env.GOOGLE_REDIRECT_URI
|
||||
}, async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
const email = profile.emails && profile.emails[0] && profile.emails[0].value;
|
||||
const update = {
|
||||
displayName: profile.displayName,
|
||||
email,
|
||||
accessToken,
|
||||
...(refreshToken ? { refreshToken } : {})
|
||||
};
|
||||
const opts = { upsert: true, new: true, setDefaultsOnInsert: true };
|
||||
const user = await User.findOneAndUpdate({ googleId: profile.id }, update, opts);
|
||||
return done(null, user);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
}));
|
||||
|
||||
/* ---------- GOOGLE OAUTH ROUTES (WEB) ---------- */
|
||||
app.get('/auth/google', passport.authenticate('google', {
|
||||
scope: ['profile', 'email', 'https://www.googleapis.com/auth/gmail.readonly'],
|
||||
accessType: 'offline',
|
||||
prompt: 'consent'
|
||||
}));
|
||||
|
||||
app.get('/auth/google/callback',
|
||||
passport.authenticate('google', { failureRedirect: '/' }),
|
||||
(req, res) => {
|
||||
res.redirect('/profile');
|
||||
}
|
||||
);
|
||||
|
||||
/* ---------- GOOGLE TOKEN LOGIN (MOBILE / FLUTTER) ---------- */
|
||||
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
|
||||
app.post('/auth/google/token', async (req, res) => {
|
||||
try {
|
||||
const { idToken } = req.body;
|
||||
const ticket = await client.verifyIdToken({
|
||||
idToken,
|
||||
audience: process.env.GOOGLE_CLIENT_ID,
|
||||
});
|
||||
|
||||
const payload = ticket.getPayload();
|
||||
const user = await User.findOneAndUpdate(
|
||||
{ googleId: payload.sub },
|
||||
{
|
||||
googleId: payload.sub,
|
||||
displayName: payload.name,
|
||||
email: payload.email,
|
||||
photo: payload.picture
|
||||
},
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
|
||||
res.json({ success: true, user });
|
||||
} catch (err) {
|
||||
res.status(401).json({ error: 'Invalid token', details: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- USER ROUTES ---------- */
|
||||
app.get('/profile', (req, res) => {
|
||||
if (!req.user) return res.status(401).json({ error: 'Not logged in' });
|
||||
res.json({
|
||||
id: req.user._id,
|
||||
name: req.user.displayName,
|
||||
email: req.user.email
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/logout', (req, res) => {
|
||||
req.logout(err => {
|
||||
if (err) console.error('Logout error', err);
|
||||
req.session.destroy(() => {
|
||||
res.clearCookie('connect.sid', { path: '/' });
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- TRANSACTION ROUTES (NOW USER-SPECIFIC) ---------- */
|
||||
app.post('/api/transaction/add', async (req, res) => {
|
||||
try {
|
||||
const { description, amount, userId } = req.body;
|
||||
if (!userId) return res.status(400).json({ error: 'Missing userId' });
|
||||
|
||||
const predictRes = await axios.post('http://192.168.1.10:5000/api/predict', {
|
||||
description,
|
||||
});
|
||||
const category = predictRes.data.category || 'Other';
|
||||
|
||||
const newTransaction = new Transaction({
|
||||
userId,
|
||||
description,
|
||||
amount,
|
||||
category
|
||||
});
|
||||
await newTransaction.save();
|
||||
|
||||
res.status(200).json({ message: '✅ Transaction saved', data: newTransaction });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/transactions', async (req, res) => {
|
||||
try {
|
||||
const { category, userId } = req.query;
|
||||
if (!userId) return res.status(400).json({ error: 'Missing userId' });
|
||||
|
||||
let query = { userId };
|
||||
if (category && category !== 'All') query.category = category;
|
||||
|
||||
const transactions = await Transaction.find(query).sort({ date: -1 });
|
||||
res.status(200).json({ success: true, data: transactions });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/transactions/recent', async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.query;
|
||||
if (!userId) return res.status(400).json({ error: 'Missing userId' });
|
||||
|
||||
const transactions = await Transaction.find({ userId })
|
||||
.sort({ date: -1 })
|
||||
.limit(5);
|
||||
|
||||
res.status(200).json({ success: true, data: transactions });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- SERVER ---------- */
|
||||
mongoose.connect(process.env.MONGO_URI, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
})
|
||||
.then(() => console.log('MongoDB connected'))
|
||||
.catch(err => console.error('MongoDB connection error:', err));
|
||||
.then(() => console.log('MongoDB connected'))
|
||||
.catch(err => console.error('MongoDB connection error:', err));
|
||||
|
||||
|
||||
// Define schema and model
|
||||
const transactionSchema = new mongoose.Schema({
|
||||
description: String,
|
||||
amount: Number,
|
||||
|
||||
category: String,
|
||||
date: { type: Date, default: Date.now },
|
||||
});
|
||||
|
||||
const Transaction = mongoose.model('Transaction', transactionSchema);
|
||||
|
||||
|
||||
|
||||
// POST endpoint to add transaction with auto-categorization
|
||||
app.post('/api/transaction/add', async (req, res) => {
|
||||
try {
|
||||
const { description, amount } = req.body;
|
||||
|
||||
// Call your AI prediction endpoint
|
||||
const predictRes = await axios.post('http://192.168.1.10:5000/api/predict', {
|
||||
description,
|
||||
});
|
||||
|
||||
const category = predictRes.data.category || 'Other';
|
||||
|
||||
const newTransaction = new Transaction({
|
||||
description,
|
||||
amount,
|
||||
category,
|
||||
});
|
||||
|
||||
await newTransaction.save();
|
||||
|
||||
res.status(200).json({
|
||||
message: '✅ Transaction saved with category',
|
||||
data: newTransaction,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('❌ Error adding transaction:', err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/transaction/update', async (req, res) => {
|
||||
try {
|
||||
const { budget } = req.body;
|
||||
// Save to database or wherever you store budgets
|
||||
await BudgetModel.updateOne({}, { amount: budget }, { upsert: true });
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update budget' });
|
||||
}
|
||||
});
|
||||
// GET transactions by category
|
||||
app.get('/api/transactions', async (req, res) => {
|
||||
try {
|
||||
const category = req.query.category;
|
||||
|
||||
let query = {};
|
||||
if (category && category !== 'All') {
|
||||
query.category = category;
|
||||
}
|
||||
|
||||
const transactions = await Transaction.find(query).sort({ date: -1 });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: transactions.map(tx => ({
|
||||
id: tx._id,
|
||||
description: tx.description,
|
||||
amount: tx.amount,
|
||||
category: tx.category,
|
||||
date: tx.date,
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
// Get 5 most recent transactions
|
||||
app.get('/api/transactions/recent', async (req, res) => {
|
||||
try {
|
||||
const transactions = await Transaction.find({})
|
||||
.sort({ date: -1 })
|
||||
.limit(5); // 👈 limit to 5
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: transactions.map(tx => ({
|
||||
id: tx._id,
|
||||
description: tx.description,
|
||||
amount: tx.amount,
|
||||
category: tx.category,
|
||||
date: tx.date,
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/transactions/voice', async (req, res) => {
|
||||
try {
|
||||
const { voiceInput } = req.body;
|
||||
|
||||
if (!voiceInput) {
|
||||
return res.status(400).json({ error: 'No voice input provided' });
|
||||
}
|
||||
|
||||
// Step 1: Use regex to extract amount
|
||||
const amountMatch = voiceInput.match(/(?:\₹|\$)?(\d+(?:\.\d{1,2})?)/);
|
||||
const amount = amountMatch ? parseFloat(amountMatch[1]) : null;
|
||||
|
||||
// Step 2: Extract description (rough logic: remove common verbs + amount)
|
||||
const cleaned = voiceInput
|
||||
.toLowerCase()
|
||||
.replace(/(bought|added|paid|spent|for|on)/g, '')
|
||||
.replace(/₹?\d+/, '')
|
||||
.trim();
|
||||
|
||||
const description = cleaned || 'misc';
|
||||
|
||||
// Step 3: Predict category using your Flask API
|
||||
const predictRes = await axios.post('http://192.168.1.10:5000/api/predict', {
|
||||
description,
|
||||
});
|
||||
|
||||
const category = predictRes.data?.category || 'Other';
|
||||
|
||||
// Step 4: Save to DB
|
||||
const newTransaction = new Transaction({
|
||||
description,
|
||||
amount,
|
||||
category,
|
||||
});
|
||||
|
||||
await newTransaction.save();
|
||||
|
||||
res.status(200).json({
|
||||
message: '✅ Voice transaction saved',
|
||||
data: newTransaction,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('❌ Voice transaction error:', err.message);
|
||||
res.status(500).json({ error: 'Server error during voice transaction' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//server
|
||||
const HOST = process.env.HOST || '0.0.0.0'; // listen on all interfaces
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`🚀 Server running on http://${HOST}:${PORT}`);
|
||||
});
|
||||
|
||||
|
|
|
|||
346
Backend/index1.js
Normal file
346
Backend/index1.js
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const mongoose = require('mongoose');
|
||||
const axios = require('axios');
|
||||
const cors = require('cors');
|
||||
const passport = require('passport');
|
||||
const session = require('express-session');
|
||||
const GoogleStrategy = require('passport-google-oauth20').Strategy;
|
||||
const { OAuth2Client } = require('google-auth-library');
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || '*'
|
||||
}));
|
||||
/* ---------- BEGIN: Replace OAuth / Passport / Session block with this ---------- */
|
||||
const userSchema = new mongoose.Schema({
|
||||
googleId: { type: String, required: true, unique: true },
|
||||
displayName: String,
|
||||
email: String,
|
||||
photo: String,
|
||||
accessToken: String,
|
||||
refreshToken: String,
|
||||
createdAt: { type: Date, default: Date.now }
|
||||
});
|
||||
|
||||
const sessionOptions = {
|
||||
secret: process.env.JWT_SECRET || 'change_this_secret',
|
||||
resave: false,
|
||||
saveUninitialized: false, // fixed spelling
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production', // set true only on HTTPS in prod
|
||||
httpOnly: true,
|
||||
sameSite: 'lax'
|
||||
}
|
||||
};
|
||||
app.use(session(sessionOptions));
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
/* Passport user serialization (store minimal info in session) */
|
||||
passport.serializeUser((user, done) => {
|
||||
// store only user id (or email) in session
|
||||
done(null, user._id || user.id || user);
|
||||
});
|
||||
passport.deserializeUser(async (id, done) => {
|
||||
try {
|
||||
// adapt to your user model - this assumes a Users collection/model exists
|
||||
const Users = mongoose.model('User'); // ensure User model is defined somewhere else in your code
|
||||
const user = await Users.findById(id).lean();
|
||||
done(null, user || id);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
|
||||
/* Google OAuth strategy — capture tokens (access + refresh). */
|
||||
passport.use(new GoogleStrategy({
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: process.env.GOOGLE_REDIRECT_URI // must match GCP console entry
|
||||
},
|
||||
async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
// Here we either create or update a user record in DB and save tokens.
|
||||
// Adjust schema fields to your Users collection.
|
||||
const Users = mongoose.model('User' , userSchema);
|
||||
const email = profile.emails && profile.emails[0] && profile.emails[0].value;
|
||||
|
||||
const update = {
|
||||
name: profile.displayName || profile.username,
|
||||
email,
|
||||
'gmail.accessToken': accessToken,
|
||||
// only store refreshToken if provided. Google provides refreshToken on first consent or when prompt=consent
|
||||
...(refreshToken ? { 'gmail.refreshToken': refreshToken } : {}),
|
||||
'gmail.tokenSavedAt': new Date()
|
||||
};
|
||||
|
||||
// Upsert user by email (or however you identify users)
|
||||
const opts = { upsert: true, new: true, setDefaultsOnInsert: true };
|
||||
const user = await Users.findOneAndUpdate({ email }, update, opts);
|
||||
|
||||
// Return the user to passport
|
||||
return done(null, user);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
/* OAuth start: request Gmail readonly scope + offline access for refreshToken */
|
||||
app.get('/auth/google', passport.authenticate('google', {
|
||||
scope: [
|
||||
'profile',
|
||||
'email',
|
||||
'https://www.googleapis.com/auth/gmail.readonly'
|
||||
],
|
||||
accessType: 'offline', // request refresh token
|
||||
prompt: 'consent' // forces refresh token to be returned (on first consent)
|
||||
}));
|
||||
|
||||
/* OAuth callback */
|
||||
app.get('/auth/google/callback',
|
||||
passport.authenticate('google', { failureRedirect: '/' }),
|
||||
(req, res) => {
|
||||
// success — redirect to profile or SPA route
|
||||
res.redirect('/profile');
|
||||
}
|
||||
);
|
||||
|
||||
/* Profile route (example) */
|
||||
app.get('/profile', (req, res) => {
|
||||
if (!req.user) return res.redirect('/');
|
||||
// send sanitized user object
|
||||
const safeUser = {
|
||||
id: req.user._id || req.user.id,
|
||||
name: req.user.name || req.user.displayName,
|
||||
email: req.user.email
|
||||
};
|
||||
res.send(`Welcome ${safeUser.name} (${safeUser.email})`);
|
||||
});
|
||||
|
||||
/* Logout route */
|
||||
app.get('/logout', (req, res) => {
|
||||
req.logout(err => {
|
||||
// in newer passport versions, logout takes a callback
|
||||
if (err) console.error('Logout error', err);
|
||||
req.session.destroy(() => {
|
||||
res.clearCookie('connect.sid', { path: '/' });
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
|
||||
|
||||
app.post('/auth/google/token', async (req, res) => {
|
||||
try {
|
||||
const { idToken } = req.body;
|
||||
const ticket = await client.verifyIdToken({
|
||||
idToken,
|
||||
audience: process.env.GOOGLE_CLIENT_ID,
|
||||
});
|
||||
|
||||
const payload = ticket.getPayload();
|
||||
const user = await User.findOneAndUpdate(
|
||||
{ googleId: payload.sub },
|
||||
{
|
||||
googleId: payload.sub,
|
||||
displayName: payload.name,
|
||||
email: payload.email,
|
||||
photo: payload.picture,
|
||||
},
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
|
||||
res.json({ success: true, user });
|
||||
} catch (err) {
|
||||
res.status(401).json({ error: 'Invalid token', details: err.message });
|
||||
}
|
||||
});
|
||||
/* ---------- END: Replace OAuth / Passport / Session block ---------- */
|
||||
|
||||
|
||||
// Prediction route
|
||||
const predictRoute = require('./predict');
|
||||
app.use('/api/predict', predictRoute);
|
||||
|
||||
mongoose.connect(process.env.MONGO_URI, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
})
|
||||
.then(() => console.log('MongoDB connected'))
|
||||
.catch(err => console.error('MongoDB connection error:', err));
|
||||
|
||||
// models/User.js
|
||||
// const mongoose = require('mongoose');
|
||||
|
||||
// const userSchema = new mongoose.Schema({
|
||||
// googleId: { type: String, required: true, unique: true },
|
||||
// displayName: String,
|
||||
// email: String,
|
||||
// photo: String,
|
||||
// accessToken: String,
|
||||
// refreshToken: String,
|
||||
// createdAt: { type: Date, default: Date.now }
|
||||
// });
|
||||
|
||||
// module.exports = mongoose.model('User', userSchema);
|
||||
|
||||
|
||||
// Define schema and model
|
||||
const transactionSchema = new mongoose.Schema({
|
||||
description: String,
|
||||
amount: Number,
|
||||
|
||||
category: String,
|
||||
date: { type: Date, default: Date.now },
|
||||
});
|
||||
|
||||
const Transaction = mongoose.model('Transaction', transactionSchema);
|
||||
|
||||
|
||||
|
||||
// POST endpoint to add transaction with auto-categorization
|
||||
app.post('/api/transaction/add', async (req, res) => {
|
||||
try {
|
||||
const { description, amount } = req.body;
|
||||
|
||||
// Call your AI prediction endpoint
|
||||
const predictRes = await axios.post('http://192.168.1.10:5000/api/predict', {
|
||||
description,
|
||||
});
|
||||
|
||||
const category = predictRes.data.category || 'Other';
|
||||
|
||||
const newTransaction = new Transaction({
|
||||
description,
|
||||
amount,
|
||||
category,
|
||||
});
|
||||
|
||||
await newTransaction.save();
|
||||
|
||||
res.status(200).json({
|
||||
message: '✅ Transaction saved with category',
|
||||
data: newTransaction,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('❌ Error adding transaction:', err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/transaction/update', async (req, res) => {
|
||||
try {
|
||||
const { budget } = req.body;
|
||||
// Save to database or wherever you store budgets
|
||||
await BudgetModel.updateOne({}, { amount: budget }, { upsert: true });
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update budget' });
|
||||
}
|
||||
});
|
||||
// GET transactions by category
|
||||
app.get('/api/transactions', async (req, res) => {
|
||||
try {
|
||||
const category = req.query.category;
|
||||
|
||||
let query = {};
|
||||
if (category && category !== 'All') {
|
||||
query.category = category;
|
||||
}
|
||||
|
||||
const transactions = await Transaction.find(query).sort({ date: -1 });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: transactions.map(tx => ({
|
||||
id: tx._id,
|
||||
description: tx.description,
|
||||
amount: tx.amount,
|
||||
category: tx.category,
|
||||
date: tx.date,
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
// Get 5 most recent transactions
|
||||
app.get('/api/transactions/recent', async (req, res) => {
|
||||
try {
|
||||
const transactions = await Transaction.find({})
|
||||
.sort({ date: -1 })
|
||||
.limit(5); // 👈 limit to 5
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: transactions.map(tx => ({
|
||||
id: tx._id,
|
||||
description: tx.description,
|
||||
amount: tx.amount,
|
||||
category: tx.category,
|
||||
date: tx.date,
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/transactions/voice', async (req, res) => {
|
||||
try {
|
||||
const { voiceInput } = req.body;
|
||||
|
||||
if (!voiceInput) {
|
||||
return res.status(400).json({ error: 'No voice input provided' });
|
||||
}
|
||||
|
||||
// Step 1: Use regex to extract amount
|
||||
const amountMatch = voiceInput.match(/(?:\₹|\$)?(\d+(?:\.\d{1,2})?)/);
|
||||
const amount = amountMatch ? parseFloat(amountMatch[1]) : null;
|
||||
|
||||
// Step 2: Extract description (rough logic: remove common verbs + amount)
|
||||
const cleaned = voiceInput
|
||||
.toLowerCase()
|
||||
.replace(/(bought|added|paid|spent|for|on)/g, '')
|
||||
.replace(/₹?\d+/, '')
|
||||
.trim();
|
||||
|
||||
const description = cleaned || 'misc';
|
||||
|
||||
// Step 3: Predict category using your Flask API
|
||||
const predictRes = await axios.post('http://192.168.1.10:5000/api/predict', {
|
||||
description,
|
||||
});
|
||||
|
||||
const category = predictRes.data?.category || 'Other';
|
||||
|
||||
// Step 4: Save to DB
|
||||
const newTransaction = new Transaction({
|
||||
description,
|
||||
amount,
|
||||
category,
|
||||
});
|
||||
|
||||
await newTransaction.save();
|
||||
|
||||
res.status(200).json({
|
||||
message: '✅ Voice transaction saved',
|
||||
data: newTransaction,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('❌ Voice transaction error:', err.message);
|
||||
res.status(500).json({ error: 'Server error during voice transaction' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//server
|
||||
const HOST = process.env.HOST || '0.0.0.0'; // listen on all interfaces
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`🚀 Server running on http://${HOST}:${PORT}`);
|
||||
});
|
||||
|
||||
163
Backend/node_modules/.package-lock.json
generated
vendored
163
Backend/node_modules/.package-lock.json
generated
vendored
|
|
@ -87,6 +87,15 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
|
|
@ -435,6 +444,46 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.7",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.1.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express-session/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
|
|
@ -1206,6 +1255,12 @@
|
|||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/oauth": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
|
||||
"integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -1239,6 +1294,15 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
|
@ -1257,6 +1321,64 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/passport": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-google-oauth20": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
|
||||
"integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"passport-oauth2": "1.x.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-oauth2": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
|
||||
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64url": "3.x.x",
|
||||
"oauth": "0.10.x",
|
||||
"passport-strategy": "1.x.x",
|
||||
"uid2": "0.0.x",
|
||||
"utils-merge": "1.x.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-strategy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
||||
|
|
@ -1266,6 +1388,11 @@
|
|||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/pause": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
|
@ -1309,6 +1436,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
|
|
@ -1549,6 +1685,24 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/uid2": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
|
||||
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
|
@ -1564,6 +1718,15 @@
|
|||
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
|
|
|||
168
Backend/package-lock.json
generated
168
Backend/package-lock.json
generated
|
|
@ -14,10 +14,13 @@
|
|||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^5.1.0",
|
||||
"express-session": "^1.18.2",
|
||||
"google-auth-library": "^10.2.1",
|
||||
"googleapis": "^156.0.0",
|
||||
"mongoose": "^8.15.0",
|
||||
"node-cron": "^4.2.1"
|
||||
"node-cron": "^4.2.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
|
|
@ -103,6 +106,15 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
|
|
@ -451,6 +463,46 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.7",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.1.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express-session/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
|
|
@ -1222,6 +1274,12 @@
|
|||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/oauth": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
|
||||
"integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -1255,6 +1313,15 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
|
@ -1273,6 +1340,64 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/passport": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-google-oauth20": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
|
||||
"integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"passport-oauth2": "1.x.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-oauth2": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
|
||||
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64url": "3.x.x",
|
||||
"oauth": "0.10.x",
|
||||
"passport-strategy": "1.x.x",
|
||||
"uid2": "0.0.x",
|
||||
"utils-merge": "1.x.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-strategy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
||||
|
|
@ -1282,6 +1407,11 @@
|
|||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/pause": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
|
@ -1325,6 +1455,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
|
|
@ -1565,6 +1704,24 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/uid2": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
|
||||
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
|
@ -1580,6 +1737,15 @@
|
|||
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
|
|
|||
|
|
@ -15,9 +15,12 @@
|
|||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^5.1.0",
|
||||
"express-session": "^1.18.2",
|
||||
"google-auth-library": "^10.2.1",
|
||||
"googleapis": "^156.0.0",
|
||||
"mongoose": "^8.15.0",
|
||||
"node-cron": "^4.2.1"
|
||||
"node-cron": "^4.2.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
Frontend/.gitignore
vendored
21
Frontend/.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
|||
# Miscellaneous
|
||||
# Miscellaneous files
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
|
|
@ -12,18 +12,16 @@
|
|||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
# IntelliJ / Android Studio
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
# VSCode - usually you want to keep .vscode in git, so commented by default
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Flutter/Dart/Pub
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
|
|
@ -31,15 +29,16 @@ migrate_working_dir/
|
|||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
build/
|
||||
|
||||
# Symbolication related
|
||||
# Symbolication and Obfuscation files - ignore generated maps and symbols
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
# Android Studio build folders
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Your Dart environment file (if this contains secrets or environment config)
|
||||
lib/other_pages/enviroment.dart
|
||||
|
|
|
|||
2
Frontend/android/.gitignore
vendored
2
Frontend/android/.gitignore
vendored
|
|
@ -6,7 +6,7 @@ gradle-wrapper.jar
|
|||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
*.env
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
|
|
|
|||
111
Frontend/lib/login.dart
Normal file
111
Frontend/lib/login.dart
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:monarch/other_pages/enviroment.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:monarch/main_pages/HomePage/homepage.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
// GoogleSignIn instance
|
||||
final GoogleSignIn _googleSignIn = GoogleSignIn(
|
||||
scopes: ['email', 'profile', 'openid'],
|
||||
serverClientId: '${Environment.serverClientId}',
|
||||
);
|
||||
|
||||
bool _isLoading = false;
|
||||
|
||||
Future<void> _handleGoogleSignIn() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
// Step 1: Trigger the sign-in flow
|
||||
final GoogleSignInAccount? account = await _googleSignIn.signIn();
|
||||
|
||||
if (account == null) {
|
||||
// User cancelled
|
||||
setState(() => _isLoading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Get authentication tokens
|
||||
final GoogleSignInAuthentication auth = await account.authentication;
|
||||
final String? idToken = auth.idToken;
|
||||
|
||||
if (idToken == null) {
|
||||
debugPrint("❌ Failed to get ID token");
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Send token to backend
|
||||
final uri = Uri.parse("http://192.168.1.10:3000/auth/google/token");
|
||||
final res = await http.post(
|
||||
uri,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({"idToken": idToken}),
|
||||
);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
final data = jsonDecode(res.body);
|
||||
if (data['success'] == true) {
|
||||
debugPrint("✅ User logged in: ${data['user']}");
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString('userId', data['user']['_id']);
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const FinTrackHomePage()),
|
||||
);
|
||||
} else {
|
||||
debugPrint("❌ Login failed: ${data['error']}");
|
||||
}
|
||||
} else {
|
||||
debugPrint("❌ Backend error: ${res.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("⚠ Google Sign-In error: $e");
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child:
|
||||
_isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 20,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
side: const BorderSide(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
|
||||
label: const Text(
|
||||
'Sign in with Google',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
onPressed: _handleGoogleSignIn,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import 'dart:convert';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:monarch/login.dart';
|
||||
import 'package:monarch/speech.dart';
|
||||
import 'package:monarch/support/add.dart';
|
||||
import 'package:monarch/main_pages/Statistics/update_budget.dart';
|
||||
|
|
@ -24,7 +25,7 @@ class MyApp extends StatelessWidget {
|
|||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(primarySwatch: Colors.blue),
|
||||
home: const FinTrackHomePage(),
|
||||
home: const LoginScreen(),
|
||||
|
||||
// home: const SpeechInputPage(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// ignore_for_file: sort_child_properties_last, use_build_context_synchronously, deprecated_member_use, unnecessary_import, unused_local_variable, unused_import, sized_box_for_whitespace
|
||||
// ignore_for_file: sort_child_properties_last, use_build_context_synchronously, deprecated_member_use, unnecessary_import, unused_local_variable, unused_import, sized_box_for_whitespace, avoid_print
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
|
@ -13,6 +13,7 @@ import 'package:monarch/main_pages/Statistics/update_budget.dart';
|
|||
import 'package:monarch/main_pages/Statistics/budget_manager.dart';
|
||||
import 'package:monarch/main_pages/HomePage/homepage.dart';
|
||||
import 'package:monarch/main_pages/HomePage/navbar.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Transaction {
|
||||
final String description;
|
||||
|
|
@ -109,20 +110,42 @@ class StatisticsState extends State<Statistics> with TickerProviderStateMixin {
|
|||
Future<void> fetchTransactions({String category = 'All'}) async {
|
||||
setState(() => isLoading = true);
|
||||
try {
|
||||
// 1. Get the saved userId from SharedPreferences or secure storage
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId = prefs.getString('userId');
|
||||
|
||||
if (userId == null) {
|
||||
throw Exception('User ID not found. Please log in again.');
|
||||
}
|
||||
|
||||
// 2. Build query parameters
|
||||
final queryParams = {
|
||||
'userId': userId,
|
||||
if (category != 'All') 'category': category,
|
||||
};
|
||||
|
||||
// 3. Build URI
|
||||
final uri = Uri.parse(
|
||||
'${Environment.baseUrl}/api/transactions',
|
||||
).replace(
|
||||
queryParameters: category == 'All' ? null : {'category': category},
|
||||
);
|
||||
).replace(queryParameters: queryParams);
|
||||
|
||||
// 4. Send GET request
|
||||
final response = await http.get(uri);
|
||||
print('Request URL: $uri');
|
||||
print('Status: ${response.statusCode}');
|
||||
print('Body: ${response.body}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final List<dynamic> list = data['data'];
|
||||
final List<dynamic> list = data['data'] ?? [];
|
||||
final fetched = list.map((json) => Transaction.fromJson(json)).toList();
|
||||
|
||||
// Calculate totals
|
||||
final totals = <String, double>{};
|
||||
for (var tx in fetched) {
|
||||
totals[tx.category] = (totals[tx.category] ?? 0) + tx.amount;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
transactions = fetched;
|
||||
totalAmountPerCategory = totals;
|
||||
|
|
@ -132,7 +155,7 @@ class StatisticsState extends State<Statistics> with TickerProviderStateMixin {
|
|||
}
|
||||
} catch (e) {
|
||||
_showCustomSnackBar(
|
||||
message: 'Error fetching transactions',
|
||||
message: 'Error fetching transactions: ${e.toString()}',
|
||||
icon: Icons.data_exploration_rounded,
|
||||
backgroundColor: const Color(0xFFFF9F43),
|
||||
iconColor: Colors.white,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
class Environment {
|
||||
static const String baseUrl = 'http://192.168.1.10:3000';
|
||||
static const String serverClientId =
|
||||
"278433619849-cg6f7gk5cc45rgu1ojrlkf794lm4udgn.apps.googleusercontent.com";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:monarch/other_pages/colors.dart';
|
||||
import 'package:monarch/other_pages/enviroment.dart';
|
||||
import 'package:monarch/main_pages/Statistics/statistics.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AddExpenseScreen extends StatefulWidget {
|
||||
const AddExpenseScreen({super.key});
|
||||
|
|
@ -19,8 +20,6 @@ class AddExpenseScreen extends StatefulWidget {
|
|||
|
||||
class _AddExpenseScreenState extends State<AddExpenseScreen>
|
||||
with TickerProviderStateMixin {
|
||||
|
||||
|
||||
final TextEditingController descriptionController = TextEditingController();
|
||||
final TextEditingController amountController = TextEditingController();
|
||||
final FocusNode _nameFocus = FocusNode();
|
||||
|
|
@ -173,7 +172,7 @@ class _AddExpenseScreenState extends State<AddExpenseScreen>
|
|||
}
|
||||
|
||||
Future<void> addTransaction() async {
|
||||
// Validation first
|
||||
// Validation
|
||||
if (descriptionController.text.trim().isEmpty ||
|
||||
amountController.text.trim().isEmpty) {
|
||||
_showCustomSnackBar(
|
||||
|
|
@ -185,7 +184,7 @@ class _AddExpenseScreenState extends State<AddExpenseScreen>
|
|||
return;
|
||||
}
|
||||
|
||||
// Show beautiful loading indicator
|
||||
// Show loading dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
|
|
@ -227,12 +226,29 @@ class _AddExpenseScreenState extends State<AddExpenseScreen>
|
|||
);
|
||||
|
||||
try {
|
||||
// 🔹 Get stored userId
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId = prefs.getString('userId');
|
||||
|
||||
if (userId == null) {
|
||||
Navigator.of(context).pop();
|
||||
_showCustomSnackBar(
|
||||
message: 'User not logged in — please log in again',
|
||||
icon: Icons.error_rounded,
|
||||
backgroundColor: const Color(0xFFFF6B6B),
|
||||
iconColor: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send POST request including userId
|
||||
final response = await http.post(
|
||||
Uri.parse('${Environment.baseUrl}/api/transaction/add'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'description': descriptionController.text,
|
||||
'amount': double.tryParse(amountController.text),
|
||||
'userId': userId, // ✅ add user ID here
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -242,14 +258,10 @@ class _AddExpenseScreenState extends State<AddExpenseScreen>
|
|||
final responseData = json.decode(response.body);
|
||||
final predictedCategory = responseData['data']['category'];
|
||||
|
||||
// Clear the input fields
|
||||
descriptionController.clear();
|
||||
amountController.clear();
|
||||
setState(() {
|
||||
_displayAmount = '';
|
||||
});
|
||||
setState(() => _displayAmount = '');
|
||||
|
||||
// Show success message
|
||||
_showCustomSnackBar(
|
||||
message:
|
||||
'Transaction added successfully!\nCategorized as: $predictedCategory',
|
||||
|
|
@ -259,11 +271,9 @@ class _AddExpenseScreenState extends State<AddExpenseScreen>
|
|||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
// Navigate back to statistics page after a short delay
|
||||
// Optionally refresh statistics screen
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(); // Go back to previous screen
|
||||
}
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
});
|
||||
} else {
|
||||
_showCustomSnackBar(
|
||||
|
|
@ -274,7 +284,7 @@ class _AddExpenseScreenState extends State<AddExpenseScreen>
|
|||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Navigator.of(context).pop(); // Close loading dialog
|
||||
Navigator.of(context).pop();
|
||||
_showCustomSnackBar(
|
||||
message: 'Network error occurred\nPlease check your connection',
|
||||
icon: Icons.wifi_off_rounded,
|
||||
|
|
|
|||
|
|
@ -2,18 +2,40 @@
|
|||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../other_pages/enviroment.dart';
|
||||
|
||||
Future<List<Map<String, dynamic>>> fetchRecentTransactions() async {
|
||||
final response = await http.get(
|
||||
try {
|
||||
// 1️⃣ Get userId from local storage
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId = prefs.getString('userId');
|
||||
|
||||
Uri.parse('${Environment.baseUrl}/api/transactions/recent'),
|
||||
);
|
||||
if (userId == null || userId.isEmpty) {
|
||||
print('❌ User ID not found — please log in again.');
|
||||
return [];
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body)['data'];
|
||||
return List<Map<String, dynamic>>.from(data);
|
||||
} else {
|
||||
print('Failed to load recent transactions');
|
||||
// 2️⃣ Create URI with userId
|
||||
final uri = Uri.parse(
|
||||
'${Environment.baseUrl}/api/transactions/recent',
|
||||
).replace(queryParameters: {'userId': userId});
|
||||
|
||||
// 3️⃣ Make API request
|
||||
final response = await http.get(uri);
|
||||
|
||||
print('📡 Fetching recent transactions from: $uri');
|
||||
print('Status code: ${response.statusCode}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body)['data'] ?? [];
|
||||
return List<Map<String, dynamic>>.from(data);
|
||||
} else {
|
||||
print('❌ Failed to load recent transactions: ${response.body}');
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
print('⚠ Error fetching recent transactions: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@
|
|||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import google_sign_in_ios
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
import speech_to_text
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin"))
|
||||
|
|
|
|||
|
|
@ -144,6 +144,54 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
google_identity_services_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_identity_services_web
|
||||
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+1"
|
||||
google_sign_in:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_sign_in
|
||||
sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
google_sign_in_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_android
|
||||
sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.1"
|
||||
google_sign_in_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_ios
|
||||
sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
google_sign_in_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_platform_interface
|
||||
sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
google_sign_in_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_web
|
||||
sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.4+4"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ dependencies:
|
|||
manual_speech_to_text: ^1.0.4
|
||||
permission_handler: ^11.4.0
|
||||
another_telephony: ^0.4.1
|
||||
google_sign_in: ^6.2.1
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue