Compare commits

...

192 Commits

Author SHA1 Message Date
Hargata Softworks
be81f9727a Merge pull request #146 from hargata/Hargata/auto.reminder.setting
Hargata/auto.reminder.setting
2024-01-23 11:48:44 -07:00
DESKTOP-T0O5CDB\DESK-555BD
04b7e7fd38 added setting to auto-insert odometer readings. 2024-01-23 11:46:11 -07:00
DESKTOP-GENO133\IvanPlex
e6b50fafd2 fixed column width when printing vehicle service report. 2024-01-23 09:06:10 -07:00
DESKTOP-GENO133\IvanPlex
d4896a7607 added setting to disable auto reminder refresh for recurring past due reminders. 2024-01-23 08:57:11 -07:00
Hargata Softworks
0b203709fa Merge pull request #144 from hargata/Hargata/mileageinterval
added 15k mile interval.
2024-01-22 14:56:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
df5faba146 added 15k mile interval. 2024-01-22 14:55:56 -07:00
Hargata Softworks
d8f8b63488 Merge pull request #143 from hargata/Hargata/supply.store
Add Functionality to Requisition Supplies when adding record.
2024-01-22 13:52:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c100fc76ed added requisition ability for plans. 2024-01-22 13:52:27 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c553d87600 display error message when no supplies are found. 2024-01-22 12:23:04 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b542bd54fb adding requisiton functionality to upgrades and repairs. 2024-01-22 12:04:33 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4ec11a47a1 added method to requisiton supplies. 2024-01-22 11:47:53 -07:00
DESKTOP-T0O5CDB\DESK-555BD
175ce2be48 added more helper methods. 2024-01-22 11:09:01 -07:00
DESKTOP-T0O5CDB\DESK-555BD
aad1655f2e added global parsefloat method which derives from C# culture. 2024-01-22 10:52:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
85eb0b70e6 Merge branch 'main' into Hargata/supply.store 2024-01-22 08:14:59 -07:00
Hargata Softworks
f54e12886a Merge pull request #142 from hargata/Hargata/fix.chart
fix misleading bar charts.
2024-01-22 08:12:44 -07:00
DESKTOP-T0O5CDB\DESK-555BD
80ebe4c292 fix misleading bar charts. 2024-01-22 08:11:29 -07:00
DESKTOP-GENO133\IvanPlex
92b3bc3aea rough draft of supply store. 2024-01-21 21:13:03 -07:00
Hargata Softworks
2cfb82c235 Merge pull request #138 from hargata/Hargata/gas.api
added Gas Post API.
2024-01-21 17:57:57 -07:00
DESKTOP-GENO133\IvanPlex
dd693323d7 added Gas Post API. 2024-01-21 17:56:03 -07:00
Hargata Softworks
5c8f03003e Merge pull request #134 from hargata/Hargata/post.api
POST API Endpoints
2024-01-21 09:03:26 -07:00
DESKTOP-GENO133\IvanPlex
023fac2ea9 added additional validations to post api endpoints and their documentation. 2024-01-21 08:36:10 -07:00
DESKTOP-GENO133\IvanPlex
9086c26b5e Merge branch 'main' into Hargata/post.api 2024-01-21 08:18:13 -07:00
DESKTOP-GENO133\IvanPlex
0af8e99e61 updated version lmao my bad 2024-01-21 08:15:33 -07:00
DESKTOP-GENO133\IvanPlex
4ca45dd32b added post endpoints for service records, upgrade records and repair records. 2024-01-21 08:14:45 -07:00
DESKTOP-GENO133\IvanPlex
127753ee86 fixed 403 for API Controller. 2024-01-20 18:36:35 -07:00
DESKTOP-GENO133\IvanPlex
30a9411cdd added post methods for tax records. 2024-01-20 17:47:48 -07:00
Hargata Softworks
e801a4a77c Merge pull request #133 from hargata/Hargata/minor.fixes
Hargata/minor.fixes
2024-01-20 14:41:02 -07:00
DESKTOP-GENO133\IvanPlex
d8c49995ce more math fixes. 2024-01-20 14:38:57 -07:00
DESKTOP-GENO133\IvanPlex
0c93663e51 Fixed a bunch of minor stuff, styling and average mpg calculation. 2024-01-20 14:30:07 -07:00
Hargata Softworks
605ac07594 Update screenshots.md 2024-01-20 13:56:14 -07:00
Hargata Softworks
9a7f2233a0 Merge pull request #130 from hargata/Hargata/to.do
added button to manually push back a recurring reminder record.
2024-01-20 12:11:41 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1339c427c4 added button to manually push back a recurring reminder record. 2024-01-20 12:10:54 -07:00
Hargata Softworks
8d79804872 Merge pull request #129 from hargata/Hargata/to.do
escape datecreated string
2024-01-20 10:34:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3598bb6adb added method to decode html entities 2024-01-20 10:33:27 -07:00
DESKTOP-T0O5CDB\DESK-555BD
50b18a1a71 escape datecreated string 2024-01-20 10:21:13 -07:00
Hargata Softworks
0ab585d4cc Merge pull request #125 from hargata/Hargata/to.do
V1.0.7 - Planner(Kanban), Recurring Reminders, Odometer Tab
2024-01-20 09:54:10 -07:00
Hargata Softworks
d32b9b879d Merge pull request #127 from hargata/Hargata/odometer.tab
Hargata/odometer.tab
2024-01-20 09:53:28 -07:00
DESKTOP-T0O5CDB\DESK-555BD
594ad38454 added API endpoints 2024-01-20 09:52:16 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ae8a885b4d added odometer tab. 2024-01-20 09:34:31 -07:00
Hargata Softworks
975bbadaae Merge pull request #126 from hargata/Hargata/recurring.reminder
Hargata/recurring.reminder
2024-01-20 09:02:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
42711bc92a added front end for recurring reminder. 2024-01-20 08:43:59 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f4538ffabd method to calculate for recurring reminders. 2024-01-20 08:21:06 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d60c3d3ea6 show cancel button. 2024-01-20 07:24:24 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3cc32e48f4 asks for odometer reading when task is marked as done. 2024-01-20 07:22:46 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0d4b7d8ee1 clean up and auto convert records. 2024-01-19 21:03:08 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d8249c7163 Added CSV Import and Export 2024-01-19 20:37:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
60edb65b55 create front end for Planner 2024-01-19 19:34:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
bc2bb3636b added backend for planner. 2024-01-19 14:54:15 -07:00
DESKTOP-T0O5CDB\DESK-555BD
58c49c1240 Merge branch 'main' into Hargata/to.do 2024-01-19 10:48:30 -07:00
DESKTOP-GENO133\IvanPlex
2eb9a58aa6 added enums. 2024-01-19 10:27:17 -07:00
Hargata Softworks
b05ded67a9 Merge pull request #123 from hargata/Hargata/gas.enhancement
Gas Enhancements
2024-01-19 09:56:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
76c9473704 added ability to add gas records by Unit Costs and added notes field to gas records. 2024-01-19 09:52:54 -07:00
Hargata Softworks
3909223d2f Merge pull request #122 from hargata/Hargata/fix.div.byzero
fix divide by zero.
2024-01-19 00:01:58 -07:00
DESKTOP-GENO133\IvanPlex
ab720272da fix divide by zero. 2024-01-19 00:00:13 -07:00
Hargata Softworks
b702a663f4 Merge pull request #120 from hargata/Hargata/mobile.columns
fixed descending option.
2024-01-18 16:51:24 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1d8baaf423 fixed descending option. 2024-01-18 16:50:53 -07:00
Hargata Softworks
0869c99090 Merge pull request #118 from hargata/Hargata/mobile.columns
Hargata/mobile.columns
2024-01-18 11:16:22 -07:00
DESKTOP-T0O5CDB\DESK-555BD
48b2bab5d6 added github action to push to docker hub 2024-01-18 11:16:06 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0bfa036603 added API controller endpoint documentation for makebackup 2024-01-18 11:12:31 -07:00
DESKTOP-GENO133\IvanPlex
a02bcf94d7 fix overflowing file names in file uploads. 2024-01-18 09:00:14 -07:00
DESKTOP-GENO133\IvanPlex
9e0b45deac fixed view for date on all screens. 2024-01-18 08:12:38 -07:00
Hargata Softworks
1b04faad9c Merge pull request #116 from hargata/Hargata/fix.mobile.nav
add api endpoint to create backup.
2024-01-17 22:19:03 -07:00
Hargata Softworks
17f5cf6a1d Update README.md 2024-01-17 22:18:18 -07:00
Hargata Softworks
bf7cffdf8f Update screenshots.md 2024-01-17 21:21:32 -07:00
Hargata Softworks
a3ba527765 Update screenshots.md 2024-01-17 21:18:47 -07:00
Hargata Softworks
a085b2d87d Create screenshots.md 2024-01-17 21:15:25 -07:00
DESKTOP-T0O5CDB\DESK-555BD
06f1ce5884 add api endpoint to create backup. 2024-01-17 15:53:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c77f9bce51 no delete option. 2024-01-17 12:33:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cf11c203ce added logging. 2024-01-17 12:25:18 -07:00
Hargata Softworks
4550d33b92 Merge pull request #114 from hargata/Hargata/version
added patreon link and version number.
2024-01-17 12:11:24 -07:00
DESKTOP-T0O5CDB\DESK-555BD
503127eb03 added patreon link and version number. 2024-01-17 12:10:54 -07:00
Hargata Softworks
e1a5a871ae Merge pull request #113 from hargata/Hargata/supplies
Supplies Tab
2024-01-17 11:04:25 -07:00
DESKTOP-GENO133\IvanPlex
0714ec6432 Create and Restore Backups. 2024-01-17 11:02:40 -07:00
DESKTOP-GENO133\IvanPlex
36339a04e1 removed unused using. 2024-01-16 21:11:30 -07:00
DESKTOP-GENO133\IvanPlex
8af9868d2f Added front end for supplies. 2024-01-16 20:38:54 -07:00
DESKTOP-GENO133\IvanPlex
99d5372f25 Added supply record backend. 2024-01-16 09:02:33 -07:00
Hargata Softworks
bd6defe205 Merge pull request #107 from hargata/Hargata/fuel.decimal.settings
named setting better and only call userconfig method once in the gas …
2024-01-16 08:46:15 -07:00
DESKTOP-GENO133\IvanPlex
f5276031c0 fixed z-index for mobile view. 2024-01-16 08:45:53 -07:00
DESKTOP-GENO133\IvanPlex
50ebdf547a default appsettings. 2024-01-16 08:44:31 -07:00
DESKTOP-GENO133\IvanPlex
ba248758cb added empty tab for supplies 2024-01-16 08:44:09 -07:00
DESKTOP-GENO133\IvanPlex
4bde8181b7 enabled functionality to select default tab. 2024-01-15 20:01:34 -07:00
DESKTOP-GENO133\IvanPlex
d0d733b7d2 moved settings around. 2024-01-15 18:47:04 -07:00
DESKTOP-GENO133\IvanPlex
e377f6c8d0 added config to hide tabs that are not visible. 2024-01-15 17:38:25 -07:00
DESKTOP-GENO133\IvanPlex
2fafe1101c named setting better and only call userconfig method once in the gas tab so it doesn't make as much calls to the cache. 2024-01-15 13:26:57 -07:00
Hargata Softworks
56a315524a Merge pull request #106 from hargata/Hargata/fuel.decimal.settings
add settings to toggle fuel decimals from three to two.
2024-01-15 13:08:40 -07:00
DESKTOP-GENO133\IvanPlex
244a164891 updated default app settings. 2024-01-15 13:05:09 -07:00
DESKTOP-GENO133\IvanPlex
2ec0c1d465 add settings to toggle fuel decimals from three to two. 2024-01-15 13:02:48 -07:00
Hargata Softworks
239ca73a64 Merge pull request #102 from ThisIsQasim/patch-1
Add ARM64 images
2024-01-15 10:13:41 -07:00
Hargata Softworks
b03460d6bb Merge pull request #105 from hargata/Hargata/qol
Quality of Life Improvements.
2024-01-15 10:10:01 -07:00
DESKTOP-GENO133\IvanPlex
d2686949c5 added additional gas columns to API controller. 2024-01-15 10:05:04 -07:00
DESKTOP-GENO133\IvanPlex
971242b015 updated gas sample. 2024-01-15 10:01:46 -07:00
DESKTOP-GENO133\IvanPlex
bd3b821226 added missedfuelup and partialfuelup to csv export and import. 2024-01-15 09:54:05 -07:00
DESKTOP-GENO133\IvanPlex
6ffa856795 defaults input to today's date but only if there aren't any dates provided. 2024-01-15 09:32:15 -07:00
DESKTOP-GENO133\IvanPlex
6895d2060d FIxed bug caused by doubling of upgrade sums. made table headers sticky so that they stay in position when scrolled past. 2024-01-15 09:21:15 -07:00
Qasim Mehmood
d413a06b87 remove test branch 2024-01-15 16:30:43 +05:00
Qasim Mehmood
263000f0ae renable image push 2024-01-15 16:29:13 +05:00
Qasim Mehmood
578f6ab62a Update Dockerfile 2024-01-15 16:27:32 +05:00
Qasim Mehmood
469b625989 disable push 2024-01-15 16:23:26 +05:00
Qasim Mehmood
95a630b600 cross-compile inside the container 2024-01-15 16:22:22 +05:00
Qasim Mehmood
5f576a2792 Update docker-image.yml 2024-01-15 16:17:45 +05:00
Qasim Mehmood
f5af92da93 Add ARM64 images 2024-01-15 16:06:00 +05:00
Hargata Softworks
4ccfcd6be7 Merge pull request #99 from hargata/Hargata/scrollposition
quick fix
2024-01-15 00:05:10 -07:00
DESKTOP-T0O5CDB\DESK-555BD
917d093bcf quick fix 2024-01-15 00:04:41 -07:00
Hargata Softworks
c916771815 Update README.md 2024-01-14 21:10:38 -07:00
Hargata Softworks
d4517ac986 Merge pull request #93 from hargata/Hargata/scrollposition
Hargata/scrollposition
2024-01-14 18:09:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
040cc6adf9 select today's date as default for quality of life. 2024-01-14 18:08:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ec740089fe preserve the scroll position. 2024-01-14 17:51:36 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e29e14bc3e default values in environment variables so people stop getting Http 500. 2024-01-14 17:21:39 -07:00
Hargata Softworks
592baa4c6e Update LICENSE 2024-01-14 14:49:55 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5e5c9c96b4 Updated readme, env file and license. 2024-01-14 13:37:10 -07:00
Hargata Softworks
487f64e459 Merge pull request #84 from hargata/Hargata/users
Users Management
2024-01-14 13:14:33 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d67aeaa333 removed fueleconomy from fuelly mapper. 2024-01-14 13:11:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
fb5cdfce29 Merge branch 'main' into Hargata/users
# Conflicts:
#	Views/Vehicle/Index.cshtml
2024-01-14 13:08:37 -07:00
DESKTOP-GENO133\IvanPlex
6e5cfde2cf only allow notification if smtp server is setup. 2024-01-14 12:13:37 -07:00
DESKTOP-GENO133\IvanPlex
7122f4ac0d renamed report and make it default tab. 2024-01-14 11:51:57 -07:00
DESKTOP-GENO133\IvanPlex
2fb24b8b65 Merge branch 'Hargata/user.config' into Hargata/users
# Conflicts:
#	Helper/ConfigHelper.cs
#	Views/Home/Index.cshtml
2024-01-14 11:20:02 -07:00
DESKTOP-GENO133\IvanPlex
6540d96d4d fixed admin view even more. 2024-01-14 11:17:51 -07:00
DESKTOP-GENO133\IvanPlex
62e196b97d fix admin view. 2024-01-14 11:12:09 -07:00
DESKTOP-GENO133\IvanPlex
4975861710 moved things around added delay admin page styling. 2024-01-14 10:52:42 -07:00
DESKTOP-GENO133\IvanPlex
8d74799099 replaced IConfiguration injection with IConfigHelper 2024-01-14 09:54:13 -07:00
DESKTOP-GENO133\IvanPlex
c58b4552b2 hide settings page from user. 2024-01-13 23:43:38 -07:00
DESKTOP-GENO133\IvanPlex
8c6920afab enable users to have their own config file. 2024-01-13 23:13:11 -07:00
Hargata Softworks
b77fd2c1c7 Merge pull request #83 from hargata/Hargata/missing.mobile.buttons
Add Missing Buttons in Mobile View.
2024-01-13 22:31:39 -07:00
Hargata Softworks
2035d6f6e2 Merge pull request #86 from hargata/Hargata/fuel.import
fix mapper.
2024-01-13 22:31:14 -07:00
DESKTOP-GENO133\IvanPlex
e58454ef5d fix mapper. 2024-01-13 22:30:11 -07:00
DESKTOP-GENO133\IvanPlex
915eb1722d added confighelper 2024-01-13 22:29:14 -07:00
DESKTOP-GENO133\IvanPlex
4f706d3e93 filtered out vehicles not owned by the user when accessing via API. 2024-01-13 21:35:23 -07:00
DESKTOP-GENO133\IvanPlex
d80f0dcb8f cleaned up unused methods. 2024-01-13 21:33:13 -07:00
DESKTOP-GENO133\IvanPlex
2ae334d06d added functions to add and remove collaborators. 2024-01-13 21:18:58 -07:00
DESKTOP-GENO133\IvanPlex
4388df71f3 added action filter attribute 2024-01-13 20:13:12 -07:00
DESKTOP-GENO133\IvanPlex
c972f9c8a2 added collaborator view. 2024-01-13 18:19:52 -07:00
DESKTOP-GENO133\IvanPlex
90fa6ad5fc fixed method to delete user access. 2024-01-13 17:55:02 -07:00
DESKTOP-GENO133\IvanPlex
a1b2b40abe reshaped user access object, added 401 page. 2024-01-13 17:49:48 -07:00
DESKTOP-GENO133\IvanPlex
00fd499805 added user logic. 2024-01-13 16:34:39 -07:00
DESKTOP-GENO133\IvanPlex
8f3f71772b added data access methods for user access. 2024-01-13 15:44:29 -07:00
DESKTOP-GENO133\IvanPlex
08104eef2a moved field around. 2024-01-13 12:52:26 -07:00
DESKTOP-GENO133\IvanPlex
8d989ee81c added forgot password feature. 2024-01-13 12:50:55 -07:00
DESKTOP-GENO133\IvanPlex
2247b1b1db added ability to notify user that they have a registration token. 2024-01-13 11:48:20 -07:00
DESKTOP-GENO133\IvanPlex
249ad938f6 added missing edit vehicle button and close button for modal in mobile view. 2024-01-13 11:16:14 -07:00
DESKTOP-GENO133\IvanPlex
c9d60910e5 added scaffolding for email sending methods 2024-01-13 11:12:37 -07:00
DESKTOP-GENO133\IvanPlex
03b89786ec make email address required for token generation and user registration. 2024-01-12 23:54:12 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8815009b04 added token based registration. 2024-01-12 18:37:51 -07:00
Hargata Softworks
bb4a8f7f83 Merge pull request #79 from hargata/Hargata/vehicle.history
order by chronological and then by odometer
2024-01-12 15:26:38 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c7730d1775 fixed print stylings on iphone. 2024-01-12 15:26:27 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b7a3ef0fa7 order by chronological and then by odometer 2024-01-12 15:17:46 -07:00
Hargata Softworks
d525e2195c Merge pull request #75 from hargata/Hargata/vehicle.history
added front end stuff for consolidated vehicle maintenance record.
2024-01-12 14:55:50 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cb73be0e43 handle 0 MPG and styling bug fixes. 2024-01-12 14:54:42 -07:00
Hargata Softworks
a06bdbff88 Merge pull request #73 from hargata/Hargata/nonroot.user
added volume for temp folder so that the container can run as non-roo…
2024-01-12 14:27:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0de6ab7547 fix printing stuff. 2024-01-12 14:26:47 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5b54b8113e added more stylings. 2024-01-12 14:19:41 -07:00
DESKTOP-GENO133\IvanPlex
4d804803a8 added front end stuff for consolidated vehicle maintenance record. 2024-01-12 12:52:01 -07:00
DESKTOP-GENO133\IvanPlex
2434245c84 added volume for temp folder so that the container can run as non-root user. 2024-01-12 07:17:40 -07:00
Hargata Softworks
d00e6e252d Merge pull request #68 from hargata/Hargata/better.mobile
make it more mobile friendly.
2024-01-11 23:10:22 -07:00
DESKTOP-GENO133\IvanPlex
e006c158fc full screen navbar for small devices. 2024-01-11 23:04:33 -07:00
DESKTOP-T0O5CDB\DESK-555BD
75d610200b make it more mobile friendly. 2024-01-11 20:52:55 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f696030ac2 fixed month order in reports page. 2024-01-11 18:58:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
20190642a8 fixed login viewport height. 2024-01-11 14:34:43 -07:00
Hargata Softworks
5d2068bd78 Merge pull request #64 from hargata/Hargata/pwa
Add PWA Manifests.
2024-01-11 14:20:49 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3a01944e30 resized screenshot 2024-01-11 14:14:05 -07:00
DESKTOP-T0O5CDB\DESK-555BD
014b6bce8d added pwa for android and iphone. 2024-01-11 13:53:02 -07:00
Hargata Softworks
8f69399f1b Merge pull request #63 from hargata/Hargata/reports.improvement
Hargata/reports.improvement
2024-01-11 11:38:19 -07:00
DESKTOP-T0O5CDB\DESK-555BD
200aa79dac Merge branch 'Hargata/consolidated.report' into Hargata/reports.improvement 2024-01-11 11:37:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
dfb6f69d69 removed "All" checkbox on reports page. 2024-01-11 11:26:47 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cd2fb2dd21 quick fix. 2024-01-11 09:27:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a396eb4f72 Updated colors again. 2024-01-11 09:22:57 -07:00
Hargata Softworks
46675c944f Merge pull request #57 from hargata/Hargata/consolidated.report
Updated Chart Colors.
2024-01-10 20:17:00 -07:00
Hargata Softworks
ff7ad04f0b Merge pull request #56 from hargata/Hargata/missed.fuelupimport
add missed_fuelup column to csv imports.
2024-01-10 20:15:43 -07:00
DESKTOP-GENO133\IvanPlex
d69ede1447 add missed_fuelup column to csv imports. 2024-01-10 20:14:59 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b477cf9d51 Updated Chart Colors. 2024-01-10 14:54:46 -07:00
Hargata Softworks
ad8c27a2e6 Merge pull request #55 from hargata/Hargata/consolidated.report
fixed for real.
2024-01-10 14:43:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
52f9ae7ea1 fixed for real. 2024-01-10 14:42:58 -07:00
Hargata Softworks
ed1066662e Merge pull request #54 from hargata/Hargata/consolidated.report
Hargata/consolidated.report
2024-01-10 14:30:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e3bf6f03d7 Make charts responsive to params. 2024-01-10 14:04:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8cc2ceecc6 Merge branch 'main' into Hargata/consolidated.report 2024-01-10 09:23:21 -07:00
Hargata Softworks
9837172774 Merge pull request #52 from hargata/Hargata/missed.fuelup
added missed fuel up feature.
2024-01-10 05:37:44 -07:00
DESKTOP-GENO133\IvanPlex
fba2a6dc68 added missed fuel up feature. 2024-01-10 05:36:42 -07:00
Hargata Softworks
a467ba08c6 Merge pull request #51 from hargata/Hargata.international.numbers
added support for international number formats.
2024-01-10 05:11:48 -07:00
DESKTOP-GENO133\IvanPlex
7bfbb49efb added support for international number formats. 2024-01-10 05:10:30 -07:00
Hargata Softworks
7e4f5807d2 Merge pull request #49 from hargata/Hargata/named.notes
added truncating function.
2024-01-09 22:16:35 -07:00
DESKTOP-GENO133\IvanPlex
d5aee08f69 added truncating function. 2024-01-09 22:15:53 -07:00
Hargata Softworks
080d50c6bc Merge pull request #48 from hargata/Hargata/named.notes
Hargata/named.notes
2024-01-09 21:49:04 -07:00
DESKTOP-GENO133\IvanPlex
3105622d63 added csv exports for gas and tax tab. 2024-01-09 21:41:58 -07:00
DESKTOP-GENO133\IvanPlex
eac181ff20 removed obsolete function. 2024-01-09 21:13:33 -07:00
DESKTOP-GENO133\IvanPlex
2a732cb343 made notes named and easier to traverse. 2024-01-09 21:10:48 -07:00
Hargata Softworks
da5e97143e Merge pull request #44 from hargata/Hargata/api
Add GET API Endpoints.
2024-01-09 19:00:12 -07:00
DESKTOP-GENO133\IvanPlex
9bdce69a2a added api documentation. 2024-01-09 18:57:55 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8b1db34860 added routes for API methods. 2024-01-09 18:26:54 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3c67d37e12 Added get methods to API. 2024-01-09 18:21:31 -07:00
DESKTOP-GENO133\IvanPlex
b73c2e979f draft for ze api controller. 2024-01-09 15:18:43 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a52fc29067 github pages. 2024-01-09 14:44:44 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c6774d4be7 Added import/export functionality for upgrades, service records, and repair records. 2024-01-09 13:51:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1271b3517d make report container smaller to accomodate for consolidated report below. 2024-01-09 12:59:25 -07:00
168 changed files with 7742 additions and 636 deletions

8
.env
View File

@@ -1,2 +1,8 @@
LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8
LANG=en_US.UTF-8
MailConfig__EmailServer=""
MailConfig__EmailFrom=""
MailConfig__UseSSL="false"
MailConfig__Port=587
MailConfig__Username=""
MailConfig__Password=""

View File

@@ -0,0 +1,30 @@
name: Docker Image To Docker Hub
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: "${{ secrets.DH_USER }}"
password: "${{ secrets.DH_PASS }}"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
hargata/lubelogger:latest

View File

@@ -1,4 +1,4 @@
name: Docker Image CI
name: Docker Image To GHCR
on:
push:
@@ -25,7 +25,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/hargata/lubelogger:latest
ghcr.io/hargata/lubelogger:latest

View File

@@ -0,0 +1,468 @@
using CarCareTracker.External.Implementations;
using CarCareTracker.External.Interfaces;
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace CarCareTracker.Controllers
{
[Authorize]
public class APIController : Controller
{
private readonly IVehicleDataAccess _dataAccess;
private readonly INoteDataAccess _noteDataAccess;
private readonly IServiceRecordDataAccess _serviceRecordDataAccess;
private readonly IGasRecordDataAccess _gasRecordDataAccess;
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
private readonly IGasHelper _gasHelper;
private readonly IUserLogic _userLogic;
private readonly IFileHelper _fileHelper;
public APIController(IVehicleDataAccess dataAccess,
IGasHelper gasHelper,
IReminderHelper reminderHelper,
INoteDataAccess noteDataAccess,
IServiceRecordDataAccess serviceRecordDataAccess,
IGasRecordDataAccess gasRecordDataAccess,
ICollisionRecordDataAccess collisionRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess,
IFileHelper fileHelper,
IUserLogic userLogic)
{
_dataAccess = dataAccess;
_noteDataAccess = noteDataAccess;
_serviceRecordDataAccess = serviceRecordDataAccess;
_gasRecordDataAccess = gasRecordDataAccess;
_collisionRecordDataAccess = collisionRecordDataAccess;
_taxRecordDataAccess = taxRecordDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess;
_gasHelper = gasHelper;
_reminderHelper = reminderHelper;
_userLogic = userLogic;
_fileHelper = fileHelper;
}
public IActionResult Index()
{
return View();
}
private int GetUserID()
{
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
[HttpGet]
[Route("/api/vehicles")]
public IActionResult Vehicles()
{
var result = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
result = _userLogic.FilterUserVehicles(result, GetUserID());
}
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/servicerecords")]
public IActionResult ServiceRecords(int vehicleId)
{
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/servicerecords/add")]
public IActionResult AddServiceRecord(int vehicleId, ServiceRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
{
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
if (string.IsNullOrWhiteSpace(input.Date) ||
string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Odometer) ||
string.IsNullOrWhiteSpace(input.Cost))
{
response.Success = false;
response.Message = "Input object invalid, Date, Description, Odometer, and Cost cannot be empty.";
Response.StatusCode = 400;
return Json(response);
}
try
{
var serviceRecord = new ServiceRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Mileage = int.Parse(input.Odometer),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord);
response.Success = true;
response.Message = "Service Record Added";
return Json(response);
}
catch (Exception ex)
{
response.Success = false;
response.Message = ex.Message;
Response.StatusCode = 500;
return Json(response);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/repairrecords")]
public IActionResult RepairRecords(int vehicleId)
{
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/repairrecords/add")]
public IActionResult AddRepairRecord(int vehicleId, ServiceRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
{
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
if (string.IsNullOrWhiteSpace(input.Date) ||
string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Odometer) ||
string.IsNullOrWhiteSpace(input.Cost))
{
response.Success = false;
response.Message = "Input object invalid, Date, Description, Odometer, and Cost cannot be empty.";
Response.StatusCode = 400;
return Json(response);
}
try
{
var repairRecord = new CollisionRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Mileage = int.Parse(input.Odometer),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(repairRecord);
response.Success = true;
response.Message = "Repair Record Added";
return Json(response);
}
catch (Exception ex)
{
response.Success = false;
response.Message = ex.Message;
Response.StatusCode = 500;
return Json(response);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/upgraderecords")]
public IActionResult UpgradeRecords(int vehicleId)
{
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/upgraderecords/add")]
public IActionResult AddUpgradeRecord(int vehicleId, ServiceRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
{
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
if (string.IsNullOrWhiteSpace(input.Date) ||
string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Odometer) ||
string.IsNullOrWhiteSpace(input.Cost))
{
response.Success = false;
response.Message = "Input object invalid, Date, Description, Odometer, and Cost cannot be empty.";
Response.StatusCode = 400;
return Json(response);
}
try
{
var upgradeRecord = new UpgradeRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Mileage = int.Parse(input.Odometer),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
};
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord);
response.Success = true;
response.Message = "Upgrade Record Added";
return Json(response);
}
catch (Exception ex)
{
response.Success = false;
response.Message = ex.Message;
Response.StatusCode = 500;
return Json(response);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/taxrecords")]
public IActionResult TaxRecords(int vehicleId)
{
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/taxrecords/add")]
public IActionResult AddTaxRecord(int vehicleId, TaxRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
{
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
if (string.IsNullOrWhiteSpace(input.Date) ||
string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Cost))
{
response.Success = false;
response.Message = "Input object invalid, Date, Description, and Cost cannot be empty.";
Response.StatusCode = 400;
return Json(response);
}
try
{
var taxRecord = new TaxRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Description = input.Description,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
};
_taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord);
response.Success = true;
response.Message = "Tax Record Added";
return Json(response);
}
catch (Exception ex)
{
response.Success = false;
response.Message = ex.Message;
Response.StatusCode = 500;
return Json(response);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/odometerrecords")]
public IActionResult OdometerRecords(int vehicleId)
{
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), Odometer = x.Mileage.ToString(), Notes = x.Notes });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/odometerrecords/add")]
public IActionResult AddOdometerRecord(int vehicleId, OdometerRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
{
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
if (string.IsNullOrWhiteSpace(input.Date) ||
string.IsNullOrWhiteSpace(input.Odometer))
{
response.Success = false;
response.Message = "Input object invalid, Date and Odometer cannot be empty.";
Response.StatusCode = 400;
return Json(response);
}
try
{
var odometerRecord = new OdometerRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Mileage = int.Parse(input.Odometer)
};
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
response.Success = true;
response.Message = "Odometer Record Added";
return Json(response);
} catch (Exception ex)
{
response.Success = false;
response.Message = ex.Message;
Response.StatusCode = 500;
return Json(response);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/gasrecords")]
public IActionResult GasRecords(int vehicleId, bool useMPG, bool useUKMPG)
{
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
var result = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG)
.Select(x => new GasRecordExportModel {
Date = x.Date,
Odometer = x.Mileage.ToString(),
Cost = x.Cost.ToString(),
FuelConsumed = x.Gallons.ToString(),
FuelEconomy = x.MilesPerGallon.ToString(),
IsFillToFull = x.IsFillToFull.ToString(),
MissedFuelUp = x.MissedFuelUp.ToString(),
Notes = x.Notes
});
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/gasrecords/add")]
public IActionResult AddGasRecord(int vehicleId, GasRecordExportModel input)
{
var response = new OperationResponse();
if (vehicleId == default)
{
response.Success = false;
response.Message = "Must provide a valid vehicle id";
Response.StatusCode = 400;
return Json(response);
}
if (string.IsNullOrWhiteSpace(input.Date) ||
string.IsNullOrWhiteSpace(input.Odometer) ||
string.IsNullOrWhiteSpace(input.FuelConsumed) ||
string.IsNullOrWhiteSpace(input.Cost) ||
string.IsNullOrWhiteSpace(input.IsFillToFull) ||
string.IsNullOrWhiteSpace(input.MissedFuelUp)
)
{
response.Success = false;
response.Message = "Input object invalid, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty.";
Response.StatusCode = 400;
return Json(response);
}
try
{
var gasRecord = new GasRecord()
{
VehicleId = vehicleId,
Date = DateTime.Parse(input.Date),
Mileage = int.Parse(input.Odometer),
Gallons = decimal.Parse(input.FuelConsumed),
IsFillToFull = bool.Parse(input.IsFillToFull),
MissedFuelUp = bool.Parse(input.MissedFuelUp),
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Cost = decimal.Parse(input.Cost)
};
_gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord);
response.Success = true;
response.Message = "Gas Record Added";
return Json(response);
}
catch (Exception ex)
{
response.Success = false;
response.Message = ex.Message;
Response.StatusCode = 500;
return Json(response);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/reminders")]
public IActionResult Reminders(int vehicleId)
{
var currentMileage = GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes});
return Json(results);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/makebackup")]
public IActionResult MakeBackup()
{
var result = _fileHelper.MakeBackup();
return Json(result);
}
private int GetMaxMileage(int vehicleId)
{
var numbersArray = new List<int>();
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
if (serviceRecords.Any())
{
numbersArray.Add(serviceRecords.Max(x => x.Mileage));
}
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
if (repairRecords.Any())
{
numbersArray.Add(repairRecords.Max(x => x.Mileage));
}
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
if (gasRecords.Any())
{
numbersArray.Add(gasRecords.Max(x => x.Mileage));
}
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (upgradeRecords.Any())
{
numbersArray.Add(upgradeRecords.Max(x => x.Mileage));
}
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
if (odometerRecords.Any())
{
numbersArray.Add(odometerRecords.Max(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
}
}

View File

@@ -0,0 +1,56 @@
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Net.Mail;
namespace CarCareTracker.Controllers
{
[Authorize(Roles = nameof(UserData.IsAdmin))]
public class AdminController : Controller
{
private ILoginLogic _loginLogic;
private IUserLogic _userLogic;
private IConfigHelper _configHelper;
public AdminController(ILoginLogic loginLogic, IUserLogic userLogic, IConfigHelper configHelper)
{
_loginLogic = loginLogic;
_userLogic = userLogic;
_configHelper = configHelper;
}
public IActionResult Index()
{
var viewModel = new AdminViewModel
{
Users = _loginLogic.GetAllUsers(),
Tokens = _loginLogic.GetAllTokens()
};
return View(viewModel);
}
public IActionResult GenerateNewToken(string emailAddress, bool autoNotify)
{
var result = _loginLogic.GenerateUserToken(emailAddress, autoNotify);
return Json(result);
}
[HttpPost]
public IActionResult DeleteToken(int tokenId)
{
var result = _loginLogic.DeleteUserToken(tokenId);
return Json(result);
}
[HttpPost]
public IActionResult DeleteUser(int userId)
{
var result =_userLogic.DeleteAllAccessToUser(userId) && _configHelper.DeleteUserConfig(userId) && _loginLogic.DeleteUser(userId);
return Json(result);
}
[HttpPost]
public IActionResult UpdateUserAdminStatus(int userId, bool isAdmin)
{
var result = _loginLogic.MakeUserAdmin(userId, isAdmin);
return Json(result);
}
}
}

View File

@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public class ErrorController : Controller
{
public IActionResult Unauthorized()
{
if (!User.IsInRole("CookieAuth"))
{
Response.StatusCode = 403;
return new EmptyResult();
}
return View("401");
}
}
}

View File

@@ -46,12 +46,25 @@ namespace CarCareTracker.Controllers
}
[HttpPost]
public ActionResult DeleteFiles(string fileLocation)
public IActionResult DeleteFiles(string fileLocation)
{
var result = _fileHelper.DeleteFile(fileLocation);
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public IActionResult MakeBackup()
{
var result = _fileHelper.MakeBackup();
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult RestoreBackup(string fileName)
{
var result = _fileHelper.RestoreBackup(fileName);
return Json(result);
}
private string UploadFile(IFormFile fileToUpload)
{
string uploadDirectory = "temp/";

View File

@@ -1,14 +1,11 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using LiteDB;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using static System.Net.Mime.MediaTypeNames;
using System.Drawing;
using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
using CarCareTracker.Helper;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using CarCareTracker.Logic;
namespace CarCareTracker.Controllers
{
@@ -17,17 +14,23 @@ namespace CarCareTracker.Controllers
{
private readonly ILogger<HomeController> _logger;
private readonly IVehicleDataAccess _dataAccess;
private readonly IFileHelper _fileHelper;
private readonly IConfiguration _config;
private readonly IUserLogic _userLogic;
private readonly IConfigHelper _config;
public HomeController(ILogger<HomeController> logger, IVehicleDataAccess dataAccess, IFileHelper fileHelper, IConfiguration configuration)
public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess,
IUserLogic userLogic,
IConfigHelper configuration)
{
_logger = logger;
_dataAccess = dataAccess;
_fileHelper = fileHelper;
_config = configuration;
_userLogic = userLogic;
}
private int GetUserID()
{
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
public IActionResult Index(string tab = "garage")
{
return View(model: tab);
@@ -35,53 +38,22 @@ namespace CarCareTracker.Controllers
public IActionResult Garage()
{
var vehiclesStored = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
return PartialView("_GarageDisplay", vehiclesStored);
}
public IActionResult Settings()
{
var userConfig = new UserConfig
{
EnableCsvImports = bool.Parse(_config[nameof(UserConfig.EnableCsvImports)]),
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)])
};
var userConfig = _config.GetUserConfig(User);
return PartialView("_Settings", userConfig);
}
[HttpPost]
public IActionResult WriteToSettings(UserConfig userConfig)
{
try
{
if (!System.IO.File.Exists(StaticHelper.UserConfigPath))
{
//if file doesn't exist it might be because it's running on a mounted volume in docker.
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(new UserConfig()));
}
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//copy over settings that are off limits on the settings page.
userConfig.EnableAuth = existingUserConfig.EnableAuth;
userConfig.UserNameHash = existingUserConfig.UserNameHash;
userConfig.UserPasswordHash = existingUserConfig.UserPasswordHash;
} else
{
userConfig.EnableAuth = false;
userConfig.UserNameHash = string.Empty;
userConfig.UserPasswordHash = string.Empty;
}
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(userConfig));
return Json(true);
} catch (Exception ex)
{
_logger.LogError(ex, "Error on saving config file.");
}
return Json(false);
var result = _config.SaveUserConfig(User, userConfig);
return Json(result);
}
public IActionResult Privacy()
{

View File

@@ -1,4 +1,5 @@
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
@@ -13,19 +14,34 @@ namespace CarCareTracker.Controllers
public class LoginController : Controller
{
private IDataProtector _dataProtector;
private ILoginLogic _loginLogic;
private readonly ILogger<LoginController> _logger;
public LoginController(
ILogger<LoginController> logger,
IDataProtectionProvider securityProvider
IDataProtectionProvider securityProvider,
ILoginLogic loginLogic
)
{
_dataProtector = securityProvider.CreateProtector("login");
_logger = logger;
_loginLogic = loginLogic;
}
public IActionResult Index()
{
return View();
}
public IActionResult Registration()
{
return View();
}
public IActionResult ForgotPassword()
{
return View();
}
public IActionResult ResetPassword()
{
return View();
}
[HttpPost]
public IActionResult Login(LoginModel credentials)
{
@@ -37,30 +53,18 @@ namespace CarCareTracker.Controllers
//compare it against hashed credentials
try
{
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
var userData = _loginLogic.ValidateUserCredentials(credentials);
if (userData.Id != default)
{
//create hashes of the login credentials.
var hashedUserName = Sha256_hash(credentials.UserName);
var hashedPassword = Sha256_hash(credentials.Password);
//compare against stored hash.
if (hashedUserName == existingUserConfig.UserNameHash &&
hashedPassword == existingUserConfig.UserPasswordHash)
AuthCookie authCookie = new AuthCookie
{
//auth success, create auth cookie
//encrypt stuff.
AuthCookie authCookie = new AuthCookie
{
Id = 1, //this is hardcoded for now
UserName = credentials.UserName,
ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1)
};
var serializedCookie = JsonSerializer.Serialize(authCookie);
var encryptedCookie = _dataProtector.Protect(serializedCookie);
Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) });
return Json(true);
}
UserData = userData,
ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1)
};
var serializedCookie = JsonSerializer.Serialize(authCookie);
var encryptedCookie = _dataProtector.Protect(serializedCookie);
Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) });
return Json(true);
}
}
catch (Exception ex)
@@ -69,26 +73,33 @@ namespace CarCareTracker.Controllers
}
return Json(false);
}
[HttpPost]
public IActionResult Register(LoginModel credentials)
{
var result = _loginLogic.RegisterNewUser(credentials);
return Json(result);
}
[HttpPost]
public IActionResult RequestResetPassword(LoginModel credentials)
{
var result = _loginLogic.RequestResetPassword(credentials);
return Json(result);
}
[HttpPost]
public IActionResult PerformPasswordReset(LoginModel credentials)
{
var result = _loginLogic.ResetPasswordByUser(credentials);
return Json(result);
}
[Authorize] //User must already be logged in to do this.
[HttpPost]
public IActionResult CreateLoginCreds(LoginModel credentials)
{
try
{
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//create hashes of the login credentials.
var hashedUserName = Sha256_hash(credentials.UserName);
var hashedPassword = Sha256_hash(credentials.Password);
//copy over settings that are off limits on the settings page.
existingUserConfig.EnableAuth = true;
existingUserConfig.UserNameHash = hashedUserName;
existingUserConfig.UserPasswordHash = hashedPassword;
}
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
return Json(true);
var result = _loginLogic.CreateRootUserCredentials(credentials);
return Json(result);
}
catch (Exception ex)
{
@@ -102,19 +113,13 @@ namespace CarCareTracker.Controllers
{
try
{
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//copy over settings that are off limits on the settings page.
existingUserConfig.EnableAuth = false;
existingUserConfig.UserNameHash = string.Empty;
existingUserConfig.UserPasswordHash = string.Empty;
}
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
var result = _loginLogic.DeleteRootUserCredentials();
//destroy any login cookies.
Response.Cookies.Delete("ACCESS_TOKEN");
return Json(true);
if (result)
{
Response.Cookies.Delete("ACCESS_TOKEN");
}
return Json(result);
}
catch (Exception ex)
{
@@ -129,20 +134,5 @@ namespace CarCareTracker.Controllers
Response.Cookies.Delete("ACCESS_TOKEN");
return Json(true);
}
private static string Sha256_hash(string value)
{
StringBuilder Sb = new StringBuilder();
using (var hash = SHA256.Create())
{
Encoding enc = Encoding.UTF8;
byte[] result = hash.ComputeHash(enc.GetBytes(value));
foreach (byte b in result)
Sb.Append(b.ToString("x2"));
}
return Sb.ToString();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,13 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /App
COPY . ./
RUN dotnet restore
RUN dotnet publish -c Release -o out
ARG TARGETARCH
RUN dotnet restore -a $TARGETARCH
RUN dotnet publish -a $TARGETARCH -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /App
COPY --from=build-env /App/out .
EXPOSE 8080
CMD ["./CarCareTracker"]
CMD ["./CarCareTracker"]

View File

@@ -5,6 +5,13 @@
ServiceRecord = 0,
RepairRecord = 1,
GasRecord = 2,
TaxRecord = 3
TaxRecord = 3,
UpgradeRecord = 4,
ReminderRecord = 5,
NoteRecord = 6,
SupplyRecord = 7,
Dashboard = 8,
PlanRecord = 9,
OdometerRecord = 10
}
}

9
Enum/PlanPriority.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public enum PlanPriority
{
Critical = 0,
Normal = 1,
Low = 2
}
}

10
Enum/PlanProgress.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace CarCareTracker.Models
{
public enum PlanProgress
{
Backlog = 0,
InProgress = 1,
Testing = 2,
Done = 3
}
}

View File

@@ -0,0 +1,21 @@
namespace CarCareTracker.Models
{
public enum ReminderMileageInterval
{
FiftyMiles = 50,
OneHundredMiles = 100,
FiveHundredMiles = 500,
OneThousandMiles = 1000,
ThreeThousandMiles = 3000,
FourThousandMiles = 4000,
FiveThousandMiles = 5000,
SevenThousandFiveHundredMiles = 7500,
TenThousandMiles = 10000,
FifteenThousandMiles = 15000,
TwentyThousandMiles = 20000,
ThirtyThousandMiles = 30000,
FiftyThousandMiles = 50000,
OneHundredThousandMiles = 100000,
OneHundredFiftyThousandMiles = 150000
}
}

View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public enum ReminderMonthInterval
{
ThreeMonths = 3,
SixMonths = 6,
OneYear = 12,
TwoYears = 24,
ThreeYears = 36,
FiveYears = 60
}
}

View File

@@ -9,16 +9,24 @@ namespace CarCareTracker.External.Implementations
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "notes";
public Note GetNoteByVehicleId(int vehicleId)
public List<Note> GetNotesByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Note>(tableName);
var noteToReturn = table.FindOne(Query.EQ(nameof(Note.VehicleId), vehicleId));
return noteToReturn ?? new Note();
var noteToReturn = table.Find(Query.EQ(nameof(Note.VehicleId), vehicleId));
return noteToReturn.ToList() ?? new List<Note>();
};
}
public bool SaveNoteToVehicleId(Note note)
public Note GetNoteById(int noteId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Note>(tableName);
return table.FindById(noteId);
};
}
public bool SaveNoteToVehicle(Note note)
{
using (var db = new LiteDatabase(dbName))
{
@@ -27,12 +35,21 @@ namespace CarCareTracker.External.Implementations
return true;
};
}
public bool DeleteNoteByVehicleId(int vehicleId)
public bool DeleteNoteById(int noteId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Note>(tableName);
table.DeleteMany(Query.EQ(nameof(Note.VehicleId), vehicleId));
table.Delete(noteId);
return true;
};
}
public bool DeleteAllNotesByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Note>(tableName);
var notes = table.DeleteMany(Query.EQ(nameof(Note.VehicleId), vehicleId));
return true;
};
}

View File

@@ -0,0 +1,57 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class OdometerRecordDataAccess : IOdometerRecordDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "odometerrecords";
public List<OdometerRecord> GetOdometerRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<OdometerRecord>(tableName);
var odometerRecords = table.Find(Query.EQ(nameof(OdometerRecord.VehicleId), vehicleId));
return odometerRecords.ToList() ?? new List<OdometerRecord>();
};
}
public OdometerRecord GetOdometerRecordById(int odometerRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<OdometerRecord>(tableName);
return table.FindById(odometerRecordId);
};
}
public bool DeleteOdometerRecordById(int odometerRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<OdometerRecord>(tableName);
table.Delete(odometerRecordId);
return true;
};
}
public bool SaveOdometerRecordToVehicle(OdometerRecord odometerRecord)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<OdometerRecord>(tableName);
table.Upsert(odometerRecord);
return true;
};
}
public bool DeleteAllOdometerRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<OdometerRecord>(tableName);
var odometerRecords = table.DeleteMany(Query.EQ(nameof(OdometerRecord.VehicleId), vehicleId));
return true;
};
}
}
}

View File

@@ -0,0 +1,57 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class PlanRecordDataAccess : IPlanRecordDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "planrecords";
public List<PlanRecord> GetPlanRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecord>(tableName);
var planRecords = table.Find(Query.EQ(nameof(PlanRecord.VehicleId), vehicleId));
return planRecords.ToList() ?? new List<PlanRecord>();
};
}
public PlanRecord GetPlanRecordById(int planRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecord>(tableName);
return table.FindById(planRecordId);
};
}
public bool DeletePlanRecordById(int planRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecord>(tableName);
table.Delete(planRecordId);
return true;
};
}
public bool SavePlanRecordToVehicle(PlanRecord planRecord)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecord>(tableName);
table.Upsert(planRecord);
return true;
};
}
public bool DeleteAllPlanRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<PlanRecord>(tableName);
var planRecords = table.DeleteMany(Query.EQ(nameof(PlanRecord.VehicleId), vehicleId));
return true;
};
}
}
}

View File

@@ -0,0 +1,57 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class SupplyRecordDataAccess : ISupplyRecordDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "supplyrecords";
public List<SupplyRecord> GetSupplyRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<SupplyRecord>(tableName);
var supplyRecords = table.Find(Query.EQ(nameof(SupplyRecord.VehicleId), vehicleId));
return supplyRecords.ToList() ?? new List<SupplyRecord>();
};
}
public SupplyRecord GetSupplyRecordById(int supplyRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<SupplyRecord>(tableName);
return table.FindById(supplyRecordId);
};
}
public bool DeleteSupplyRecordById(int supplyRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<SupplyRecord>(tableName);
table.Delete(supplyRecordId);
return true;
};
}
public bool SaveSupplyRecordToVehicle(SupplyRecord supplyRecord)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<SupplyRecord>(tableName);
table.Upsert(supplyRecord);
return true;
};
}
public bool DeleteAllSupplyRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<SupplyRecord>(tableName);
var supplyRecords = table.DeleteMany(Query.EQ(nameof(SupplyRecord.VehicleId), vehicleId));
return true;
};
}
}
}

View File

@@ -0,0 +1,57 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class TokenRecordDataAccess : ITokenRecordDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "tokenrecords";
public List<Token> GetTokens()
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Token>(tableName);
return table.FindAll().ToList();
};
}
public Token GetTokenRecordByBody(string tokenBody)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Token>(tableName);
var tokenRecord = table.FindOne(Query.EQ(nameof(Token.Body), tokenBody));
return tokenRecord ?? new Token();
};
}
public Token GetTokenRecordByEmailAddress(string emailAddress)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Token>(tableName);
var tokenRecord = table.FindOne(Query.EQ(nameof(Token.EmailAddress), emailAddress));
return tokenRecord ?? new Token();
};
}
public bool CreateNewToken(Token token)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Token>(tableName);
table.Insert(token);
return true;
};
}
public bool DeleteToken(int tokenId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Token>(tableName);
table.Delete(tokenId);
return true;
};
}
}
}

View File

@@ -0,0 +1,88 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class UserAccessDataAccess : IUserAccessDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "useraccessrecords";
/// <summary>
/// Gets a list of vehicles user have access to.
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public List<UserAccess> GetUserAccessByUserId(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
return table.Find(x=>x.Id.UserId == userId).ToList();
};
}
public UserAccess GetUserAccessByVehicleAndUserId(int userId, int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
return table.Find(x => x.Id.UserId == userId && x.Id.VehicleId == vehicleId).FirstOrDefault();
};
}
public List<UserAccess> GetUserAccessByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
return table.Find(x => x.Id.VehicleId == vehicleId).ToList();
};
}
public bool SaveUserAccess(UserAccess userAccess)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
table.Upsert(userAccess);
return true;
};
}
public bool DeleteUserAccess(int userId, int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
table.DeleteMany(x => x.Id.UserId == userId && x.Id.VehicleId == vehicleId);
return true;
};
}
/// <summary>
/// Delete all access records when a vehicle is deleted.
/// </summary>
/// <param name="vehicleId"></param>
/// <returns></returns>
public bool DeleteAllAccessRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
table.DeleteMany(x=>x.Id.VehicleId == vehicleId);
return true;
};
}
/// <summary>
/// Delee all access records when a user is deleted.
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public bool DeleteAllAccessRecordsByUserId(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
table.DeleteMany(x => x.Id.UserId == userId);
return true;
};
}
}
}

View File

@@ -0,0 +1,40 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace CarCareTracker.External.Implementations
{
public class UserConfigDataAccess: IUserConfigDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "userconfigrecords";
public UserConfigData GetUserConfig(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserConfigData>(tableName);
return table.FindById(userId);
};
}
public bool SaveUserConfig(UserConfigData userConfigData)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserConfigData>(tableName);
table.Upsert(userConfigData);
return true;
};
}
public bool DeleteUserConfig(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserConfigData>(tableName);
table.Delete(userId);
return true;
};
}
}
}

View File

@@ -0,0 +1,66 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class UserRecordDataAccess : IUserRecordDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "userrecords";
public List<UserData> GetUsers()
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserData>(tableName);
return table.FindAll().ToList();
};
}
public UserData GetUserRecordByUserName(string userName)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserData>(tableName);
var userRecord = table.FindOne(Query.EQ(nameof(UserData.UserName), userName));
return userRecord ?? new UserData();
};
}
public UserData GetUserRecordByEmailAddress(string emailAddress)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserData>(tableName);
var userRecord = table.FindOne(Query.EQ(nameof(UserData.EmailAddress), emailAddress));
return userRecord ?? new UserData();
};
}
public UserData GetUserRecordById(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserData>(tableName);
var userRecord = table.FindById(userId);
return userRecord ?? new UserData();
};
}
public bool SaveUserRecord(UserData userRecord)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserData>(tableName);
table.Upsert(userRecord);
return true;
};
}
public bool DeleteUserRecord(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserData>(tableName);
table.Delete(userId);
return true;
};
}
}
}

View File

@@ -14,7 +14,7 @@ namespace CarCareTracker.External.Implementations
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Vehicle>(tableName);
table.Upsert(vehicle);
var result = table.Upsert(vehicle);
return true;
};
}

View File

@@ -4,8 +4,10 @@ namespace CarCareTracker.External.Interfaces
{
public interface INoteDataAccess
{
public Note GetNoteByVehicleId(int vehicleId);
public bool SaveNoteToVehicleId(Note note);
bool DeleteNoteByVehicleId(int vehicleId);
public List<Note> GetNotesByVehicleId(int vehicleId);
public Note GetNoteById(int noteId);
public bool SaveNoteToVehicle(Note note);
public bool DeleteNoteById(int noteId);
public bool DeleteAllNotesByVehicleId(int vehicleId);
}
}

View File

@@ -0,0 +1,13 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IOdometerRecordDataAccess
{
public List<OdometerRecord> GetOdometerRecordsByVehicleId(int vehicleId);
public OdometerRecord GetOdometerRecordById(int odometerRecordId);
public bool DeleteOdometerRecordById(int odometerRecordId);
public bool SaveOdometerRecordToVehicle(OdometerRecord odometerRecord);
public bool DeleteAllOdometerRecordsByVehicleId(int vehicleId);
}
}

View File

@@ -0,0 +1,13 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IPlanRecordDataAccess
{
public List<PlanRecord> GetPlanRecordsByVehicleId(int vehicleId);
public PlanRecord GetPlanRecordById(int planRecordId);
public bool DeletePlanRecordById(int planRecordId);
public bool SavePlanRecordToVehicle(PlanRecord planRecord);
public bool DeleteAllPlanRecordsByVehicleId(int vehicleId);
}
}

View File

@@ -0,0 +1,13 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface ISupplyRecordDataAccess
{
public List<SupplyRecord> GetSupplyRecordsByVehicleId(int vehicleId);
public SupplyRecord GetSupplyRecordById(int supplyRecordId);
public bool DeleteSupplyRecordById(int supplyRecordId);
public bool SaveSupplyRecordToVehicle(SupplyRecord supplyRecord);
public bool DeleteAllSupplyRecordsByVehicleId(int vehicleId);
}
}

View File

@@ -0,0 +1,13 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface ITokenRecordDataAccess
{
public List<Token> GetTokens();
public Token GetTokenRecordByBody(string tokenBody);
public Token GetTokenRecordByEmailAddress(string emailAddress);
public bool CreateNewToken(Token token);
public bool DeleteToken(int tokenId);
}
}

View File

@@ -0,0 +1,15 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IUserAccessDataAccess
{
List<UserAccess> GetUserAccessByUserId(int userId);
UserAccess GetUserAccessByVehicleAndUserId(int userId, int vehicleId);
List<UserAccess> GetUserAccessByVehicleId(int vehicleId);
bool SaveUserAccess(UserAccess userAccess);
bool DeleteUserAccess(int userId, int vehicleId);
bool DeleteAllAccessRecordsByVehicleId(int vehicleId);
bool DeleteAllAccessRecordsByUserId(int userId);
}
}

View File

@@ -0,0 +1,11 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IUserConfigDataAccess
{
public UserConfigData GetUserConfig(int userId);
public bool SaveUserConfig(UserConfigData userConfigData);
public bool DeleteUserConfig(int userId);
}
}

View File

@@ -0,0 +1,14 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IUserRecordDataAccess
{
public List<UserData> GetUsers();
public UserData GetUserRecordByUserName(string userName);
public UserData GetUserRecordByEmailAddress(string emailAddress);
public UserData GetUserRecordById(int userId);
public bool SaveUserRecord(UserData userRecord);
public bool DeleteUserRecord(int userId);
}
}

View File

@@ -0,0 +1,28 @@
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Security.Claims;
namespace CarCareTracker.Filter
{
public class CollaboratorFilter: ActionFilterAttribute
{
private readonly IUserLogic _userLogic;
public CollaboratorFilter(IUserLogic userLogic) {
_userLogic = userLogic;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.HttpContext.User.IsInRole(nameof(UserData.IsRootUser)))
{
var vehicleId = int.Parse(filterContext.ActionArguments["vehicleId"].ToString());
var userId = int.Parse(filterContext.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
if (!_userLogic.UserCanEditVehicle(userId, vehicleId))
{
filterContext.Result = new RedirectResult("/Error/Unauthorized");
}
}
}
}
}

142
Helper/ConfigHelper.cs Normal file
View File

@@ -0,0 +1,142 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Security.Claims;
namespace CarCareTracker.Helper
{
public interface IConfigHelper
{
UserConfig GetUserConfig(ClaimsPrincipal user);
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
public bool DeleteUserConfig(int userId);
}
public class ConfigHelper : IConfigHelper
{
private readonly IConfiguration _config;
private readonly IUserConfigDataAccess _userConfig;
private IMemoryCache _cache;
public ConfigHelper(IConfiguration serverConfig,
IUserConfigDataAccess userConfig,
IMemoryCache memoryCache)
{
_config = serverConfig;
_userConfig = userConfig;
_cache = memoryCache;
}
public bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData)
{
var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier);
int userId = 0;
if (storedUserId != null)
{
userId = int.Parse(storedUserId);
}
bool isRootUser = user.IsInRole(nameof(UserData.IsRootUser)) || userId == -1;
if (isRootUser)
{
try
{
if (!File.Exists(StaticHelper.UserConfigPath))
{
//if file doesn't exist it might be because it's running on a mounted volume in docker.
File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(new UserConfig()));
}
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//copy over settings that are off limits on the settings page.
configData.EnableAuth = existingUserConfig.EnableAuth;
configData.UserNameHash = existingUserConfig.UserNameHash;
configData.UserPasswordHash = existingUserConfig.UserPasswordHash;
}
else
{
configData.EnableAuth = false;
configData.UserNameHash = string.Empty;
configData.UserPasswordHash = string.Empty;
}
File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(configData));
_cache.Set<UserConfig>($"userConfig_{userId}", configData);
return true;
}
catch (Exception ex)
{
return false;
}
} else
{
var userConfig = new UserConfigData()
{
Id = userId,
UserConfig = configData
};
var result = _userConfig.SaveUserConfig(userConfig);
_cache.Set<UserConfig>($"userConfig_{userId}", configData);
return result;
}
}
public bool DeleteUserConfig(int userId)
{
_cache.Remove($"userConfig_{userId}");
var result = _userConfig.DeleteUserConfig(userId);
return result;
}
public UserConfig GetUserConfig(ClaimsPrincipal user)
{
var serverConfig = new UserConfig
{
EnableCsvImports = bool.Parse(_config[nameof(UserConfig.EnableCsvImports)]),
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]),
UseThreeDecimalGasCost = bool.Parse(_config[nameof(UserConfig.UseThreeDecimalGasCost)]),
EnableAutoReminderRefresh = bool.Parse(_config[nameof(UserConfig.EnableAutoReminderRefresh)]),
EnableAutoOdometerInsert = bool.Parse(_config[nameof(UserConfig.EnableAutoOdometerInsert)]),
VisibleTabs = _config.GetSection("VisibleTabs").Get<List<ImportMode>>(),
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
};
int userId = 0;
if (user != null)
{
var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier);
if (storedUserId != null)
{
userId = int.Parse(storedUserId);
}
} else
{
return serverConfig;
}
return _cache.GetOrCreate<UserConfig>($"userConfig_{userId}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromHours(1);
if (!user.Identity.IsAuthenticated)
{
return serverConfig;
}
bool isRootUser = user.IsInRole(nameof(UserData.IsRootUser)) || userId == -1;
if (isRootUser)
{
return serverConfig;
}
else
{
var result = _userConfig.GetUserConfig(userId);
if (result == null)
{
return serverConfig;
}
else
{
return result.UserConfig;
}
}
});
}
}
}

View File

@@ -1,19 +1,25 @@
namespace CarCareTracker.Helper
using System.IO.Compression;
namespace CarCareTracker.Helper
{
public interface IFileHelper
{
string GetFullFilePath(string currentFilePath);
public string MoveFileFromTemp(string currentFilePath, string newFolder);
public bool DeleteFile(string currentFilePath);
string GetFullFilePath(string currentFilePath, bool mustExist = true);
string MoveFileFromTemp(string currentFilePath, string newFolder);
bool DeleteFile(string currentFilePath);
string MakeBackup();
bool RestoreBackup(string fileName);
}
public class FileHelper: IFileHelper
public class FileHelper : IFileHelper
{
private readonly IWebHostEnvironment _webEnv;
public FileHelper(IWebHostEnvironment webEnv)
private readonly ILogger<IFileHelper> _logger;
public FileHelper(IWebHostEnvironment webEnv, ILogger<IFileHelper> logger)
{
_webEnv = webEnv;
_logger = logger;
}
public string GetFullFilePath(string currentFilePath)
public string GetFullFilePath(string currentFilePath, bool mustExist = true)
{
if (currentFilePath.StartsWith("/"))
{
@@ -23,11 +29,131 @@
if (File.Exists(oldFilePath))
{
return oldFilePath;
} else
}
else if (!mustExist)
{
return oldFilePath;
}
{
return string.Empty;
}
}
public bool RestoreBackup(string fileName)
{
var fullFilePath = GetFullFilePath(fileName);
if (string.IsNullOrWhiteSpace(fullFilePath))
{
return false;
}
try
{
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{Guid.NewGuid()}");
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
//extract zip file
ZipFile.ExtractToDirectory(fullFilePath, tempPath);
//copy over images and documents.
var imagePath = Path.Combine(tempPath, "images");
var documentPath = Path.Combine(tempPath, "documents");
var dataPath = Path.Combine(tempPath, StaticHelper.DbName);
var configPath = Path.Combine(tempPath, StaticHelper.UserConfigPath);
if (Directory.Exists(imagePath))
{
var existingPath = Path.Combine(_webEnv.WebRootPath, "images");
if (!Directory.Exists(existingPath))
{
Directory.CreateDirectory(existingPath);
}
//copy each files from temp folder to newPath
var filesToUpload = Directory.GetFiles(imagePath);
foreach(string file in filesToUpload)
{
File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true);
}
}
if (Directory.Exists(documentPath))
{
var existingPath = Path.Combine(_webEnv.WebRootPath, "documents");
if (!Directory.Exists(existingPath))
{
Directory.CreateDirectory(existingPath);
}
//copy each files from temp folder to newPath
var filesToUpload = Directory.GetFiles(documentPath);
foreach (string file in filesToUpload)
{
File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true);
}
}
if (File.Exists(dataPath))
{
//data path will always exist as it is created on startup if not.
File.Move(dataPath, StaticHelper.DbName, true);
}
if (File.Exists(configPath))
{
//check if config folder exists.
if (!Directory.Exists("config/"))
{
Directory.CreateDirectory("config/");
}
File.Move(configPath, StaticHelper.UserConfigPath, true);
}
return true;
} catch (Exception ex)
{
_logger.LogError(ex, $"Error Restoring Database Backup: {ex.Message}");
return false;
}
}
public string MakeBackup()
{
var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}";
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{folderName}");
var imagePath = Path.Combine(_webEnv.WebRootPath, "images");
var documentPath = Path.Combine(_webEnv.WebRootPath, "documents");
var dataPath = StaticHelper.DbName;
var configPath = StaticHelper.UserConfigPath;
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
if (Directory.Exists(imagePath))
{
var files = Directory.GetFiles(imagePath);
foreach (var file in files)
{
var newPath = Path.Combine(tempPath, "images");
Directory.CreateDirectory(newPath);
File.Copy(file, $"{newPath}/{Path.GetFileName(file)}");
}
}
if (Directory.Exists(documentPath))
{
var files = Directory.GetFiles(documentPath);
foreach (var file in files)
{
var newPath = Path.Combine(tempPath, "documents");
Directory.CreateDirectory(newPath);
File.Copy(file, $"{newPath}/{Path.GetFileName(file)}");
}
}
if (File.Exists(dataPath))
{
var newPath = Path.Combine(tempPath, "data");
Directory.CreateDirectory(newPath);
File.Copy(dataPath, $"{newPath}/{Path.GetFileName(dataPath)}");
}
if (File.Exists(configPath))
{
var newPath = Path.Combine(tempPath, "config");
Directory.CreateDirectory(newPath);
File.Copy(configPath, $"{newPath}/{Path.GetFileName(configPath)}");
}
var destFilePath = $"{tempPath}.zip";
ZipFile.CreateFromDirectory(tempPath, destFilePath);
//delete temp directory
Directory.Delete(tempPath, true);
return $"/temp/{folderName}.zip";
}
public string MoveFileFromTemp(string currentFilePath, string newFolder)
{
string tempPath = "temp/";
@@ -35,7 +161,8 @@
{
return currentFilePath;
}
if (currentFilePath.StartsWith("/")) {
if (currentFilePath.StartsWith("/"))
{
currentFilePath = currentFilePath.Substring(1);
}
string uploadPath = Path.Combine(_webEnv.WebRootPath, newFolder);
@@ -64,7 +191,8 @@
if (!File.Exists(filePath)) //verify file no longer exists.
{
return true;
} else
}
else
{
return false;
}

116
Helper/GasHelper.cs Normal file
View File

@@ -0,0 +1,116 @@
using CarCareTracker.Models;
namespace CarCareTracker.Helper
{
public interface IGasHelper
{
List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG);
string GetAverageGasMileage(List<GasRecordViewModel> results);
}
public class GasHelper : IGasHelper
{
public string GetAverageGasMileage(List<GasRecordViewModel> results)
{
var recordWithCalculatedMPG = results.Where(x => x.MilesPerGallon > 0);
var minMileage = results.Min(x => x.Mileage);
if (recordWithCalculatedMPG.Any())
{
var maxMileage = recordWithCalculatedMPG.Max(x => x.Mileage);
var totalGallonsConsumed = recordWithCalculatedMPG.Sum(x => x.Gallons);
var deltaMileage = maxMileage - minMileage;
var averageGasMileage = (maxMileage - minMileage) / totalGallonsConsumed;
return averageGasMileage.ToString("F");
}
return "0";
}
public List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG)
{
var computedResults = new List<GasRecordViewModel>();
int previousMileage = 0;
decimal unFactoredConsumption = 0.00M;
int unFactoredMileage = 0;
//perform computation.
for (int i = 0; i < result.Count; i++)
{
var currentObject = result[i];
decimal convertedConsumption;
if (useUKMPG && useMPG)
{
//if we're using UK MPG and the user wants imperial calculation insteace of l/100km
//if UK MPG is selected then the gas consumption are stored in liters but need to convert into UK gallons for computation.
convertedConsumption = currentObject.Gallons / 4.546M;
}
else
{
convertedConsumption = currentObject.Gallons;
}
if (i > 0)
{
var deltaMileage = currentObject.Mileage - previousMileage;
var gasRecordViewModel = new GasRecordViewModel()
{
Id = currentObject.Id,
VehicleId = currentObject.VehicleId,
MonthId = currentObject.Date.Month,
Date = currentObject.Date.ToShortDateString(),
Mileage = currentObject.Mileage,
Gallons = convertedConsumption,
Cost = currentObject.Cost,
DeltaMileage = deltaMileage,
CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0,
IsFillToFull = currentObject.IsFillToFull,
MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes
};
if (currentObject.MissedFuelUp)
{
//if they missed a fuel up, we skip MPG calculation.
gasRecordViewModel.MilesPerGallon = 0;
//reset unFactored vars for missed fuel up because the numbers wont be reliable.
unFactoredConsumption = 0;
unFactoredMileage = 0;
}
else if (currentObject.IsFillToFull)
{
//if user filled to full.
if (convertedConsumption > 0.00M)
{
gasRecordViewModel.MilesPerGallon = useMPG ? (unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption));
}
//reset unFactored vars
unFactoredConsumption = 0;
unFactoredMileage = 0;
}
else
{
unFactoredConsumption += convertedConsumption;
unFactoredMileage += deltaMileage;
gasRecordViewModel.MilesPerGallon = 0;
}
computedResults.Add(gasRecordViewModel);
}
else
{
computedResults.Add(new GasRecordViewModel()
{
Id = currentObject.Id,
VehicleId = currentObject.VehicleId,
MonthId = currentObject.Date.Month,
Date = currentObject.Date.ToShortDateString(),
Mileage = currentObject.Mileage,
Gallons = convertedConsumption,
Cost = currentObject.Cost,
DeltaMileage = 0,
MilesPerGallon = 0,
CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0,
IsFillToFull = currentObject.IsFillToFull,
MissedFuelUp = currentObject.MissedFuelUp,
Notes = currentObject.Notes
});
}
previousMileage = currentObject.Mileage;
}
return computedResults;
}
}
}

85
Helper/MailHelper.cs Normal file
View File

@@ -0,0 +1,85 @@
using CarCareTracker.Models;
using System.Net.Mail;
using System.Net;
namespace CarCareTracker.Helper
{
public interface IMailHelper
{
OperationResponse NotifyUserForRegistration(string emailAddress, string token);
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
}
public class MailHelper : IMailHelper
{
private readonly MailConfig mailConfig;
public MailHelper(
IConfiguration config
) {
//load mailConfig from Configuration
mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
}
public OperationResponse NotifyUserForRegistration(string emailAddress, string token)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
}
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token)) {
return new OperationResponse { Success = false, Message = "Email Address or Token is invalid" };
}
string emailSubject = "Your Registration Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please complete your registration for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
} else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
public OperationResponse NotifyUserForPasswordReset(string emailAddress, string token)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
}
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token))
{
return new OperationResponse { Success = false, Message = "Email Address or Token is invalid" };
}
string emailSubject = "Your Password Reset Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please reset your password for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
}
else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
private bool SendEmail(string emailTo, string emailSubject, string emailBody) {
string to = emailTo;
string from = mailConfig.EmailFrom;
var server = mailConfig.EmailServer;
MailMessage message = new MailMessage(from, to);
message.Subject = emailSubject;
message.Body = emailBody;
SmtpClient client = new SmtpClient(server);
client.EnableSsl = mailConfig.UseSSL;
client.Port = mailConfig.Port;
client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password);
try
{
client.Send(message);
return true;
}
catch (Exception ex)
{
return false;
}
}
}
}

116
Helper/ReminderHelper.cs Normal file
View File

@@ -0,0 +1,116 @@
using CarCareTracker.Models;
namespace CarCareTracker.Helper
{
public interface IReminderHelper
{
ReminderRecord GetUpdatedRecurringReminderRecord(ReminderRecord existingReminder);
List<ReminderRecordViewModel> GetReminderRecordViewModels(List<ReminderRecord> reminders, int currentMileage, DateTime dateCompare);
}
public class ReminderHelper: IReminderHelper
{
public ReminderRecord GetUpdatedRecurringReminderRecord(ReminderRecord existingReminder)
{
if (existingReminder.Metric == ReminderMetric.Both)
{
existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval);
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
}
else if (existingReminder.Metric == ReminderMetric.Odometer)
{
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
}
else if (existingReminder.Metric == ReminderMetric.Date)
{
existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval);
}
return existingReminder;
}
public List<ReminderRecordViewModel> GetReminderRecordViewModels(List<ReminderRecord> reminders, int currentMileage, DateTime dateCompare)
{
List<ReminderRecordViewModel> reminderViewModels = new List<ReminderRecordViewModel>();
foreach (var reminder in reminders)
{
var reminderViewModel = new ReminderRecordViewModel()
{
Id = reminder.Id,
VehicleId = reminder.VehicleId,
Date = reminder.Date,
Mileage = reminder.Mileage,
Description = reminder.Description,
Notes = reminder.Notes,
Metric = reminder.Metric,
IsRecurring = reminder.IsRecurring
};
if (reminder.Metric == ReminderMetric.Both)
{
if (reminder.Date < dateCompare)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Date < dateCompare.AddDays(7))
{
//if less than a week from today or less than 50 miles from current mileage then very urgent.
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
//have to specify by which metric this reminder is urgent.
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage + 50)
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Date < dateCompare.AddDays(30))
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage + 100)
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
}
else if (reminder.Metric == ReminderMetric.Date)
{
if (reminder.Date < dateCompare)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
}
else if (reminder.Date < dateCompare.AddDays(7))
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
}
else if (reminder.Date < dateCompare.AddDays(30))
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
}
}
else if (reminder.Metric == ReminderMetric.Odometer)
{
if (reminder.Mileage < currentMileage)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Mileage < currentMileage + 50)
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
}
else if (reminder.Mileage < currentMileage + 100)
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
}
}
reminderViewModels.Add(reminderViewModel);
}
return reminderViewModels;
}
}
}

82
Helper/ReportHelper.cs Normal file
View File

@@ -0,0 +1,82 @@
using CarCareTracker.Models;
using System.Globalization;
namespace CarCareTracker.Helper
{
public interface IReportHelper
{
IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0);
IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0);
}
public class ReportHelper: IReportHelper
{
public IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0)
{
if (year != default)
{
serviceRecords.RemoveAll(x => x.Date.Year != year);
}
return serviceRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
public IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0)
{
if (year != default)
{
repairRecords.RemoveAll(x => x.Date.Year != year);
}
return repairRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
public IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0)
{
if (year != default)
{
upgradeRecords.RemoveAll(x => x.Date.Year != year);
}
return upgradeRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
public IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0)
{
if (year != default)
{
gasRecords.RemoveAll(x => x.Date.Year != year);
}
return gasRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
public IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0)
{
if (year != default)
{
taxRecords.RemoveAll(x => x.Date.Year != year);
}
return taxRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthId = x.Key,
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y => y.Cost)
});
}
}
}

View File

@@ -1,4 +1,6 @@
namespace CarCareTracker.Helper
using CarCareTracker.Models;
namespace CarCareTracker.Helper
{
/// <summary>
/// helper method for static vars
@@ -7,5 +9,59 @@
{
public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json";
public static string GenericErrorMessage = "An error occurred, please try again later";
public static string TruncateStrings(string input, int maxLength = 25)
{
if (string.IsNullOrWhiteSpace(input))
{
return string.Empty;
}
if (input.Length > maxLength)
{
return (input.Substring(0, maxLength) + "...");
}
else
{
return input;
}
}
public static string DefaultActiveTab(UserConfig userConfig, ImportMode tab)
{
var defaultTab = userConfig.DefaultTab;
var visibleTabs = userConfig.VisibleTabs;
if (visibleTabs.Contains(tab) && tab == defaultTab)
{
return "active";
}
else if (!visibleTabs.Contains(tab))
{
return "d-none";
}
return "";
}
public static string DefaultActiveTabContent(UserConfig userConfig, ImportMode tab)
{
var defaultTab = userConfig.DefaultTab;
if (tab == defaultTab)
{
return "show active";
}
return "";
}
public static string DefaultTabSelected(UserConfig userConfig, ImportMode tab)
{
var defaultTab = userConfig.DefaultTab;
var visibleTabs = userConfig.VisibleTabs;
if (!visibleTabs.Contains(tab))
{
return "disabled";
}
else if (tab == defaultTab)
{
return "selected";
}
return "";
}
}
}

View File

@@ -1,3 +1,8 @@
LubeLogger by Hargata Softworks is licensed under the MIT License for individual
and personal use. Commercial users and/or corporate entities are required
to maintain an active subscription in order to continue using LubeLogger.
For pricing information please contact us at hargatasoftworks@gmail.com
MIT License
Copyright (c) 2023 Hargata Softworks

354
Logic/LoginLogic.cs Normal file
View File

@@ -0,0 +1,354 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Net;
using System.Net.Mail;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace CarCareTracker.Logic
{
public interface ILoginLogic
{
bool MakeUserAdmin(int userId, bool isAdmin);
OperationResponse GenerateUserToken(string emailAddress, bool autoNotify);
bool DeleteUserToken(int tokenId);
bool DeleteUser(int userId);
OperationResponse RegisterNewUser(LoginModel credentials);
OperationResponse RequestResetPassword(LoginModel credentials);
OperationResponse ResetPasswordByUser(LoginModel credentials);
OperationResponse ResetUserPassword(LoginModel credentials);
UserData ValidateUserCredentials(LoginModel credentials);
bool CheckIfUserIsValid(int userId);
bool CreateRootUserCredentials(LoginModel credentials);
bool DeleteRootUserCredentials();
List<UserData> GetAllUsers();
List<Token> GetAllTokens();
}
public class LoginLogic : ILoginLogic
{
private readonly IUserRecordDataAccess _userData;
private readonly ITokenRecordDataAccess _tokenData;
private readonly IMailHelper _mailHelper;
private IMemoryCache _cache;
public LoginLogic(IUserRecordDataAccess userData,
ITokenRecordDataAccess tokenData,
IMailHelper mailHelper,
IMemoryCache memoryCache)
{
_userData = userData;
_tokenData = tokenData;
_mailHelper = mailHelper;
_cache = memoryCache;
}
public bool CheckIfUserIsValid(int userId)
{
if (userId == -1)
{
return true;
}
var result = _userData.GetUserRecordById(userId);
if (result == null)
{
return false;
} else
{
return result.Id != 0;
}
}
//handles user registration
public OperationResponse RegisterNewUser(LoginModel credentials)
{
//validate their token.
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress)
{
return new OperationResponse { Success = false, Message = "Invalid Token" };
}
//token is valid, check if username and password is acceptable and that username is unique.
if (string.IsNullOrWhiteSpace(credentials.EmailAddress) || string.IsNullOrWhiteSpace(credentials.UserName) || string.IsNullOrWhiteSpace(credentials.Password))
{
return new OperationResponse { Success = false, Message = "Neither username nor password can be blank" };
}
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUser.Id != default)
{
return new OperationResponse { Success = false, Message = "Username already taken" };
}
var existingUserWithEmail = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUserWithEmail.Id != default)
{
return new OperationResponse { Success = false, Message = "A user with that email already exists" };
}
//username is unique then we delete the token and create the user.
_tokenData.DeleteToken(existingToken.Id);
var newUser = new UserData()
{
UserName = credentials.UserName,
Password = GetHash(credentials.Password),
EmailAddress = credentials.EmailAddress
};
var result = _userData.SaveUserRecord(newUser);
if (result)
{
return new OperationResponse { Success = true, Message = "You will be redirected to the login page briefly." };
}
else
{
return new OperationResponse { Success = false, Message = "Something went wrong, please try again later." };
}
}
/// <summary>
/// Generates a token and notifies user via email so they can reset their password.
/// </summary>
/// <param name="credentials"></param>
/// <returns></returns>
public OperationResponse RequestResetPassword(LoginModel credentials)
{
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUser.Id != default)
{
//user exists, generate a token and send email.
//check to see if there is an existing token sent to the user.
var existingToken = _tokenData.GetTokenRecordByEmailAddress(existingUser.EmailAddress);
if (existingToken.Id == default)
{
var token = new Token()
{
Body = NewToken(),
EmailAddress = existingUser.EmailAddress
};
var result = _tokenData.CreateNewToken(token);
if (result)
{
result = _mailHelper.NotifyUserForPasswordReset(existingUser.EmailAddress, token.Body).Success;
}
}
}
//for security purposes we want to always return true for this method.
//otherwise someone can spam the reset password method to sniff out users.
return new OperationResponse { Success = true, Message = "If your user exists in the system you should receive an email shortly with instructions on how to proceed." };
}
public OperationResponse ResetPasswordByUser(LoginModel credentials)
{
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress)
{
return new OperationResponse { Success = false, Message = "Invalid Token" };
}
if (string.IsNullOrWhiteSpace(credentials.Password))
{
return new OperationResponse { Success = false, Message = "New Password cannot be blank" };
}
//if token is valid.
var existingUser = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUser.Id == default)
{
return new OperationResponse { Success = false, Message = "Unable to locate user" };
}
existingUser.Password = GetHash(credentials.Password);
var result = _userData.SaveUserRecord(existingUser);
//delete token
_tokenData.DeleteToken(existingToken.Id);
if (result)
{
return new OperationResponse { Success = true, Message = "Password resetted, you will be redirected to login page shortly." };
} else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
/// <summary>
/// Returns an empty user if can't auth against neither root nor db user.
/// </summary>
/// <param name="credentials">credentials from login page</param>
/// <returns></returns>
public UserData ValidateUserCredentials(LoginModel credentials)
{
if (UserIsRoot(credentials))
{
return new UserData()
{
Id = -1,
UserName = credentials.UserName,
IsAdmin = true,
IsRootUser = true
};
}
else
{
//authenticate via DB.
var result = _userData.GetUserRecordByUserName(credentials.UserName);
if (GetHash(credentials.Password) == result.Password)
{
result.Password = string.Empty;
return result;
}
else
{
return new UserData();
}
}
}
#region "Admin Functions"
public bool MakeUserAdmin(int userId, bool isAdmin)
{
var user = _userData.GetUserRecordById(userId);
if (user == default)
{
return false;
}
user.IsAdmin = isAdmin;
var result = _userData.SaveUserRecord(user);
return result;
}
public List<UserData> GetAllUsers()
{
var result = _userData.GetUsers();
return result;
}
public List<Token> GetAllTokens()
{
var result = _tokenData.GetTokens();
return result;
}
public OperationResponse GenerateUserToken(string emailAddress, bool autoNotify)
{
//check if email address already has a token attached to it.
var existingToken = _tokenData.GetTokenRecordByEmailAddress(emailAddress);
if (existingToken.Id != default)
{
return new OperationResponse { Success = false, Message = "There is an existing token tied to this email address" };
}
var token = new Token()
{
Body = NewToken(),
EmailAddress = emailAddress
};
var result = _tokenData.CreateNewToken(token);
if (result && autoNotify)
{
result = _mailHelper.NotifyUserForRegistration(emailAddress, token.Body).Success;
if (!result)
{
return new OperationResponse { Success = false, Message = "Token Generated, but Email failed to send, please check your SMTP settings." };
}
}
if (result)
{
return new OperationResponse { Success = true, Message = "Token Generated!" };
}
else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
public bool DeleteUserToken(int tokenId)
{
var result = _tokenData.DeleteToken(tokenId);
return result;
}
public bool DeleteUser(int userId)
{
var result = _userData.DeleteUserRecord(userId);
return result;
}
public OperationResponse ResetUserPassword(LoginModel credentials)
{
//user might have forgotten their password.
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUser.Id == default)
{
return new OperationResponse { Success = false, Message = "Unable to find user" };
}
var newPassword = Guid.NewGuid().ToString().Substring(0, 8);
existingUser.Password = GetHash(newPassword);
var result = _userData.SaveUserRecord(existingUser);
if (result)
{
return new OperationResponse { Success = true, Message = newPassword };
}
else
{
return new OperationResponse { Success = false, Message = "Something went wrong, please try again later." };
}
}
#endregion
#region "Root User"
public bool CreateRootUserCredentials(LoginModel credentials)
{
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//create hashes of the login credentials.
var hashedUserName = GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password);
//copy over settings that are off limits on the settings page.
existingUserConfig.EnableAuth = true;
existingUserConfig.UserNameHash = hashedUserName;
existingUserConfig.UserPasswordHash = hashedPassword;
}
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
_cache.Remove("userConfig_-1");
return true;
}
public bool DeleteRootUserCredentials()
{
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//copy over settings that are off limits on the settings page.
existingUserConfig.EnableAuth = false;
existingUserConfig.UserNameHash = string.Empty;
existingUserConfig.UserPasswordHash = string.Empty;
}
//clear out the cached config for the root user.
_cache.Remove("userConfig_-1");
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
return true;
}
private bool UserIsRoot(LoginModel credentials)
{
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//create hashes of the login credentials.
var hashedUserName = GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password);
//compare against stored hash.
if (hashedUserName == existingUserConfig.UserNameHash &&
hashedPassword == existingUserConfig.UserPasswordHash)
{
return true;
}
}
return false;
}
#endregion
private static string GetHash(string value)
{
StringBuilder Sb = new StringBuilder();
using (var hash = SHA256.Create())
{
Encoding enc = Encoding.UTF8;
byte[] result = hash.ComputeHash(enc.GetBytes(value));
foreach (byte b in result)
Sb.Append(b.ToString("x2"));
}
return Sb.ToString();
}
private string NewToken()
{
return Guid.NewGuid().ToString().Substring(0, 8);
}
}
}

118
Logic/UserLogic.cs Normal file
View File

@@ -0,0 +1,118 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace CarCareTracker.Logic
{
public interface IUserLogic
{
List<UserCollaborator> GetCollaboratorsForVehicle(int vehicleId);
bool AddUserAccessToVehicle(int userId, int vehicleId);
bool DeleteCollaboratorFromVehicle(int userId, int vehicleId);
OperationResponse AddCollaboratorToVehicle(int vehicleId, string username);
List<Vehicle> FilterUserVehicles(List<Vehicle> results, int userId);
bool UserCanEditVehicle(int userId, int vehicleId);
bool DeleteAllAccessToVehicle(int vehicleId);
bool DeleteAllAccessToUser(int userId);
}
public class UserLogic: IUserLogic
{
private readonly IUserAccessDataAccess _userAccess;
private readonly IUserRecordDataAccess _userData;
public UserLogic(IUserAccessDataAccess userAccess,
IUserRecordDataAccess userData) {
_userAccess = userAccess;
_userData = userData;
}
public List<UserCollaborator> GetCollaboratorsForVehicle(int vehicleId)
{
var result = _userAccess.GetUserAccessByVehicleId(vehicleId);
var convertedResult = new List<UserCollaborator>();
//convert useraccess to usercollaborator
foreach(UserAccess userAccess in result)
{
var userCollaborator = new UserCollaborator
{
UserName = _userData.GetUserRecordById(userAccess.Id.UserId).UserName,
UserVehicle = userAccess.Id
};
convertedResult.Add(userCollaborator);
}
return convertedResult;
}
public OperationResponse AddCollaboratorToVehicle(int vehicleId, string username)
{
//try to find existing user.
var existingUser = _userData.GetUserRecordByUserName(username);
if (existingUser.Id != default)
{
//user exists.
var result = AddUserAccessToVehicle(existingUser.Id, vehicleId);
if (result)
{
return new OperationResponse { Success = true, Message = "Collaborator Added" };
}
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
return new OperationResponse { Success = false, Message = $"Unable to find user {username} in the system" };
}
public bool DeleteCollaboratorFromVehicle(int userId, int vehicleId)
{
var result = _userAccess.DeleteUserAccess(userId, vehicleId);
return result;
}
public bool AddUserAccessToVehicle(int userId, int vehicleId)
{
if (userId == -1)
{
return true;
}
var userVehicle = new UserVehicle { UserId = userId, VehicleId = vehicleId };
var userAccess = new UserAccess { Id = userVehicle };
var result = _userAccess.SaveUserAccess(userAccess);
return result;
}
public List<Vehicle> FilterUserVehicles(List<Vehicle> results, int userId)
{
//user is root user.
if (userId == -1)
{
return results;
}
var accessibleVehicles = _userAccess.GetUserAccessByUserId(userId);
if (accessibleVehicles.Any())
{
var vehicleIds = accessibleVehicles.Select(x => x.Id.VehicleId);
return results.Where(x => vehicleIds.Contains(x.Id)).ToList();
}
else
{
return new List<Vehicle>();
}
}
public bool UserCanEditVehicle(int userId, int vehicleId)
{
if (userId == -1)
{
return true;
}
var userAccess = _userAccess.GetUserAccessByVehicleAndUserId(userId, vehicleId);
if (userAccess != null)
{
return true;
}
return false;
}
public bool DeleteAllAccessToVehicle(int vehicleId)
{
var result = _userAccess.DeleteAllAccessRecordsByVehicleId(vehicleId);
return result;
}
public bool DeleteAllAccessToUser(int userId)
{
var result = _userAccess.DeleteAllAccessRecordsByUserId(userId);
return result;
}
}
}

View File

@@ -3,19 +3,28 @@ using CsvHelper.Configuration;
namespace CarCareTracker.MapProfile
{
public class FuellyMapper: ClassMap<ImportModel>
public class ImportMapper: ClassMap<ImportModel>
{
public FuellyMapper()
public ImportMapper()
{
Map(m => m.Date).Name(["date", "fuelup_date"]);
Map(m => m.DateCreated).Name(["datecreated"]);
Map(m => m.DateModified).Name(["datemodified"]);
Map(m => m.Odometer).Name(["odometer"]);
Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "fuelconsumed"]);
Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "quantity", "fuelconsumed"]);
Map(m => m.Cost).Name(["cost", "total cost", "totalcost", "total price"]);
Map(m => m.Notes).Name("notes", "note");
Map(m => m.Price).Name(["price"]);
Map(m => m.PartialFuelUp).Name(["partial_fuelup"]);
Map(m => m.IsFillToFull).Name(["isfilltofull", "filled up"]);
Map(m => m.Description).Name(["description"]);
Map(m => m.MissedFuelUp).Name(["missed_fuelup", "missedfuelup"]);
Map(m => m.PartSupplier).Name(["partsupplier"]);
Map(m => m.PartQuantity).Name(["partquantity"]);
Map(m => m.PartNumber).Name(["partnumber"]);
Map(m => m.Progress).Name(["progress"]);
Map(m => m.Type).Name(["type"]);
Map(m => m.Priority).Name(["priority"]);
}
}
}

View File

@@ -1,4 +1,5 @@
using CarCareTracker.Models;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.DataProtection;
@@ -14,17 +15,20 @@ namespace CarCareTracker.Middleware
{
private IHttpContextAccessor _httpContext;
private IDataProtector _dataProtector;
private ILoginLogic _loginLogic;
private bool enableAuth;
public Authen(
IOptionsMonitor<AuthenticationSchemeOptions> options,
UrlEncoder encoder,
ILoggerFactory logger,
IConfiguration configuration,
ILoginLogic loginLogic,
IDataProtectionProvider securityProvider,
IHttpContextAccessor httpContext) : base(options, logger, encoder)
{
_httpContext = httpContext;
_dataProtector = securityProvider.CreateProtector("login");
_loginLogic = loginLogic;
enableAuth = bool.Parse(configuration["EnableAuth"]);
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
@@ -35,7 +39,9 @@ namespace CarCareTracker.Middleware
var appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim>
{
new(ClaimTypes.Name, "admin")
new(ClaimTypes.Name, "admin"),
new(ClaimTypes.NameIdentifier, "-1"),
new(ClaimTypes.Role, nameof(UserData.IsRootUser))
};
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
@@ -45,47 +51,127 @@ namespace CarCareTracker.Middleware
{
//auth is enabled by user, we will have to authenticate the user via a ticket retrieved from the auth cookie.
var access_token = _httpContext.HttpContext.Request.Cookies["ACCESS_TOKEN"];
if (string.IsNullOrWhiteSpace(access_token))
//auth using Basic Auth for API.
var request_header = _httpContext.HttpContext.Request.Headers["Authorization"];
if (string.IsNullOrWhiteSpace(access_token) && string.IsNullOrWhiteSpace(request_header))
{
return AuthenticateResult.Fail("Cookie is invalid or does not exist.");
}
else
else if (!string.IsNullOrWhiteSpace(request_header))
{
//decrypt the access token.
var decryptedCookie = _dataProtector.Unprotect(access_token);
AuthCookie authCookie = JsonSerializer.Deserialize<AuthCookie>(decryptedCookie);
if (authCookie != null)
var cleanedHeader = request_header.ToString().Replace("Basic ", "").Trim();
byte[] data = Convert.FromBase64String(cleanedHeader);
string decodedString = System.Text.Encoding.UTF8.GetString(data);
var splitString = decodedString.Split(":");
if (splitString.Count() != 2)
{
//validate auth cookie
if (authCookie.ExpiresOn < DateTime.Now)
{
//if cookie is expired
return AuthenticateResult.Fail("Expired credentials");
}
else if (authCookie.Id == default || string.IsNullOrWhiteSpace(authCookie.UserName))
{
return AuthenticateResult.Fail("Corrupted credentials");
}
else
return AuthenticateResult.Fail("Invalid credentials");
}
else
{
var userData = _loginLogic.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] });
if (userData.Id != default)
{
var appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim>
{
new(ClaimTypes.Name, authCookie.UserName)
new(ClaimTypes.Name, splitString[0]),
new(ClaimTypes.NameIdentifier, userData.Id.ToString())
};
if (userData.IsAdmin)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin)));
}
if (userData.IsRootUser)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser)));
}
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
else if (!string.IsNullOrWhiteSpace(access_token))
{
try
{
//decrypt the access token.
var decryptedCookie = _dataProtector.Unprotect(access_token);
AuthCookie authCookie = JsonSerializer.Deserialize<AuthCookie>(decryptedCookie);
if (authCookie != null)
{
//validate auth cookie
if (authCookie.ExpiresOn < DateTime.Now)
{
//if cookie is expired
return AuthenticateResult.Fail("Expired credentials");
}
else if (authCookie.UserData is null || authCookie.UserData.Id == default || string.IsNullOrWhiteSpace(authCookie.UserData.UserName))
{
return AuthenticateResult.Fail("Corrupted credentials");
}
else
{
if (!_loginLogic.CheckIfUserIsValid(authCookie.UserData.Id))
{
return AuthenticateResult.Fail("Cookie points to non-existant user.");
}
//validate if user is still valid
var appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim>
{
new(ClaimTypes.Name, authCookie.UserData.UserName),
new(ClaimTypes.NameIdentifier, authCookie.UserData.Id.ToString()),
new(ClaimTypes.Role, "CookieAuth")
};
if (authCookie.UserData.IsAdmin)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin)));
}
if (authCookie.UserData.IsRootUser)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser)));
}
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
catch (Exception ex)
{
return AuthenticateResult.Fail("Corrupted credentials");
}
}
return AuthenticateResult.Fail("Invalid credentials");
}
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
if (Request.RouteValues.TryGetValue("controller", out object value))
{
if (value.ToString().ToLower() == "api")
{
Response.StatusCode = 401;
return Task.CompletedTask;
}
}
Response.Redirect("/Login/Index");
return Task.CompletedTask;
}
protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
{
if (Request.RouteValues.TryGetValue("controller", out object value))
{
if (value.ToString().ToLower() == "api")
{
Response.StatusCode = 403;
return Task.CompletedTask;
}
}
Response.Redirect("/Error/401");
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class AdminViewModel
{
public List<UserData> Users { get; set; }
public List<Token> Tokens { get; set; }
}
}

View File

@@ -4,12 +4,13 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Date { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public CollisionRecord ToCollisionRecord() { return new CollisionRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; }
}
}

View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class MailConfig
{
public string EmailServer { get; set; }
public string EmailFrom { get; set; }
public bool UseSSL { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@@ -15,6 +15,8 @@
public decimal Gallons { get; set; }
public decimal Cost { get; set; }
public bool IsFillToFull { get; set; } = true;
public bool MissedFuelUp { get; set; } = false;
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -4,7 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Date { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
/// <summary>
/// American moment
/// </summary>
@@ -15,6 +15,8 @@
public decimal Gallons { get; set; }
public decimal Cost { get; set; }
public bool IsFillToFull { get; set; } = true;
public bool MissedFuelUp { get; set; } = false;
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public GasRecord ToGasRecord() { return new GasRecord {
Id = Id,
@@ -24,7 +26,9 @@
Mileage = Mileage,
VehicleId = VehicleId,
Files = Files,
IsFillToFull = IsFillToFull
IsFillToFull = IsFillToFull,
MissedFuelUp = MissedFuelUp,
Notes = Notes
}; }
}
}

View File

@@ -4,6 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int MonthId { get; set; }
public string Date { get; set; }
/// <summary>
/// American moment
@@ -17,5 +18,8 @@
public int DeltaMileage { get; set; }
public decimal MilesPerGallon { get; set; }
public decimal CostPerGallon { get; set; }
public bool IsFillToFull { get; set; }
public bool MissedFuelUp { get; set; }
public string Notes { get; set; }
}
}

View File

@@ -1,11 +1,16 @@
namespace CarCareTracker.Models
{
/// <summary>
/// Import model used for importing Gas records.
/// Import model used for importing records via CSV.
/// </summary>
public class ImportModel
{
public string Date { get; set; }
public string DateCreated { get; set; }
public string DateModified { get; set; }
public string Type { get; set; }
public string Priority { get; set; }
public string Progress { get; set; }
public string Odometer { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
@@ -14,5 +19,72 @@
public string Price { get; set; }
public string PartialFuelUp { get; set; }
public string IsFillToFull { get; set; }
public string MissedFuelUp { get; set; }
public string PartNumber { get; set; }
public string PartSupplier { get; set; }
public string PartQuantity { get; set; }
}
public class SupplyRecordExportModel
{
public string Date { get; set; }
public string PartNumber { get; set; }
public string PartSupplier { get; set; }
public string PartQuantity { get; set; }
public string Description { get; set; }
public string Cost { get; set; }
public string Notes { get; set; }
}
public class ServiceRecordExportModel
{
public string Date { get; set; }
public string Odometer { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public string Cost { get; set; }
}
public class OdometerRecordExportModel
{
public string Date { get; set; }
public string Odometer { get; set; }
public string Notes { get; set; }
}
public class TaxRecordExportModel
{
public string Date { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public string Cost { get; set; }
}
public class GasRecordExportModel
{
public string Date { get; set; }
public string Odometer { get; set; }
public string FuelConsumed { get; set; }
public string Cost { get; set; }
public string FuelEconomy { get; set; }
public string IsFillToFull { get; set; }
public string MissedFuelUp { get; set; }
public string Notes { get; set; }
}
public class ReminderExportModel
{
public string Description { get; set; }
public string Urgency { get; set; }
public string Metric { get; set; }
public string Notes { get; set; }
}
public class PlanRecordExportModel
{
public string DateCreated { get; set; }
public string DateModified { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public string Type { get; set; }
public string Priority { get; set; }
public string Progress { get; set; }
public string Cost { get; set; }
}
}

View File

@@ -2,8 +2,7 @@
{
public class AuthCookie
{
public int Id { get; set; }
public string UserName { get; set; }
public UserData UserData { get; set; }
public DateTime ExpiresOn { get; set; }
}
}

View File

@@ -4,6 +4,8 @@
{
public string UserName { get; set; }
public string Password { get; set; }
public string EmailAddress { get; set; }
public string Token { get; set; }
public bool IsPersistent { get; set; } = false;
}
}

9
Models/Login/Token.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public class Token
{
public int Id { get; set; }
public string Body { get; set; }
public string EmailAddress { get; set; }
}
}

View File

@@ -4,6 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Description { get; set; }
public string NoteText { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class OdometerRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -0,0 +1,13 @@
namespace CarCareTracker.Models
{
public class OdometerRecordInput
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public OdometerRecord ToOdometerRecord() { return new OdometerRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Mileage = Mileage, Notes = Notes, Files = Files }; }
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class OperationResponse
{
public bool Success { get; set; }
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class PlanCostItem
{
public string CostName { get; set; }
public decimal CostAmount { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
namespace CarCareTracker.Models
{
public class PlanRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateModified { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public ImportMode ImportMode { get; set; }
public PlanPriority Priority { get; set; }
public PlanProgress Progress { get; set; }
public decimal Cost { get; set; }
}
}

View File

@@ -0,0 +1,31 @@
namespace CarCareTracker.Models
{
public class PlanRecordInput
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string DateCreated { get; set; } = DateTime.Now.ToShortDateString();
public string DateModified { get; set; } = DateTime.Now.ToShortDateString();
public string Description { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public ImportMode ImportMode { get; set; }
public PlanPriority Priority { get; set; }
public PlanProgress Progress { get; set; }
public decimal Cost { get; set; }
public PlanRecord ToPlanRecord() { return new PlanRecord {
Id = Id,
VehicleId = VehicleId,
DateCreated = DateTime.Parse(DateCreated),
DateModified = DateTime.Parse(DateModified),
Description = Description,
Notes = Notes,
Files = Files,
ImportMode = ImportMode,
Cost = Cost,
Priority = Priority,
Progress = Progress
}; }
}
}

View File

@@ -8,6 +8,9 @@
public int Mileage { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
}
}

View File

@@ -8,6 +8,9 @@
public int Mileage { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
public ReminderRecord ToReminderRecord() { return new ReminderRecord {
Id = Id,
@@ -16,6 +19,9 @@
Mileage = Mileage,
Description = Description,
Metric = Metric,
IsRecurring = IsRecurring,
ReminderMileageInterval = ReminderMileageInterval,
ReminderMonthInterval = ReminderMonthInterval,
Notes = Notes }; }
}
}

View File

@@ -13,5 +13,9 @@
/// </summary>
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
public ReminderUrgency Urgency { get; set; } = ReminderUrgency.NotUrgent;
/// <summary>
/// Recurring Reminders
/// </summary>
public bool IsRecurring { get; set; } = false;
}
}

View File

@@ -1,7 +1,8 @@
namespace CarCareTracker.Models
{
public class GasCostForVehicleByMonth
public class CostForVehicleByMonth
{
public int MonthId { get; set; }
public string MonthName { get; set; }
public decimal Cost { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace CarCareTracker.Models
{
/// <summary>
/// Generic Model used for vehicle history report.
/// </summary>
public class GenericReportModel
{
public ImportMode DataType { get; set; }
public DateTime Date { get; set; }
public int Odometer { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public decimal Cost { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace CarCareTracker.Models
{
public class ReminderMakeUpForVehicle
{
public int NotUrgentCount { get; set; }
public int UrgentCount { get; set; }
public int VeryUrgentCount { get; set; }
public int PastDueCount { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class ReportViewModel
{
public List<CostForVehicleByMonth> CostForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
public List<CostForVehicleByMonth> FuelMileageForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle();
public ReminderMakeUpForVehicle ReminderMakeUpForVehicle { get; set; } = new ReminderMakeUpForVehicle();
public List<int> Years { get; set; } = new List<int>();
public List<UserCollaborator> Collaborators { get; set; } = new List<UserCollaborator>();
}
}

View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class VehicleHistoryViewModel
{
public Vehicle VehicleData { get; set; }
public List<GenericReportModel> VehicleHistory { get; set; }
public string Odometer { get; set; }
public string MPG { get; set; }
public decimal TotalCost { get; set; }
public decimal TotalGasCost { get; set; }
}
}

View File

@@ -4,12 +4,13 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Date { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public ServiceRecord ToServiceRecord() { return new ServiceRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; }
}
}

View File

@@ -0,0 +1,37 @@
namespace CarCareTracker.Models
{
public class SupplyRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
/// <summary>
/// When the part or supplies were purchased.
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// Part number can be alphanumeric.
/// </summary>
public string PartNumber { get; set; }
/// <summary>
/// Where the part/supplies were purchased from.
/// </summary>
public string PartSupplier { get; set; }
/// <summary>
/// Amount purchased, can be partial quantities such as fluids.
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Description of the part/supplies purchased.
/// </summary>
public string Description { get; set; }
/// <summary>
/// How much it costs
/// </summary>
public decimal Cost { get; set; }
/// <summary>
/// Additional notes.
/// </summary>
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -0,0 +1,27 @@
namespace CarCareTracker.Models
{
public class SupplyRecordInput
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public string PartNumber { get; set; }
public string PartSupplier { get; set; }
public decimal Quantity { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public SupplyRecord ToSupplyRecord() { return new SupplyRecord {
Id = Id,
VehicleId = VehicleId,
Date = DateTime.Parse(Date),
Cost = Cost,
PartNumber = PartNumber,
PartSupplier = PartSupplier,
Quantity = Quantity,
Description = Description,
Notes = Notes,
Files = Files }; }
}
}

View File

@@ -0,0 +1,7 @@
namespace CarCareTracker.Models
{
public class SupplyUsage {
public int SupplyId { get; set; }
public decimal Quantity { get; set; }
}
}

View File

@@ -4,7 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Date { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }

View File

@@ -4,12 +4,13 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Date { get; set; }
public string Date { get; set; } = DateTime.Now.ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public List<SupplyUsage> Supplies { get; set; } = new List<SupplyUsage>();
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; }
}
}

12
Models/User/UserAccess.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class UserVehicle
{
public int UserId { get; set; }
public int VehicleId { get; set; }
}
public class UserAccess
{
public UserVehicle Id { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class UserCollaborator
{
public string UserName { get; set; }
public UserVehicle UserVehicle { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace CarCareTracker.Models
{
public class UserConfigData
{
/// <summary>
/// User ID
/// </summary>
public int Id { get; set; }
public UserConfig UserConfig { get; set; }
}
}

12
Models/User/UserData.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class UserData
{
public int Id { get; set; }
public string UserName { get; set; }
public string EmailAddress { get; set; }
public string Password { get; set; }
public bool IsAdmin { get; set; }
public bool IsRootUser { get; set; } = false;
}
}

View File

@@ -9,7 +9,20 @@
public bool EnableAuth { get; set; }
public bool HideZero { get; set; }
public bool UseUKMPG {get;set;}
public bool UseThreeDecimalGasCost { get; set; }
public bool EnableAutoReminderRefresh { get; set; }
public bool EnableAutoOdometerInsert { get; set; }
public string UserNameHash { get; set; }
public string UserPasswordHash { get; set;}
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
ImportMode.Dashboard,
ImportMode.ServiceRecord,
ImportMode.RepairRecord,
ImportMode.GasRecord,
ImportMode.UpgradeRecord,
ImportMode.TaxRecord,
ImportMode.ReminderRecord,
ImportMode.NoteRecord};
public ImportMode DefaultTab { get; set; } = ImportMode.Dashboard;
}
}

View File

@@ -1,6 +1,7 @@
using CarCareTracker.External.Implementations;
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Middleware;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@@ -17,7 +18,25 @@ builder.Services.AddSingleton<ICollisionRecordDataAccess, CollisionRecordDataAcc
builder.Services.AddSingleton<ITaxRecordDataAccess, TaxRecordDataAccess>();
builder.Services.AddSingleton<IReminderRecordDataAccess, ReminderRecordDataAccess>();
builder.Services.AddSingleton<IUpgradeRecordDataAccess, UpgradeRecordDataAccess>();
builder.Services.AddSingleton<IUserRecordDataAccess, UserRecordDataAccess>();
builder.Services.AddSingleton<ITokenRecordDataAccess, TokenRecordDataAccess>();
builder.Services.AddSingleton<IUserAccessDataAccess, UserAccessDataAccess>();
builder.Services.AddSingleton<IUserConfigDataAccess, UserConfigDataAccess>();
builder.Services.AddSingleton<ISupplyRecordDataAccess, SupplyRecordDataAccess>();
builder.Services.AddSingleton<IPlanRecordDataAccess, PlanRecordDataAccess>();
builder.Services.AddSingleton<IOdometerRecordDataAccess, OdometerRecordDataAccess>();
//configure helpers
builder.Services.AddSingleton<IFileHelper, FileHelper>();
builder.Services.AddSingleton<IGasHelper, GasHelper>();
builder.Services.AddSingleton<IReminderHelper, ReminderHelper>();
builder.Services.AddSingleton<IReportHelper, ReportHelper>();
builder.Services.AddSingleton<IMailHelper, MailHelper>();
builder.Services.AddSingleton<IConfigHelper, ConfigHelper>();
//configure logic
builder.Services.AddSingleton<ILoginLogic, LoginLogic>();
builder.Services.AddSingleton<IUserLogic, UserLogic>();
if (!Directory.Exists("data"))
{

View File

@@ -2,9 +2,14 @@
A self-hosted, open-source vehicle service records and maintainence tracker.
Support this project on Patreon: https://patreon.com/LubeLogger
## Why
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintainence.
## Screenshots
<a href="/docs/screenshots.md">Screenshots</a>
## Dependencies
- Bootstrap
- LiteDB
@@ -16,14 +21,14 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
## Docker Setup (GHCR)
1. Install Docker
2. Run `docker pull ghcr.io/hargata/lubelogger:latest`
3. CHECK culture in .env file, default is en_US, this will change the currency and date formats.
3. CHECK culture in .env file, default is en_US, this will change the currency and date formats. You can also setup SMTP Config here.
4. If not using traefik, use docker-compose-notraefik.yml
5. Run `docker-compose up`
## Docker Setup (Manual Build)
1. Install Docker
2. Clone this repo
3. CHECK culture in .env file, default is en_US
3. CHECK culture in .env file, default is en_US, also setup SMTP for user management if you want that.
4. Run `docker build -t lubelogger -f Dockerfile .`
5. CHECK docker-compose.yml and make sure the mounting directories look correct.
6. If not using traefik, use docker-compose-notraefik.yml

292
Views/API/Index.cshtml Normal file
View File

@@ -0,0 +1,292 @@
@{
ViewData["Title"] = "LubeLogger API";
}
<div class="row">
<div class="d-flex justify-content-center">
<h6 class="display-6 mt-2">API</h6>
</div>
</div>
<div class="row">
<div class="d-flex justify-content-center">
<p class="lead">If authentication is enabled, use the credentials of the user for Basic Auth(RFC2617)</p>
</div>
</div>
<hr />
<div class="row">
<div class="col-1">
<h6>Method</h6>
</div>
<div class="col-5">
<h6>Endpoint</h6>
</div>
<div class="col-3">
<h6>Description</h6>
</div>
<div class="col-3">
<h6>Parameters</h6>
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicles</code>
</div>
<div class="col-3">
Returns a list of vehicles
</div>
<div class="col-3">
No Params
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicle/servicerecords</code>
</div>
<div class="col-3">
Returns a list of service records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
POST
</div>
<div class="col-5">
<code>/api/vehicle/servicerecords/add</code>
</div>
<div class="col-3">
Adds Service Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
odometer - Odometer reading<br />
description - Description<br/>
cost - Cost<br />
notes - notes(optional)<br />
}
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicle/repairrecords</code>
</div>
<div class="col-3">
Returns a list of repair records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
POST
</div>
<div class="col-5">
<code>/api/vehicle/repairrecords/add</code>
</div>
<div class="col-3">
Adds Repair Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
odometer - Odometer reading<br />
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
}
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicle/upgraderecords</code>
</div>
<div class="col-3">
Returns a list of upgrade records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
POST
</div>
<div class="col-5">
<code>/api/vehicle/upgraderecords/add</code>
</div>
<div class="col-3">
Adds Upgrade Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
odometer - Odometer reading<br />
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
}
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicle/taxrecords</code>
</div>
<div class="col-3">
Returns a list of tax records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
POST
</div>
<div class="col-5">
<code>/api/vehicle/taxrecords/add</code>
</div>
<div class="col-3">
Adds Tax Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
}
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicle/gasrecords</code>
</div>
<div class="col-3">
Returns a list of gas records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
useMPG(bool) - Use Imperial Units and Calculation
<br />
useUKMPG(bool) - Use UK Imperial Calculation
</div>
</div>
<div class="row">
<div class="col-1">
POST
</div>
<div class="col-5">
<code>/api/vehicle/gasrecords/add</code>
</div>
<div class="col-3">
Adds Gas Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
odometer - Odometer reading<br />
fuelConsumed - Fuel Consumed<br />
cost - Cost<br />
isFillToFull(bool) - Filled To Full<br />
missedFuelUp(bool) - Missed Fuel Up<br />
notes - notes(optional)<br />
}
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicle/reminders</code>
</div>
<div class="col-3">
Returns a list of reminders for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/makebackup</code>
</div>
<div class="col-3">
Creates a snapshot/backup of the DB at the time and returns a URL to download it.
</div>
<div class="col-3">
No Params(must be root user)
</div>
</div>
}
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5">
<code>/api/vehicle/odometerrecords</code>
</div>
<div class="col-3">
Returns a list of odometer records for the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
</div>
</div>
<div class="row">
<div class="col-1">
POST
</div>
<div class="col-5">
<code>/api/vehicle/odometerrecords/add</code>
</div>
<div class="col-3">
Adds Odometer Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
date - Date to be entered<br />
odometer - Odometer reading<br />
notes - notes(optional)<br />
}
</div>
</div>

153
Views/Admin/Index.cshtml Normal file
View File

@@ -0,0 +1,153 @@
@{
ViewData["Title"] = "Admin";
}
@inject IConfiguration config;
@{
bool emailServerIsSetup = true;
var mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
if (mailConfig is null || string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
emailServerIsSetup = false;
}
}
@model AdminViewModel
<div class="container">
<div class="row">
<div class="col-1">
<a href="/Home" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></a>
</div>
<div class="col-11">
<span class="display-6">Admin Panel</span>
</div>
</div>
<hr />
<div class="row">
<div class="col-md-5 col-12">
<span class="lead">Tokens</span>
<hr />
<div class="row">
<div class="col-6">
<button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Generate User Token</button>
</div>
<div class="col-6 d-flex align-items-center">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")>
<label class="form-check-label" for="enableAutoNotify">Auto Notify(via Email)</label>
</div>
</div>
</div>
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-4">Token</th>
<th scope="col" class="col-6">Issued To</th>
<th scope="col" class="col-2">Delete</th>
</tr>
</thead>
<tbody>
@foreach (Token token in Model.Tokens)
{
<tr class="d-flex">
<td class="col-4" style="cursor:pointer;" onclick="copyToClipboard(this)">@token.Body</td>
<td class="col-6 text-truncate">@token.EmailAddress</td>
<td class="col-2">
<button type="button" class="btn btn-danger" onclick="deleteToken(@token.Id, this)"><i class="bi bi-trash"></i></button>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="col-12 col-md-7">
<span class="lead">Users</span>
<hr />
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-4">Username</th>
<th scope="col" class="col-4">Email</th>
<th scope="col" class="col-2">Is Admin</th>
<th scope="col" class="col-2">Delete</th>
</tr>
</thead>
<tbody>
@foreach (UserData userData in Model.Users)
{
<tr class="d-flex" style="cursor:pointer;">
<td class="col-4">@userData.UserName</td>
<td class="col-4">@userData.EmailAddress</td>
<td class="col-2"><input class="form-check-input" type="checkbox" value="" onchange="updateUserAdmin(@userData.Id, this)" @(userData.IsAdmin ? "checked" : "")/></td>
<td class="col-2"><button type="button" class="btn btn-danger" onclick="deleteUser(@userData.Id, this)"><i class="bi bi-trash"></i></button></td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<script>
function updateUserAdmin(userId, sender){
var isChecked = $(sender).is(":checked");
$.post('/Admin/UpdateUserAdminStatus', { userId: userId, isAdmin: isChecked }, function (data) {
if (data){
reloadPage();
} else {
errorToast("An error has occurred, please try again later.");
}
});
}
function reloadPage() {
window.location.reload();
}
function deleteToken(tokenId) {
$.post(`/Admin/DeleteToken?tokenId=${tokenId}`, function (data) {
if (data) {
reloadPage();
} else {
errorToast("An error has occurred, please try again later.");
}
});
}
function deleteUser(userId) {
$.post(`/Admin/DeleteUser?userId=${userId}`, function (data) {
if (data) {
reloadPage();
} else {
errorToast("An error has occurred, please try again later.");
}
})
}
function copyToClipboard(e) {
var textToCopy = e.textContent;
navigator.clipboard.writeText(textToCopy);
successToast("Copied to Clipboard");
}
function generateNewToken() {
Swal.fire({
title: 'Generate Token',
html: `
<input type="text" id="inputEmail" class="swal2-input" placeholder="Email Address">
`,
confirmButtonText: 'Generate',
focusConfirm: false,
preConfirm: () => {
const emailAddress = $("#inputEmail").val();
if (!emailAddress) {
Swal.showValidationMessage(`Please enter an email address`)
}
return { emailAddress }
},
}).then(function (result) {
if (result.isConfirmed) {
var autoNotify = $("#enableAutoNotify").is(":checked");
$.get('/Admin/GenerateNewToken', { emailAddress: result.value.emailAddress, autoNotify: autoNotify }, function (data) {
if (data.success) {
reloadPage();
} else {
errorToast(data.message)
}
});
}
});
}
</script>

View File

@@ -1,6 +1,7 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@{
var enableAuth = bool.Parse(Configuration[nameof(UserConfig.EnableAuth)]);
var enableAuth = config.GetUserConfig(User).EnableAuth;
}
@model string
@{
@@ -9,24 +10,60 @@
@section Scripts {
<script src="~/js/garage.js" asp-append-version="true"></script>
}
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
<ul class="navbar-nav" id="homeTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link @(Model == "garage" ? "active" : "")" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-car-front me-2"></i>Garage</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>Settings</span></button>
</li>
@if (User.IsInRole("CookieAuth"))
{
@if (User.IsInRole(nameof(UserData.IsAdmin)))
{
<li class="nav-item" role="presentation">
<a class="dropdown-item" href="/Admin"><span class="display-3 ms-2"><i class="bi bi-people me-2"></i>Admin Panel</span></a>
</li>
}
<li class="nav-item" role="presentation">
<button class="nav-link" onclick="performLogOut()"><span class="display-3 ms-2"><i class="bi bi-box-arrow-right me-2"></i>Logout</span></button>
</li>
}
</ul>
</div>
<div class="container">
<div class="row mt-2">
<div class="d-flex justify-content-center">
<img src="/defaults/lubelogger_logo.png"/>
<div class="d-flex lubelogger-navbar">
<img src="/defaults/lubelogger_logo.png" />
<div class="lubelogger-navbar-button">
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
</div>
</div>
</div>
<hr />
<ul class="nav nav-tabs" id="homeTab" role="tablist">
<ul class="nav nav-tabs lubelogger-tab" id="homeTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link @(Model == "garage" ? "active" : "")" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front me-2"></i>Garage</button>
</li>
<li class="nav-item ms-auto" role="presentation">
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear me-2"></i>Settings</button>
</li>
@if (enableAuth)
@if (User.IsInRole("CookieAuth"))
{
<li class="nav-item">
<button class="nav-link" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
<li class="nav-item dropdown" role="presentation">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="bi bi-person me-2"></i>@User.Identity.Name</a>
<ul class="dropdown-menu">
@if (User.IsInRole(nameof(UserData.IsAdmin)))
{
<li>
<a class="dropdown-item" href="/Admin"><i class="bi bi-people me-2"></i>Admin Panel</a>
</li>
}
<li>
<button class="dropdown-item" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
</li>
</ul>
</li>
}
</ul>
@@ -38,7 +75,6 @@
</div>
</div>
<div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0">
</div>
</div>
</div>
@@ -46,11 +82,10 @@
<div class="modal fade" id="addVehicleModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content" id="addVehicleModalContent">
</div>
</div>
</div>
<script>
loadGarage();
loadSettings();
bindWindowResize();
</script>

View File

@@ -1,4 +1,5 @@
@model UserConfig
@using CarCareTracker.Helper
@model UserConfig
<div class="row">
<div class="d-flex justify-content-center">
@@ -22,8 +23,6 @@
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useUKMPG" checked="@Model.UseUKMPG">
<label class="form-check-label" for="useUKMPG">Use UK MPG Calculation<br /><small class="text-body-secondary">Input Gas Consumption in Liters, it will be converted to UK Gals for MPG Calculation</small></label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useDescending" checked="@Model.UseDescending">
<label class="form-check-label" for="useDescending">Sort lists in Descending Order(Newest to Oldest)</label>
@@ -33,9 +32,116 @@
<label class="form-check-label" for="hideZero">Replace @(0.ToString("C")) Costs with ---</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth">
<label class="form-check-label" for="enableAuth">Enable Authentication</label>
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useThreeDecimal" checked="@Model.UseThreeDecimalGasCost">
<label class="form-check-label" for="useThreeDecimal">Use Three Decimals For Fuel Cost</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableAutoReminderRefresh" checked="@Model.EnableAutoReminderRefresh">
<label class="form-check-label" for="enableAutoReminderRefresh">Auto Refresh Lapsed Recurring Reminders</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableAutoOdometerInsert" checked="@Model.EnableAutoOdometerInsert">
<label class="form-check-label" for="enableAutoOdometerInsert">Auto Insert Odometer Records<br /><small class="text-body-secondary">Only when Adding Service/Repair/Upgrade/Fuel Record or Completing a Plan</small></label>
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="form-check form-switch">
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth">
<label class="form-check-label" for="enableAuth">Enable Authentication</label>
</div>
}
</div>
<div class="col-12 col-md-6">
<div class="row" id="visibleTabs">
<div class="col-12">
<span class="lead">Visible Tabs</span>
</div>
<div class="col-12 col-md-6">
<ul class="list-group">
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="ServiceRecord" id="serviceRecordTab" @(Model.VisibleTabs.Contains(ImportMode.ServiceRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="serviceRecordTab">Service Records</label>
</li>
<li class="list-group-item d-none">
<input onChange="updateSettings()" disabled class="form-check-input me-1" type="checkbox" value="Dashboard" id="dashboardTab" @(Model.VisibleTabs.Contains(ImportMode.Dashboard) ? "checked" : "")>
<label class="form-check-label stretched-link" for="dashboardTab">Dashboard</label>
</li>
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="RepairRecord" id="repairRecordTab" @(Model.VisibleTabs.Contains(ImportMode.RepairRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="repairRecordTab">Repairs</label>
</li>
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="UpgradeRecord" id="upgradeRecordTab" @(Model.VisibleTabs.Contains(ImportMode.UpgradeRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="upgradeRecordTab">Upgrades</label>
</li>
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="GasRecord" id="gasRecordTab" @(Model.VisibleTabs.Contains(ImportMode.GasRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="gasRecordTab">Fuel</label>
</li>
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="OdometerRecord" id="odometerRecordTab" @(Model.VisibleTabs.Contains(ImportMode.OdometerRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="odometerRecordTab">Odometer</label>
</li>
</ul>
</div>
<div class="col-12 col-md-6">
<ul class="list-group">
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="TaxRecord" id="taxRecordTab" @(Model.VisibleTabs.Contains(ImportMode.TaxRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="taxRecordTab">Taxes</label>
</li>
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="NoteRecord" id="noteRecordTab" @(Model.VisibleTabs.Contains(ImportMode.NoteRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="noteRecordTab">Notes</label>
</li>
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="ReminderRecord" id="reminderRecordTab" @(Model.VisibleTabs.Contains(ImportMode.ReminderRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="reminderRecordTab">Reminder</label>
</li>
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="SupplyRecord" id="supplyRecordTab" @(Model.VisibleTabs.Contains(ImportMode.SupplyRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="supplyRecordTab">Supplies</label>
</li>
<li class="list-group-item">
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="PlanRecord" id="planRecordTab" @(Model.VisibleTabs.Contains(ImportMode.PlanRecord) ? "checked" : "")>
<label class="form-check-label stretched-link" for="planRecordTab">Planner</label>
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col-12 col-md-6">
<span class="lead">Default Tab</span>
<select class="form-select" onchange="updateSettings()" id="defaultTab">
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.Dashboard)) value="Dashboard">Dashboard</!option>
<!option @(StaticHelper.DefaultTabSelected(Model,ImportMode.ServiceRecord)) value="ServiceRecord">Service Record</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.RepairRecord)) value="RepairRecord">Repairs</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.UpgradeRecord)) value="UpgradeRecord">Upgrades</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.GasRecord)) value="GasRecord">Fuel</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.TaxRecord)) value="TaxRecord">Tax</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.NoteRecord)) value="NoteRecord">Notes</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.ReminderRecord)) value="ReminderRecord">Reminders</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.SupplyRecord)) value="SupplyRecord">Supplies</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.PlanRecord)) value="PlanRecord">Planner</!option>
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.OdometerRecord)) value="OdometerRecord">Odometer</!option>
</select>
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="col-12 col-md-6">
<span class="lead">Backups</span>
<div class="row">
<div class="col-6 d-grid">
<button onclick="makeBackup()" class="btn btn-primary btn-md">Make</button>
</div>
<div class="col-6 d-grid">
<input onChange="restoreBackup(this)" type="file" accept=".zip" class="d-none" id="inputBackup">
<button onclick="openRestoreBackup()" class="btn btn-secondary btn-md">Restore</button>
</div>
</div>
</div>
}
</div>
</div>
</div>
<div class="row">
@@ -47,10 +153,16 @@
<div class="d-flex justify-content-center">
<img src="/defaults/lubelogger_logo.png" />
</div>
<div class="d-flex justify-content-center">
<small class="text-body-secondary">Version 1.0.7</small>
</div>
<p class="lead">
Proudly developed in the rural town of Price, Utah by Hargata Softworks.
</p>
<p class="lead">If you enjoyed using this app, please consider spreading the good word.</p>
<p class="lead">
If you enjoyed using this app, please consider spreading the good word.<br />
If you are a commercial user, or if you just want to support the development of this project, consider subscribing to <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a>
</p>
<div class="d-flex justify-content-center">
<h6 class="display-7 mt-2">Hometown Shoutout</h6>
</div>
@@ -79,16 +191,34 @@
</div>
</div>
<script>
function getCheckedTabs() {
var visibleTabs = $("#visibleTabs :checked").map(function () {
return this.value;
});
return visibleTabs.toArray();
}
function updateSettings(){
var visibleTabs = getCheckedTabs();
var defaultTab = $("#defaultTab").val();
if (!visibleTabs.includes(defaultTab)){
defaultTab = "Dashboard"; //default to dashboard.
}
var userConfigObject = {
useDarkMode: $("#enableDarkMode").is(':checked'),
enableCsvImports: $("#enableCsvImports").is(':checked'),
useMPG: $("#useMPG").is(':checked'),
useDescending: $("#useDescending").is(':checked'),
hideZero: $("#hideZero").is(":checked"),
useUKMpg: $("#useUKMPG").is(":checked")
useUKMpg: $("#useUKMPG").is(":checked"),
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
visibleTabs: visibleTabs,
defaultTab: defaultTab
}
sloader.show();
$.post('/Home/WriteToSettings', { userConfig: userConfigObject}, function(data){
sloader.hide();
if (data) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else {
@@ -96,6 +226,40 @@
}
})
}
function makeBackup() {
$.get('/Files/MakeBackup', function (data) {
window.location.href = data;
});
}
function openRestoreBackup(){
$("#inputBackup").click();
}
function restoreBackup(event) {
let formData = new FormData();
formData.append("file", event.files[0]);
sloader.show();
$.ajax({
url: "/Files/HandleFileUpload",
data: formData,
cache: false,
processData: false,
contentType: false,
type: 'POST',
success: function (response) {
if (response.trim() != '') {
$.post('/Files/RestoreBackup', { fileName : response}, function (data) {
sloader.hide();
if (data){
successToast("Backup Restored");
setTimeout(function () { window.location.href = '/Home/Index' }, 500);
} else {
errorToast("An error occurred, please try again later.");
}
});
}
}
});
}
function enableAuthCheckChanged(){
var enableAuth = $("#enableAuth").is(":checked");
if (enableAuth) {
@@ -120,7 +284,7 @@
if (result.isConfirmed) {
$.post('/Login/CreateLoginCreds', { userName: result.value.username, password: result.value.password }, function (data) {
if (data) {
window.location.href = '/Login';
setTimeout(function () { window.location.href = '/Login' }, 500);
} else {
errorToast("An error occurred, please try again later.");
}

View File

@@ -0,0 +1,26 @@
@{
ViewData["Title"] = "LubeLogger - Login";
}
@section Scripts {
<script src="~/js/login.js" asp-append-version="true"></script>
}
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<img src="/defaults/lubelogger_logo.png" />
<div class="form-group">
<label for="inputUserName">Username</label>
<input type="text" id="inputUserName" class="form-control">
</div>
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="requestPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>Request</button>
</div>
<div class="d-grid">
<a href="/Login/ResetPassword" class="btn btn-link mt-2">I Have a Token</a>
</div>
<div class="d-grid">
<a href="/Login/Index" class="btn btn-link mt-2">Back to Login</a>
</div>
</div>
</div>
</div>

View File

@@ -4,7 +4,7 @@
@section Scripts {
<script src="~/js/login.js" asp-append-version="true"></script>
}
<div class="container chartContainer d-flex align-items-center justify-content-center">
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<img src="/defaults/lubelogger_logo.png" />
@@ -23,6 +23,12 @@
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>Login</button>
</div>
<div class="d-grid">
<a href="/Login/ForgotPassword" class="btn btn-link mt-2">Forgot Password</a>
</div>
<div class="d-grid">
<a href="/Login/Registration" class="btn btn-link mt-2">Register</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,35 @@
@{
ViewData["Title"] = "LubeLogger - Register";
}
@section Scripts {
<script src="~/js/login.js" asp-append-version="true"></script>
}
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<img src="/defaults/lubelogger_logo.png" />
<div class="form-group">
<label for="inputToken">Token</label>
<input type="text" id="inputToken" class="form-control">
</div>
<div class="form-group">
<label for="inputUserName">Email Address</label>
<input type="text" id="inputEmail" class="form-control">
</div>
<div class="form-group">
<label for="inputUserName">Username</label>
<input type="text" id="inputUserName" class="form-control">
</div>
<div class="form-group">
<label for="inputUserPassword">Password</label>
<input type="password" id="inputUserPassword" class="form-control">
</div>
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>Register</button>
</div>
<div class="d-grid">
<a href="/Login/Index" class="btn btn-link mt-2">Back to Login</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,31 @@
@{
ViewData["Title"] = "LubeLogger - Register";
}
@section Scripts {
<script src="~/js/login.js" asp-append-version="true"></script>
}
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
<div class="row">
<div class="col-12">
<img src="/defaults/lubelogger_logo.png" />
<div class="form-group">
<label for="inputToken">Token</label>
<input type="text" id="inputToken" class="form-control">
</div>
<div class="form-group">
<label for="inputUserName">Email Address</label>
<input type="text" id="inputEmail" class="form-control">
</div>
<div class="form-group">
<label for="inputUserPassword">New Password</label>
<input type="password" id="inputUserPassword" class="form-control">
</div>
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>Reset Password</button>
</div>
<div class="d-grid">
<a href="/Login/Index" class="btn btn-link mt-2">Back to Login</a>
</div>
</div>
</div>
</div>

1
Views/Shared/401.cshtml Normal file
View File

@@ -0,0 +1 @@
<h1>Access Denied</h1>

View File

@@ -1,9 +1,12 @@
<!DOCTYPE html>
@inject IConfiguration Configuration
@using CarCareTracker.Helper
<!DOCTYPE html>
@inject IConfigHelper config
@{
var useDarkMode = bool.Parse(Configuration["UseDarkMode"]);
var enableCsvImports = bool.Parse(Configuration["EnableCsvImports"]);
var userConfig = config.GetUserConfig(User);
var useDarkMode = userConfig.UseDarkMode;
var enableCsvImports = userConfig.EnableCsvImports;
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
var numberFormat = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
shortDatePattern = shortDatePattern.ToLower();
if (!shortDatePattern.Contains("dd"))
{
@@ -18,6 +21,9 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="apple-mobile-web-app-title" content="LubeLogger" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>@ViewData["Title"] - CarCareTracker</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" />
@@ -25,6 +31,12 @@
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/loader.css" asp-append-version="true" />
<link rel="stylesheet" href="~/sweetalert/sweetalert2.min.css" asp-append-version="true" />
<link rel="icon" sizes="192x192" href="~/defaults/lubelogger_icon_192.png" />
<link rel="icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
<link rel="apple-touch-icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
<link rel="apple-touch-icon-precomposed" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
<link rel="apple-touch-startup-image" href="~/defaults/lubelogger_launch.png" />
<link rel="manifest" href="~/manifest.json">
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/js/shared.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
@@ -43,6 +55,21 @@
pattern: "@shortDatePattern"
}
}
function globalParseFloat(input){
//remove thousands separator.
var thousandSeparator = "@numberFormat.NumberGroupSeparator";
var decimalSeparator = "@numberFormat.NumberDecimalSeparator";
//strip thousands from input.
input = input.replace(thousandSeparator, "");
//convert to JS format where decimal is only separated by .
input = input.replace(decimalSeparator, ".");
return parseFloat(input);
}
function globalFloatToString(input) {
var decimalSeparator = "@numberFormat.NumberDecimalSeparator";
input = input.replace(".", decimalSeparator);
return input;
}
</script>
@await RenderSectionAsync("Scripts", required: false)
</head>

View File

@@ -1,6 +1,11 @@
@{
@using CarCareTracker.Helper
@{
ViewData["Title"] = "LubeLogger - View Vehicle";
}
@inject IConfigHelper config
@{
var userConfig = config.GetUserConfig(User);
}
@model Vehicle
@section Scripts {
<script src="~/js/vehicle.js" asp-append-version="true"></script>
@@ -10,41 +15,104 @@
<script src="~/js/taxrecord.js" asp-append-version="true"></script>
<script src="~/js/reminderrecord.js" asp-append-version="true"></script>
<script src="~/js/upgraderecord.js" asp-append-version="true"></script>
<script src="~/js/note.js" asp-append-version="true"></script>
<script src="~/js/reports.js" asp-append-version="true"></script>
<script src="~/js/supplyrecord.js" asp-append-version="true"></script>
<script src="~/js/planrecord.js" asp-append-version="true"></script>
<script src="~/js/odometerrecord.js" asp-append-version="true"></script>
<script src="~/lib/chart-js/chart.umd.js"></script>
}
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
<ul class="nav navbar-nav" id="vehicleTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link" onclick="returnToGarage()"><span class="display-3 ms-2"><i class="bi bi-arrow-left-square"></i>Garage</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" onclick="editVehicle(@Model.Id)"><span class="display-3 ms-2"><i class="bi bi-pencil-square"></i>Edit Vehicle</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-file-bar-graph me-2"></i>Dashboard</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-bar-chart-steps me-2"></i>Planner</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-speedometer me-2"></i>Odometer</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><span class="display-3 ms-2"><i class="bi bi-card-checklist me-2"></i>Service Records</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-exclamation-octagon me-2"></i>Repairs</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-fuel-pump me-2"></i>Fuel</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-shop me-2"></i>Supplies</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-currency-dollar me-2"></i>Taxes</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-journal-bookmark me-2"></i>Notes</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>Reminders</span></button>
</li>
<li class="nav-item" role="presentation">
<button onclick="deleteVehicle(@Model.Id)" class="dropdown-item"><span class="display-3 ms-2"><i class="bi bi-trash me-2"></i>Delete Vehicle</span></button>
</li>
</ul>
</div>
<div class="container">
<div class="row">
<div class="d-flex justify-content-between">
<button onclick="returnToGarage()" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></button>
<button onclick="returnToGarage()" class="lubelogger-tab btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></button>
<h1 class="text-truncate display-4">@($"{Model.Year} {Model.Make} {Model.Model}")<small class="text-body-secondary">@($"(#{Model.LicensePlate})")</small></h1>
<button onclick="editVehicle(@Model.Id)" class="btn btn-warning btn-md mt-1 mb-1"><i class="bi bi-pencil-square"></i></button>
<button onclick="editVehicle(@Model.Id)" class="lubelogger-tab btn btn-warning btn-md mt-1 mb-1"><i class="bi bi-pencil-square"></i></button>
<div class="lubelogger-navbar-button">
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
</div>
</div>
</div>
<hr />
<ul class="nav nav-tabs" id="vehicleTab" role="tablist">
<ul class="nav nav-tabs lubelogger-tab" id="vehicleTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist me-2"></i>Service Records</button>
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph me-2"></i>Dash</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon me-2"></i>Repairs</button>
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-bar-chart-steps me-2"></i>Planner</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</button>
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-speedometer me-2"></i>Odometer</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump me-2"></i>Fuel</button>
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist me-2"></i>Service Records</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar me-2"></i>Taxes</button>
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon me-2"></i>Repairs</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark me-2"></i>Notes</button>
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div id="reminderBellDiv" style="display:inline-flex;"><i id="reminderBell" class="bi bi-bell me-2"></i></div>Reminders</button>
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump me-2"></i>Fuel</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph me-2"></i>Reports</button>
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop me-2"></i>Supplies</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar me-2"></i>Taxes</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark me-2"></i>Notes</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>Reminders</button>
</li>
<li class="nav-item dropdown ms-auto" role="presentation">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Manage Vehicle</a>
@@ -54,28 +122,17 @@
</li>
</ul>
<div class="tab-content" id="vehicleTabContent">
<div class="tab-pane fade show active" id="servicerecord-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="gas-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="tax-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="notes-tab-pane" role="tabpanel" tabindex="0">
<div class="row">
<div class="col-12">
<label for="noteTextArea" class="form-label">This is where you can store notes related to the vehicle such as tire size, oil filter size, oil types, etc.</label>
<textarea class="form-control vehicleNoteContainer" id="noteTextArea"></textarea>
</div>
</div>
<div class="row">
<div class="d-flex flex-row-reverse">
<div>
<button onclick="saveVehicleNote(@Model.Id)" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square"></i>Save Note</button>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="accident-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="reminder-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="report-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="upgrade-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.GasRecord)" id="gas-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.TaxRecord)" id="tax-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.NoteRecord)" id="notes-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.RepairRecord)" id="accident-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.ReminderRecord)" id="reminder-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.Dashboard)" id="report-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.SupplyRecord)" id="supply-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.PlanRecord)" id="plan-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.OdometerRecord)" id="odometer-tab-pane" role="tabpanel" tabindex="0"></div>
</div>
</div>
<div class="modal fade" id="editVehicleModal" tabindex="-1" role="dialog">
@@ -96,8 +153,17 @@
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="inputSuppliesModal" tabindex="-1" role="dialog" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="inputSuppliesModalContent"></div>
</div>
</div>
<script>
function GetVehicleId() {
return { vehicleId: @Model.Id};
}
function GetDefaultTab() {
return { tab: "@userConfig.DefaultTab" };
}
bindWindowResize();
</script>

View File

@@ -19,12 +19,21 @@
@if (Model == ImportMode.GasRecord)
{
<a class="btn btn-link" href="/defaults/gassample.csv" target="_blank">Download Sample</a>
} else if (Model == ImportMode.ServiceRecord || Model == ImportMode.RepairRecord)
} else if (Model == ImportMode.ServiceRecord || Model == ImportMode.RepairRecord || Model == ImportMode.UpgradeRecord)
{
<a class="btn btn-link" href="/defaults/servicerecordsample.csv" target="_blank">Download Sample</a>
} else if (Model == ImportMode.TaxRecord)
{
<a class="btn btn-link" href="/defaults/taxrecordsample.csv" target="_blank">Download Sample</a>
} else if (Model == ImportMode.SupplyRecord)
{
<a class="btn btn-link" href="/defaults/supplysample.csv" target="_blank">Download Sample</a>
} else if (Model == ImportMode.PlanRecord)
{
<a class="btn btn-link" href="/defaults/plansample.csv" target="_blank">Download Sample</a>
} else if (Model == ImportMode.OdometerRecord)
{
<a class="btn btn-link" href="/defaults/odometersample.csv" target="_blank">Download Sample</a>
}
</div>
</div>
@@ -60,6 +69,14 @@
getVehicleCollisionRecords(vehicleId);
} else if (mode == "TaxRecord") {
getVehicleTaxRecords(vehicleId);
} else if (mode == "UpgradeRecord") {
getVehicleUpgradeRecords(vehicleId);
} else if (mode == "SupplyRecord") {
getVehicleSupplyRecords(vehicleId);
} else if (mode == "PlanRecord"){
getVehiclePlanRecords(vehicleId);
} else if (mode == "OdometerRecord") {
getVehicleOdometerRecords(vehicleId);
}
} else {
errorToast("An error has occurred, please double check the data and try again.");

View File

@@ -0,0 +1,72 @@
@model List<UserCollaborator>
<div class="row">
<div class="col-8">
<span class="lead">Collaborators</span>
</div>
<div class="col-4">
<button onclick="addCollaborator()" class="btn btn-link btn-sm"><i class="bi bi-person-add"></i></button>
</div>
</div>
<div class="row">
<table class="table table-hover">
<thead>
<tr class="d-flex">
<th scope="col" class="col-8">Username</th>
<th scope="col" class="col-4">Delete</th>
</tr>
</thead>
<tbody>
@foreach (UserCollaborator user in Model)
{
<tr class="d-flex">
<td class="col-8">@user.UserName</td>
<td class="col-4">
@if(User.Identity.Name != user.UserName)
{
<button onclick="deleteCollaborator(@user.UserVehicle.UserId, @user.UserVehicle.VehicleId)" class="btn btn-outline-danger btn-sm"><i class="bi bi-trash"></i></button>
}
</td>
</tr>
}
</tbody>
</table>
</div>
<script>
function deleteCollaborator(userId, vehicleId) {
$.post('/Vehicle/DeleteCollaboratorFromVehicle', {userId: userId, vehicleId: vehicleId}, function(data){
if (data) {
refreshCollaborators();
} else {
errorToast("An error occurred, please try again later");
}
})
}
function addCollaborator() {
Swal.fire({
title: 'Add Collaborator',
html: `
<input type="text" id="inputUserName" class="swal2-input" placeholder="Username">
`,
confirmButtonText: 'Add',
focusConfirm: false,
preConfirm: () => {
const userName = $("#inputUserName").val();
if (!userName) {
Swal.showValidationMessage(`Please enter a username`);
}
return { userName }
},
}).then(function (result) {
if (result.isConfirmed) {
var vehicleId = GetVehicleId().vehicleId;
$.post('/Vehicle/AddCollaboratorsToVehicle', { username: result.value.userName, vehicleId: vehicleId }, function (data) {
if (data.success) {
refreshCollaborators();
} else {
errorToast(data.message)
}
});
}
});
}
</script>

View File

@@ -22,7 +22,11 @@
<label for="collisionRecordDescription">Description</label>
<input type="text" id="collisionRecordDescription" class="form-control" placeholder="Description of item(s) repaired(i.e. Alternator)" value="@Model.Description">
<label for="collisionRecordCost">Cost</label>
<input type="number" id="collisionRecordCost" class="form-control" placeholder="Cost of the repair" value="@(isNew ? "" : Model.Cost)">
<input type="text" id="collisionRecordCost" class="form-control" placeholder="Cost of the repair" value="@(isNew ? "" : Model.Cost)">
@if (isNew)
{
@await Html.PartialAsync("_SupplyStore", "RepairRecord")
}
</div>
<div class="col-md-6 col-12">
<label for="collisionRecordNotes">Notes(optional)</label>
@@ -71,6 +75,7 @@
</div>
<script>
var uploadedFiles = [];
var selectedSupplies = [];
getUploadedFilesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)

View File

@@ -1,7 +1,8 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
var hideZero = config.GetUserConfig(User).HideZero;
}
@model List<CollisionRecord>
<div class="row">
@@ -20,6 +21,7 @@
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('RepairRecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('RepairRecord')">Export to CSV</a></li>
</ul>
</div>
}
@@ -32,12 +34,17 @@
</div>
<div class="row vehicleDetailTabContainer">
<div class="col-12">
<div class="row mt-2 showOnPrint">
<div class="d-flex">
<img src="/defaults/lubelogger_logo.png" />
</div>
</div>
<table class="table table-hover">
<thead>
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-1">Date</th>
<th scope="col" class="col-2 col-xl-1">Date</th>
<th scope="col" class="col-2">Odometer</th>
<th scope="col" class="col-4">Description</th>
<th scope="col" class="col-3 col-xl-4">Description</th>
<th scope="col" class="col-2">Cost</th>
<th scope="col" class="col-3">Notes</th>
</tr>
@@ -46,11 +53,11 @@
@foreach (CollisionRecord collisionRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditCollisionRecordModal(@collisionRecord.Id)">
<td class="col-1">@collisionRecord.Date.ToShortDateString()</td>
<td class="col-2 col-xl-1">@collisionRecord.Date.ToShortDateString()</td>
<td class="col-2">@collisionRecord.Mileage</td>
<td class="col-4">@collisionRecord.Description</td>
<td class="col-3 col-xl-4">@collisionRecord.Description</td>
<td class="col-2">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate">@collisionRecord.Notes</td>
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(collisionRecord.Notes)</td>
</tr>
}
</tbody>

Some files were not shown because too many files have changed in this diff Show More