Compare commits

..

307 Commits

Author SHA1 Message Date
Hargata Softworks
4a007530a6 Merge pull request #188 from hargata/Hargata/parse.url
added setting to automatically load parsed markdown.
2024-01-30 08:19:04 -07:00
DESKTOP-GENO133\IvanPlex
04ce448b23 removed onclick event since it ain;t helping. 2024-01-30 08:18:45 -07:00
DESKTOP-GENO133\IvanPlex
ccd446f299 added setting to automatically load parsed markdown. 2024-01-30 08:16:28 -07:00
Hargata Softworks
c49c8a5301 Merge pull request #186 from hargata/Hargata/parse.url
Hargata/parse.url
2024-01-29 21:05:24 -07:00
DESKTOP-GENO133\IvanPlex
55cc2819d0 show overlay in place instead. 2024-01-29 21:04:22 -07:00
DESKTOP-GENO133\IvanPlex
f8de7de0d6 added markdown for named notes. 2024-01-29 20:33:37 -07:00
Hargata Softworks
2ea1bc2c20 Merge pull request #185 from hargata/Hargata/parse.url
Added Markdown Parsing.
2024-01-29 19:53:38 -07:00
DESKTOP-GENO133\IvanPlex
90d095ea51 Updated version. 2024-01-29 19:50:01 -07:00
DESKTOP-GENO133\IvanPlex
e1d12d0918 added dependency 2024-01-29 19:47:45 -07:00
DESKTOP-GENO133\IvanPlex
d9d0957040 added method to parse markdown data. 2024-01-29 19:43:53 -07:00
Hargata Softworks
9d73db3c51 Merge pull request #184 from hargata/Hargata/reduce.login.error
added env variable to reduce auth-related logs.
2024-01-29 17:21:07 -07:00
DESKTOP-GENO133\IvanPlex
c0f0786fd4 added env variable to reduce auth-related logs. 2024-01-29 17:20:39 -07:00
Hargata Softworks
357eff116f Merge pull request #183 from hargata/Hargata/export.attachments
Export attachments for vehicle.
2024-01-29 13:33:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
32a047c522 added print function. 2024-01-29 13:32:42 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8a237bb7ec Export attachments for vehicle. 2024-01-29 13:24:30 -07:00
DESKTOP-GENO133\IvanPlex
f5b9072cc6 Updated Github pages. 2024-01-28 15:06:06 -07:00
Hargata Softworks
4e3eaa53ff Merge pull request #180 from hargata/Hargata/hr-mpg
Hargata/hr mpg
2024-01-28 07:39:42 -07:00
Hargata Softworks
4779d3f161 Merge pull request #181 from hargata/Hargata/move.records
Move records around.
2024-01-28 07:38:04 -07:00
DESKTOP-GENO133\IvanPlex
b0173fae94 added object inheritance. 2024-01-28 07:37:12 -07:00
DESKTOP-GENO133\IvanPlex
c6ee8830a3 added hours per gallon calculation. 2024-01-27 23:29:05 -07:00
DESKTOP-GENO133\IvanPlex
c2eeab5025 Merge commit '43fd40347f34bc7676bfd5531abf612e06285e5c' into Hargata/hr-mpg 2024-01-27 22:53:55 -07:00
DESKTOP-GENO133\IvanPlex
43fd40347f added engine hours variable. 2024-01-27 21:14:29 -07:00
Hargata Softworks
53139f9bb2 Merge pull request #179 from hargata/Hargata/sorting.supplies
added sorting in supplies tab.
2024-01-27 12:17:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
0b6033cc00 added sorting in supplies tab. 2024-01-27 12:16:49 -07:00
Hargata Softworks
6f0115e5c5 Merge pull request #176 from hargata/Hargata/pinned.notes.garage
touch screen support for pinned notes.
2024-01-27 07:41:06 -07:00
Hargata Softworks
2403adf537 Merge pull request #177 from hargata/Hargata/sort.columns
make cost columns sortable.
2024-01-27 07:40:49 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f00ab897b5 make cost columns sortable. 2024-01-27 07:37:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
093631bf6f touch screen support for pinned notes. 2024-01-26 13:19:37 -07:00
Hargata Softworks
5c2835ab76 Merge pull request #175 from hargata/Hargata/pinned.notes.garage
added maxfilesize label and error handling.
2024-01-26 12:42:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
03029981fd added maxfilesize label and error handling. 2024-01-26 12:40:39 -07:00
Hargata Softworks
69b1838038 Merge pull request #174 from hargata/Hargata/pinned.notes.garage
display pinned notes in garage.
2024-01-26 12:02:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1c2f83026c display pinned notes in garage. 2024-01-26 12:01:53 -07:00
Hargata Softworks
d36f2c59e3 Merge pull request #172 from hargata/Hargata/demo.stuff
clear temp files restoring demo instance.
2024-01-26 09:26:15 -07:00
DESKTOP-T0O5CDB\DESK-555BD
53ebec3f03 clear temp files restoring demo instance. 2024-01-26 09:25:44 -07:00
Hargata Softworks
f2cbbaeb12 Merge pull request #170 from hargata/Hargata/average.fuel.fix
include partial fuel up fuel consumption in average mpg.
2024-01-25 22:19:02 -07:00
DESKTOP-GENO133\IvanPlex
31c1202649 include partial fuel up fuel consumption in average mpg. 2024-01-25 22:18:33 -07:00
Hargata Softworks
331499461a Merge pull request #148 from hargata/Hargata/documentation
Hargata/documentation
2024-01-25 17:52:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8c6dd5e343 Updated website 2024-01-25 17:52:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
01b1e8228d Merge branch 'main' into Hargata/documentation 2024-01-25 17:01:31 -07:00
Hargata Softworks
43979d6115 Merge pull request #167 from hargata/Hargata/demo.mode
Demo Mode
2024-01-25 15:02:20 -07:00
DESKTOP-T0O5CDB\DESK-555BD
6a9860e202 added demo mode. 2024-01-25 15:01:56 -07:00
Hargata Softworks
39338e8028 Merge pull request #166 from hargata/Hargata/decimal.odometer
added more screenshots
2024-01-25 14:23:03 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d1d5351a01 added more screenshots 2024-01-25 14:22:21 -07:00
Hargata Softworks
c9385c7fdd Merge pull request #165 from hargata/Hargata/decimal.odometer
updated website.
2024-01-25 14:06:23 -07:00
DESKTOP-T0O5CDB\DESK-555BD
afaae89af6 updated website. 2024-01-25 14:05:59 -07:00
Hargata Softworks
b9d799cd49 Merge pull request #164 from hargata/Hargata/decimal.odometer
Prevent decimals in odometer fields to error out.
2024-01-25 13:59:32 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e47c541e08 Prevent decimals in odometer fields to error out. 2024-01-25 13:58:47 -07:00
Hargata Softworks
51ff01d2cd Merge pull request #162 from hargata/Hargata/locale.gas.fix
more gas fixes due to european locale.
2024-01-25 08:33:25 -07:00
DESKTOP-GENO133\IvanPlex
618399cb09 more gas fixes due to european locale. 2024-01-25 08:31:56 -07:00
Hargata Softworks
b837a2e528 Merge pull request #158 from hargata/Hargata/reminder.email.endpoint
stop method from returning prematurely.
2024-01-24 22:27:32 -07:00
DESKTOP-GENO133\IvanPlex
a1e8b8f9cc stop method from returning prematurely. 2024-01-24 22:26:55 -07:00
Hargata Softworks
787c5da72a Merge pull request #157 from hargata/Hargata/reminder.email.endpoint
added vehicle information.
2024-01-24 22:24:40 -07:00
DESKTOP-GENO133\IvanPlex
260703be8e added vehicle information. 2024-01-24 22:23:31 -07:00
Hargata Softworks
053801b046 Merge pull request #156 from hargata/Hargata/reminder.email.endpoint
Hargata/reminder.email.endpoint
2024-01-24 21:48:15 -07:00
DESKTOP-GENO133\IvanPlex
db9b1970c5 updated API documentation. 2024-01-24 21:47:05 -07:00
DESKTOP-GENO133\IvanPlex
b153ef5ea5 added more mileage intervals and api endpoint to send out reminder emails. 2024-01-24 21:43:26 -07:00
Hargata Softworks
b54809f399 Merge pull request #155 from hargata/Hargata/zero.chart
Baseline zero charges for bar charts.
2024-01-24 09:07:33 -07:00
DESKTOP-GENO133\IvanPlex
f7f47c54ff added baseline zero costs charge so that bar charts display all months regardless of whether there are costs or not. 2024-01-24 09:03:55 -07:00
Hargata Softworks
92564ae527 Merge pull request #151 from hargata/Hargata/recurring.tax
Hargata/recurring.tax
2024-01-23 22:41:51 -07:00
DESKTOP-GENO133\IvanPlex
52ada8574d add pinned icon for pinned notes. 2024-01-23 22:15:53 -07:00
DESKTOP-GENO133\IvanPlex
013fb67943 Recurring fees. 2024-01-23 21:48:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d86298f502 make tax recurring. 2024-01-23 21:10:59 -07:00
Hargata Softworks
5891b78be0 Merge pull request #149 from hargata/Hargata/pin.notes
added ability to pin notes so that they always show up on top.
2024-01-23 17:48:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a9e3e44f2c added ability to pin notes so that they always show up on top. 2024-01-23 17:46:54 -07:00
Hargata Softworks
0d1c7234e8 Create Collaboration.md 2024-01-23 17:22:07 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e3abe5f209 Merge branch 'main' into Hargata/documentation 2024-01-23 17:15:11 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b453bfce5b removed auth html. 2024-01-23 17:14:53 -07:00
Hargata Softworks
147a1b03a7 Create Registration.md 2024-01-23 16:57:02 -07:00
Hargata Softworks
3017db5f86 Update Configuring.md 2024-01-23 16:45:34 -07:00
Hargata Softworks
399d0d8058 Update GettingStarted.md 2024-01-23 16:43:52 -07:00
Hargata Softworks
b8c0d4ef67 Update GettingStarted.md 2024-01-23 16:43:03 -07:00
Hargata Softworks
918d086705 Update Configuring.md 2024-01-23 16:39:40 -07:00
Hargata Softworks
ac05acf96b Update Configuring.md 2024-01-23 16:37:49 -07:00
Hargata Softworks
e7449806c0 Create Configuring.md 2024-01-23 16:36:46 -07:00
Hargata Softworks
baab3213b5 Update GettingStarted.md 2024-01-23 16:19:41 -07:00
Hargata Softworks
d68e9e9589 Create GettingStarted.md 2024-01-23 16:16:38 -07:00
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
DESKTOP-T0O5CDB\DESK-555BD
dbb139dfad updated documentation. 2024-01-17 12:00:05 -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
Hargata Softworks
96914b3db4 Merge pull request #39 from hargata/Hargata/upgradestab
Upgrades Tab
2024-01-08 19:04:57 -07:00
DESKTOP-GENO133\IvanPlex
7ad6d3369d updated field placeholders 2024-01-08 19:00:30 -07:00
DESKTOP-GENO133\IvanPlex
0ae28d436b added upgrade tab functionality. 2024-01-08 18:33:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4575cf338f Added upgrades section backend. 2024-01-08 17:47:11 -07:00
Hargata Softworks
c526a9f207 Merge pull request #37 from hargata/Hargata/uk.mpg
Hargata/uk.mpg
2024-01-08 16:11:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1a3282c1ef removed unnecessary lines. 2024-01-08 16:11:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f52e5b49c7 rehashed logic. 2024-01-08 15:31:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
507fb6daf6 refreshed logic. 2024-01-08 15:30:15 -07:00
Hargata Softworks
9d3fbd05fc Merge pull request #35 from hargata/Hargata/uk.mpg
added support for UK MPG
2024-01-08 15:14:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
2a0f884e89 fixed unit cost for gas. 2024-01-08 15:13:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
26012bf27a added support for UK MPG 2024-01-08 15:11:23 -07:00
Hargata Softworks
3fa3dbaa8c Merge pull request #32 from hargata/Hargata/ghcr.multiplatform
removing arm64 support for now until I can figure out a workaround fo…
2024-01-08 13:49:12 -07:00
DESKTOP-T0O5CDB\DESK-555BD
95cac60c71 removing arm64 support for now until I can figure out a workaround for qemu. 2024-01-08 13:48:51 -07:00
Hargata Softworks
7878ce65ca Merge pull request #31 from hargata/Hargata/ghcr.multiplatform
...
2024-01-08 13:44:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c6c57a5de6 ... 2024-01-08 13:44:08 -07:00
Hargata Softworks
2ece3cd113 Merge pull request #30 from hargata/Hargata/ghcr.multiplatform
Hargata/ghcr.multiplatform
2024-01-08 13:37:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9504674933 tab 2024-01-08 13:36:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3c0bdcea0e approach 2 2024-01-08 13:35:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a4762d5b32 lil disappointment 2024-01-08 13:26:24 -07:00
Hargata Softworks
dc18bb5b8f Merge pull request #29 from hargata/Hargata/ghcr.multiplatform
lemme try this.
2024-01-08 13:15:54 -07:00
DESKTOP-T0O5CDB\DESK-555BD
452248e681 lemme try this. 2024-01-08 13:15:32 -07:00
Hargata Softworks
a7ea03014f Merge pull request #28 from hargata/Hargata/ghcr.multiplatform
Multi Platform Docker Image
2024-01-08 12:49:16 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cb89ca3426 hm 2024-01-08 12:47:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
760f7e1888 I wondre if this works 2024-01-08 12:44:07 -07:00
Hargata Softworks
ba149568a5 Merge pull request #27 from hargata/Hargata/rename.uploadedfiles
Rename Uploaded Files
2024-01-08 11:25:50 -07:00
DESKTOP-GENO133\IvanPlex
2ef281bd0a added setting to hide zero costs with dashes 2024-01-08 11:24:51 -07:00
DESKTOP-GENO133\IvanPlex
859796cfe8 fixed file deletion button not working. 2024-01-08 11:00:19 -07:00
DESKTOP-GENO133\IvanPlex
f1e0254f95 added ability to rename uploaded files on tax records. 2024-01-08 10:53:44 -07:00
DESKTOP-GENO133\IvanPlex
70d0827432 added functionality to edit uploaded file names for gas records. 2024-01-08 10:50:41 -07:00
DESKTOP-GENO133\IvanPlex
851a7af287 added functionality to rename uploaded files in collision records. 2024-01-08 10:34:45 -07:00
DESKTOP-GENO133\IvanPlex
e46be7ba0a added ability to rename files on service record page. 2024-01-08 09:05:09 -07:00
Hargata Softworks
b4258dc11a Merge pull request #24 from hargata/fuelly.import
Fuelly.import
2024-01-07 23:14:06 -07:00
DESKTOP-GENO133\IvanPlex
0f35621846 config 2024-01-07 23:08:44 -07:00
DESKTOP-GENO133\IvanPlex
374f919296 added description to mapper 2024-01-07 23:04:20 -07:00
DESKTOP-GENO133\IvanPlex
33b824b316 fuelly import but can only import one csv per car. 2024-01-07 23:00:07 -07:00
Hargata Softworks
418e468f14 Update README.md 2024-01-07 16:42:52 -07:00
Hargata Softworks
0a1f6a569d Merge pull request #22 from hargata/electric.vehicle
fixed datepicker format not parsing correctly.
2024-01-07 16:12:40 -07:00
DESKTOP-GENO133\IvanPlex
2a842d1c8c fixed datepicker format not parsing correctly. 2024-01-07 16:12:16 -07:00
190 changed files with 9690 additions and 809 deletions

7
.env
View File

@@ -1,2 +1,9 @@
LC_ALL=en_US.UTF-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=""
LOGGING__LOGLEVEL__DEFAULT=Error

View File

@@ -1,19 +0,0 @@
name: Docker Image CI
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and push the Docker image
run: |
docker login -u "hargata" -p "${{ secrets.GHCR_PAT }}" ghcr.io
docker build . --file Dockerfile --tag ghcr.io/hargata/lubelogger:latest
docker push ghcr.io/hargata/lubelogger:latest

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

31
.github/workflows/ghcr-docker-image.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Docker Image To GHCR
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 GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: "hargata"
password: "${{ secrets.GHCR_PAT }}"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/hargata/lubelogger:latest

View File

@@ -0,0 +1,530 @@
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 IUserAccessDataAccess _userAccessDataAccess;
private readonly IUserRecordDataAccess _userRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
private readonly IGasHelper _gasHelper;
private readonly IUserLogic _userLogic;
private readonly IFileHelper _fileHelper;
private readonly IMailHelper _mailHelper;
public APIController(IVehicleDataAccess dataAccess,
IGasHelper gasHelper,
IReminderHelper reminderHelper,
INoteDataAccess noteDataAccess,
IServiceRecordDataAccess serviceRecordDataAccess,
IGasRecordDataAccess gasRecordDataAccess,
ICollisionRecordDataAccess collisionRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess,
IUserAccessDataAccess userAccessDataAccess,
IUserRecordDataAccess userRecordDataAccess,
IMailHelper mailHelper,
IFileHelper fileHelper,
IUserLogic userLogic)
{
_dataAccess = dataAccess;
_noteDataAccess = noteDataAccess;
_serviceRecordDataAccess = serviceRecordDataAccess;
_gasRecordDataAccess = gasRecordDataAccess;
_collisionRecordDataAccess = collisionRecordDataAccess;
_taxRecordDataAccess = taxRecordDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess;
_userAccessDataAccess = userAccessDataAccess;
_userRecordDataAccess = userRecordDataAccess;
_mailHelper = mailHelper;
_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/vehicle/reminders/send")]
public IActionResult SendReminders(List<ReminderUrgency> urgencies)
{
var vehicles = _dataAccess.GetVehicles();
List<OperationResponse> operationResponses = new List<OperationResponse>();
foreach(Vehicle vehicle in vehicles)
{
var vehicleId = vehicle.Id;
//get reminders
var currentMileage = GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).OrderByDescending(x => x.Urgency).ToList();
results.RemoveAll(x => !urgencies.Contains(x.Urgency));
if (!results.Any())
{
continue;
}
//get list of recipients.
var userIds = _userAccessDataAccess.GetUserAccessByVehicleId(vehicleId).Select(x => x.Id.UserId);
List<string> emailRecipients = new List<string>();
foreach (int userId in userIds)
{
var userData = _userRecordDataAccess.GetUserRecordById(userId);
emailRecipients.Add(userData.EmailAddress);
};
if (!emailRecipients.Any())
{
continue;
}
var result = _mailHelper.NotifyUserForReminders(vehicle, emailRecipients, results);
operationResponses.Add(result);
}
if (operationResponses.All(x => x.Success))
{
return Json(new OperationResponse { Success = true, Message = "Emails sent" });
} else if (operationResponses.All(x => !x.Success))
{
return Json(new OperationResponse { Success = false, Message = "All emails failed, check SMTP settings" });
} else
{
return Json(new OperationResponse { Success = true, Message = "Some emails sent, some failed, check recipient settings" });
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/makebackup")]
public IActionResult MakeBackup()
{
var result = _fileHelper.MakeBackup();
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
[Route("/api/demo/restore")]
public IActionResult RestoreDemo()
{
var result = _fileHelper.RestoreBackup("/defaults/demo_default.zip", true);
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] [HttpPost]
public ActionResult DeleteFiles(string fileLocation) public IActionResult DeleteFiles(string fileLocation)
{ {
var result = _fileHelper.DeleteFile(fileLocation); var result = _fileHelper.DeleteFile(fileLocation);
return Json(result); 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) private string UploadFile(IFormFile fileToUpload)
{ {
string uploadDirectory = "temp/"; string uploadDirectory = "temp/";

View File

@@ -1,14 +1,11 @@
using CarCareTracker.External.Interfaces; using CarCareTracker.External.Interfaces;
using CarCareTracker.Models; using CarCareTracker.Models;
using LiteDB;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Diagnostics; using System.Diagnostics;
using static System.Net.Mime.MediaTypeNames;
using System.Drawing;
using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
using CarCareTracker.Helper; using CarCareTracker.Helper;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using CarCareTracker.Logic;
namespace CarCareTracker.Controllers namespace CarCareTracker.Controllers
{ {
@@ -17,17 +14,23 @@ namespace CarCareTracker.Controllers
{ {
private readonly ILogger<HomeController> _logger; private readonly ILogger<HomeController> _logger;
private readonly IVehicleDataAccess _dataAccess; private readonly IVehicleDataAccess _dataAccess;
private readonly IFileHelper _fileHelper; private readonly IUserLogic _userLogic;
private readonly IConfiguration _config; 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; _logger = logger;
_dataAccess = dataAccess; _dataAccess = dataAccess;
_fileHelper = fileHelper;
_config = configuration; _config = configuration;
_userLogic = userLogic;
}
private int GetUserID()
{
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
} }
public IActionResult Index(string tab = "garage") public IActionResult Index(string tab = "garage")
{ {
return View(model: tab); return View(model: tab);
@@ -35,51 +38,22 @@ namespace CarCareTracker.Controllers
public IActionResult Garage() public IActionResult Garage()
{ {
var vehiclesStored = _dataAccess.GetVehicles(); var vehiclesStored = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
return PartialView("_GarageDisplay", vehiclesStored); return PartialView("_GarageDisplay", vehiclesStored);
} }
public IActionResult Settings() public IActionResult Settings()
{ {
var userConfig = new UserConfig var userConfig = _config.GetUserConfig(User);
{
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)])
};
return PartialView("_Settings", userConfig); return PartialView("_Settings", userConfig);
} }
[HttpPost] [HttpPost]
public IActionResult WriteToSettings(UserConfig userConfig) public IActionResult WriteToSettings(UserConfig userConfig)
{ {
try var result = _config.SaveUserConfig(User, userConfig);
{ return Json(result);
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);
} }
public IActionResult Privacy() public IActionResult Privacy()
{ {

View File

@@ -1,4 +1,5 @@
using CarCareTracker.Helper; using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models; using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
@@ -13,19 +14,34 @@ namespace CarCareTracker.Controllers
public class LoginController : Controller public class LoginController : Controller
{ {
private IDataProtector _dataProtector; private IDataProtector _dataProtector;
private ILoginLogic _loginLogic;
private readonly ILogger<LoginController> _logger; private readonly ILogger<LoginController> _logger;
public LoginController( public LoginController(
ILogger<LoginController> logger, ILogger<LoginController> logger,
IDataProtectionProvider securityProvider IDataProtectionProvider securityProvider,
ILoginLogic loginLogic
) )
{ {
_dataProtector = securityProvider.CreateProtector("login"); _dataProtector = securityProvider.CreateProtector("login");
_logger = logger; _logger = logger;
_loginLogic = loginLogic;
} }
public IActionResult Index() public IActionResult Index()
{ {
return View(); return View();
} }
public IActionResult Registration()
{
return View();
}
public IActionResult ForgotPassword()
{
return View();
}
public IActionResult ResetPassword()
{
return View();
}
[HttpPost] [HttpPost]
public IActionResult Login(LoginModel credentials) public IActionResult Login(LoginModel credentials)
{ {
@@ -37,23 +53,12 @@ namespace CarCareTracker.Controllers
//compare it against hashed credentials //compare it against hashed credentials
try try
{ {
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath); var userData = _loginLogic.ValidateUserCredentials(credentials);
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents); if (userData.Id != default)
if (existingUserConfig is not null)
{ {
//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)
{
//auth success, create auth cookie
//encrypt stuff.
AuthCookie authCookie = new AuthCookie AuthCookie authCookie = new AuthCookie
{ {
Id = 1, //this is hardcoded for now UserData = userData,
UserName = credentials.UserName,
ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1) ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1)
}; };
var serializedCookie = JsonSerializer.Serialize(authCookie); var serializedCookie = JsonSerializer.Serialize(authCookie);
@@ -62,33 +67,39 @@ namespace CarCareTracker.Controllers
return Json(true); return Json(true);
} }
} }
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error on saving config file."); _logger.LogError(ex, "Error on saving config file.");
} }
return Json(false); 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. [Authorize] //User must already be logged in to do this.
[HttpPost] [HttpPost]
public IActionResult CreateLoginCreds(LoginModel credentials) public IActionResult CreateLoginCreds(LoginModel credentials)
{ {
try try
{ {
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath); var result = _loginLogic.CreateRootUserCredentials(credentials);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents); return Json(result);
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);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -102,19 +113,13 @@ namespace CarCareTracker.Controllers
{ {
try try
{ {
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath); var result = _loginLogic.DeleteRootUserCredentials();
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));
//destroy any login cookies. //destroy any login cookies.
if (result)
{
Response.Cookies.Delete("ACCESS_TOKEN"); Response.Cookies.Delete("ACCESS_TOKEN");
return Json(true); }
return Json(result);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -129,20 +134,5 @@ namespace CarCareTracker.Controllers
Response.Cookies.Delete("ACCESS_TOKEN"); Response.Cookies.Delete("ACCESS_TOKEN");
return Json(true); 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,9 +1,10 @@
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 WORKDIR /App
COPY . ./ COPY . ./
RUN dotnet restore ARG TARGETARCH
RUN dotnet publish -c Release -o out RUN dotnet restore -a $TARGETARCH
RUN dotnet publish -a $TARGETARCH -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:8.0 FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /App WORKDIR /App

17
Enum/ImportMode.cs Normal file
View File

@@ -0,0 +1,17 @@
namespace CarCareTracker.Models
{
public enum ImportMode
{
ServiceRecord = 0,
RepairRecord = 1,
GasRecord = 2,
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,23 @@
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,
FortyThousandMiles = 40000,
FiftyThousandMiles = 50000,
SixtyThousandMiles = 60000,
OneHundredThousandMiles = 100000,
OneHundredFiftyThousandMiles = 150000
}
}

View File

@@ -0,0 +1,13 @@
namespace CarCareTracker.Models
{
public enum ReminderMonthInterval
{
OneMonth = 1,
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 dbName = StaticHelper.DbName;
private static string tableName = "notes"; private static string tableName = "notes";
public Note GetNoteByVehicleId(int vehicleId) public List<Note> GetNotesByVehicleId(int vehicleId)
{ {
using (var db = new LiteDatabase(dbName)) using (var db = new LiteDatabase(dbName))
{ {
var table = db.GetCollection<Note>(tableName); var table = db.GetCollection<Note>(tableName);
var noteToReturn = table.FindOne(Query.EQ(nameof(Note.VehicleId), vehicleId)); var noteToReturn = table.Find(Query.EQ(nameof(Note.VehicleId), vehicleId));
return noteToReturn ?? new Note(); 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)) using (var db = new LiteDatabase(dbName))
{ {
@@ -27,12 +35,21 @@ namespace CarCareTracker.External.Implementations
return true; return true;
}; };
} }
public bool DeleteNoteByVehicleId(int vehicleId) public bool DeleteNoteById(int noteId)
{ {
using (var db = new LiteDatabase(dbName)) using (var db = new LiteDatabase(dbName))
{ {
var table = db.GetCollection<Note>(tableName); 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; 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,57 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class UpgradeRecordDataAccess : IUpgradeRecordDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "upgraderecords";
public List<UpgradeRecord> GetUpgradeRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
var upgradeRecords = table.Find(Query.EQ(nameof(UpgradeRecord.VehicleId), vehicleId));
return upgradeRecords.ToList() ?? new List<UpgradeRecord>();
};
}
public UpgradeRecord GetUpgradeRecordById(int upgradeRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
return table.FindById(upgradeRecordId);
};
}
public bool DeleteUpgradeRecordById(int upgradeRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
table.Delete(upgradeRecordId);
return true;
};
}
public bool SaveUpgradeRecordToVehicle(UpgradeRecord upgradeRecord)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
table.Upsert(upgradeRecord);
return true;
};
}
public bool DeleteAllUpgradeRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
var upgradeRecords = table.DeleteMany(Query.EQ(nameof(UpgradeRecord.VehicleId), vehicleId));
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)) using (var db = new LiteDatabase(dbName))
{ {
var table = db.GetCollection<Vehicle>(tableName); var table = db.GetCollection<Vehicle>(tableName);
table.Upsert(vehicle); var result = table.Upsert(vehicle);
return true; return true;
}; };
} }

View File

@@ -4,8 +4,10 @@ namespace CarCareTracker.External.Interfaces
{ {
public interface INoteDataAccess public interface INoteDataAccess
{ {
public Note GetNoteByVehicleId(int vehicleId); public List<Note> GetNotesByVehicleId(int vehicleId);
public bool SaveNoteToVehicleId(Note note); public Note GetNoteById(int noteId);
bool DeleteNoteByVehicleId(int vehicleId); 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,13 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IUpgradeRecordDataAccess
{
public List<UpgradeRecord> GetUpgradeRecordsByVehicleId(int vehicleId);
public UpgradeRecord GetUpgradeRecordById(int upgradeRecordId);
public bool DeleteUpgradeRecordById(int upgradeRecordId);
public bool SaveUpgradeRecordToVehicle(UpgradeRecord upgradeRecord);
public bool DeleteAllUpgradeRecordsByVehicleId(int vehicleId);
}
}

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");
}
}
}
}
}

143
Helper/ConfigHelper.cs Normal file
View File

@@ -0,0 +1,143 @@
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)]),
UseMarkDownOnSavedNotes = bool.Parse(_config[nameof(UserConfig.UseMarkDownOnSavedNotes)]),
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,27 @@
namespace CarCareTracker.Helper using CarCareTracker.Models;
using System.IO.Compression;
namespace CarCareTracker.Helper
{ {
public interface IFileHelper public interface IFileHelper
{ {
string GetFullFilePath(string currentFilePath); string GetFullFilePath(string currentFilePath, bool mustExist = true);
public string MoveFileFromTemp(string currentFilePath, string newFolder); string MoveFileFromTemp(string currentFilePath, string newFolder);
public bool DeleteFile(string currentFilePath); bool DeleteFile(string currentFilePath);
string MakeBackup();
bool RestoreBackup(string fileName, bool clearExisting = false);
string MakeAttachmentsExport(List<GenericReportModel> exportData);
} }
public class FileHelper : IFileHelper public class FileHelper : IFileHelper
{ {
private readonly IWebHostEnvironment _webEnv; private readonly IWebHostEnvironment _webEnv;
public FileHelper(IWebHostEnvironment webEnv) private readonly ILogger<IFileHelper> _logger;
public FileHelper(IWebHostEnvironment webEnv, ILogger<IFileHelper> logger)
{ {
_webEnv = webEnv; _webEnv = webEnv;
_logger = logger;
} }
public string GetFullFilePath(string currentFilePath) public string GetFullFilePath(string currentFilePath, bool mustExist = true)
{ {
if (currentFilePath.StartsWith("/")) if (currentFilePath.StartsWith("/"))
{ {
@@ -23,11 +31,172 @@
if (File.Exists(oldFilePath)) if (File.Exists(oldFilePath))
{ {
return oldFilePath; return oldFilePath;
} else }
else if (!mustExist)
{
return oldFilePath;
}
{ {
return string.Empty; return string.Empty;
} }
} }
public bool RestoreBackup(string fileName, bool clearExisting = false)
{
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);
}
else if (clearExisting)
{
var filesToDelete = Directory.GetFiles(existingPath);
foreach (string file in filesToDelete)
{
File.Delete(file);
}
}
//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);
}
else if (clearExisting)
{
var filesToDelete = Directory.GetFiles(existingPath);
foreach (string file in filesToDelete)
{
File.Delete(file);
}
}
//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 MakeAttachmentsExport(List<GenericReportModel> exportData)
{
var folderName = Guid.NewGuid();
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{folderName}");
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
int fileIndex = 0;
foreach(GenericReportModel reportModel in exportData)
{
foreach(UploadedFiles file in reportModel.Files)
{
var fileToCopy = GetFullFilePath(file.Location);
var destFileName = $"{tempPath}/{fileIndex}{Path.GetExtension(file.Location)}";
File.Copy(fileToCopy, destFileName);
fileIndex++;
}
}
var destFilePath = $"{tempPath}.zip";
ZipFile.CreateFromDirectory(tempPath, destFilePath);
//delete temp directory
Directory.Delete(tempPath, true);
var zipFileName = $"/temp/{folderName}.zip";
return zipFileName;
}
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) public string MoveFileFromTemp(string currentFilePath, string newFolder)
{ {
string tempPath = "temp/"; string tempPath = "temp/";
@@ -35,7 +204,8 @@
{ {
return currentFilePath; return currentFilePath;
} }
if (currentFilePath.StartsWith("/")) { if (currentFilePath.StartsWith("/"))
{
currentFilePath = currentFilePath.Substring(1); currentFilePath = currentFilePath.Substring(1);
} }
string uploadPath = Path.Combine(_webEnv.WebRootPath, newFolder); string uploadPath = Path.Combine(_webEnv.WebRootPath, newFolder);
@@ -64,7 +234,8 @@
if (!File.Exists(filePath)) //verify file no longer exists. if (!File.Exists(filePath)) //verify file no longer exists.
{ {
return true; return true;
} else }
else
{ {
return false; return false;
} }

123
Helper/GasHelper.cs Normal file
View File

@@ -0,0 +1,123 @@
using CarCareTracker.Models;
namespace CarCareTracker.Helper
{
public interface IGasHelper
{
List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG);
string GetAverageGasMileage(List<GasRecordViewModel> results, bool useMPG);
}
public class GasHelper : IGasHelper
{
public string GetAverageGasMileage(List<GasRecordViewModel> results, bool useMPG)
{
var recordsToCalculateGallons = results.Where(x => x.MilesPerGallon > 0 || !x.IsFillToFull);
var recordWithCalculatedMPG = recordsToCalculateGallons.Where(x => x.MilesPerGallon > 0);
var minMileage = results.Min(x => x.Mileage);
if (recordWithCalculatedMPG.Any())
{
var maxMileage = recordWithCalculatedMPG.Max(x => x.Mileage);
var totalGallonsConsumed = recordsToCalculateGallons.Sum(x => x.Gallons);
var deltaMileage = maxMileage - minMileage;
var averageGasMileage = (maxMileage - minMileage) / totalGallonsConsumed;
if (!useMPG)
{
averageGasMileage = 100 / averageGasMileage;
}
return averageGasMileage.ToString("F");
}
return "0";
}
public List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG)
{
//need to order by to get correct results
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
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;
}
}
}

128
Helper/MailHelper.cs Normal file
View File

@@ -0,0 +1,128 @@
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);
OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders);
}
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 };
}
}
public OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
}
if (!emailAddresses.Any())
{
return new OperationResponse { Success = false, Message = "No recipients could be found" };
}
if (!reminders.Any())
{
return new OperationResponse { Success = false, Message = "No reminders could be found" };
}
string emailSubject = $"Vehicle Reminders From LubeLogger - {DateTime.Now.ToShortDateString()}";
//construct html table.
string emailBody = $"<h4>{vehicle.Year} {vehicle.Make} {vehicle.Model} #{vehicle.LicensePlate}</h4><br /><table style='width:100%'><tr><th style='padding:8px;'>Urgency</th><th style='padding:8px;'>Description</th></tr>";
foreach(ReminderRecordViewModel reminder in reminders)
{
emailBody += $"<tr><td style='padding:8px; text-align:center;'>{reminder.Urgency}</td><td style='padding:8px; text-align:center;'>{reminder.Description}</td></tr>";
}
emailBody += "</table>";
try
{
foreach (string emailAddress in emailAddresses)
{
SendEmail(emailAddress, emailSubject, emailBody, true, true);
}
return new OperationResponse { Success = true, Message = "Email Sent!" };
} catch (Exception ex)
{
return new OperationResponse { Success = false, Message = ex.Message };
}
}
private bool SendEmail(string emailTo, string emailSubject, string emailBody, bool isBodyHtml = false, bool useAsync = false) {
string to = emailTo;
string from = mailConfig.EmailFrom;
var server = mailConfig.EmailServer;
MailMessage message = new MailMessage(from, to);
message.Subject = emailSubject;
message.Body = emailBody;
message.IsBodyHtml = isBodyHtml;
SmtpClient client = new SmtpClient(server);
client.EnableSsl = mailConfig.UseSSL;
client.Port = mailConfig.Port;
client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password);
try
{
if (useAsync)
{
client.SendMailAsync(message, new CancellationToken());
}
else
{
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,7 @@
namespace CarCareTracker.Helper using CarCareTracker.Models;
using System.Globalization;
namespace CarCareTracker.Helper
{ {
/// <summary> /// <summary>
/// helper method for static vars /// helper method for static vars
@@ -7,5 +10,135 @@
{ {
public static string DbName = "data/cartracker.db"; public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json"; 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 "";
}
public static List<CostForVehicleByMonth> GetBaseLineCosts()
{
return new List<CostForVehicleByMonth>()
{
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(1), MonthId = 1, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(2), MonthId = 2, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(3), MonthId = 3, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(4), MonthId = 4, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(5), MonthId = 5, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(6), MonthId = 6, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(7), MonthId = 7, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(8), MonthId = 8, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(9), MonthId = 9, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(10), MonthId = 10, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(11), MonthId = 11, Cost = 0M},
new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(12), MonthId = 12, Cost = 0M}
};
}
public static List<CostForVehicleByMonth> GetBaseLineCostsNoMonthName()
{
return new List<CostForVehicleByMonth>()
{
new CostForVehicleByMonth { MonthId = 1, Cost = 0M},
new CostForVehicleByMonth {MonthId = 2, Cost = 0M},
new CostForVehicleByMonth {MonthId = 3, Cost = 0M},
new CostForVehicleByMonth {MonthId = 4, Cost = 0M},
new CostForVehicleByMonth {MonthId = 5, Cost = 0M},
new CostForVehicleByMonth {MonthId = 6, Cost = 0M},
new CostForVehicleByMonth {MonthId = 7, Cost = 0M},
new CostForVehicleByMonth {MonthId = 8, Cost = 0M},
new CostForVehicleByMonth {MonthId = 9, Cost = 0M},
new CostForVehicleByMonth { MonthId = 10, Cost = 0M},
new CostForVehicleByMonth { MonthId = 11, Cost = 0M},
new CostForVehicleByMonth { MonthId = 12, Cost = 0M}
};
}
public static ServiceRecord GenericToServiceRecord(GenericRecord input)
{
return new ServiceRecord
{
VehicleId = input.VehicleId,
Date = input.Date,
Description = input.Description,
Cost = input.Cost,
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes
};
}
public static CollisionRecord GenericToRepairRecord(GenericRecord input)
{
return new CollisionRecord
{
VehicleId = input.VehicleId,
Date = input.Date,
Description = input.Description,
Cost = input.Cost,
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes
};
}
public static UpgradeRecord GenericToUpgradeRecord(GenericRecord input)
{
return new UpgradeRecord
{
VehicleId = input.VehicleId,
Date = input.Date,
Description = input.Description,
Cost = input.Cost,
Mileage = input.Mileage,
Files = input.Files,
Notes = input.Notes
};
}
} }
} }

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 MIT License
Copyright (c) 2023 Hargata Softworks 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

@@ -0,0 +1,30 @@
using CarCareTracker.Models;
using CsvHelper.Configuration;
namespace CarCareTracker.MapProfile
{
public class ImportMapper: ClassMap<ImportModel>
{
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", "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.Authentication;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
@@ -14,17 +15,20 @@ namespace CarCareTracker.Middleware
{ {
private IHttpContextAccessor _httpContext; private IHttpContextAccessor _httpContext;
private IDataProtector _dataProtector; private IDataProtector _dataProtector;
private ILoginLogic _loginLogic;
private bool enableAuth; private bool enableAuth;
public Authen( public Authen(
IOptionsMonitor<AuthenticationSchemeOptions> options, IOptionsMonitor<AuthenticationSchemeOptions> options,
UrlEncoder encoder, UrlEncoder encoder,
ILoggerFactory logger, ILoggerFactory logger,
IConfiguration configuration, IConfiguration configuration,
ILoginLogic loginLogic,
IDataProtectionProvider securityProvider, IDataProtectionProvider securityProvider,
IHttpContextAccessor httpContext) : base(options, logger, encoder) IHttpContextAccessor httpContext) : base(options, logger, encoder)
{ {
_httpContext = httpContext; _httpContext = httpContext;
_dataProtector = securityProvider.CreateProtector("login"); _dataProtector = securityProvider.CreateProtector("login");
_loginLogic = loginLogic;
enableAuth = bool.Parse(configuration["EnableAuth"]); enableAuth = bool.Parse(configuration["EnableAuth"]);
} }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
@@ -35,7 +39,9 @@ namespace CarCareTracker.Middleware
var appIdentity = new ClaimsIdentity("Custom"); var appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim> 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); appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name); AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
@@ -45,11 +51,50 @@ namespace CarCareTracker.Middleware
{ {
//auth is enabled by user, we will have to authenticate the user via a ticket retrieved from the auth cookie. //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"]; 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."); return AuthenticateResult.Fail("Cookie is invalid or does not exist.");
} }
else if (!string.IsNullOrWhiteSpace(request_header))
{
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)
{
return AuthenticateResult.Fail("Invalid credentials");
}
else 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, 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. //decrypt the access token.
var decryptedCookie = _dataProtector.Unprotect(access_token); var decryptedCookie = _dataProtector.Unprotect(access_token);
@@ -62,30 +107,71 @@ namespace CarCareTracker.Middleware
//if cookie is expired //if cookie is expired
return AuthenticateResult.Fail("Expired credentials"); return AuthenticateResult.Fail("Expired credentials");
} }
else if (authCookie.Id == default || string.IsNullOrWhiteSpace(authCookie.UserName)) else if (authCookie.UserData is null || authCookie.UserData.Id == default || string.IsNullOrWhiteSpace(authCookie.UserData.UserName))
{ {
return AuthenticateResult.Fail("Corrupted credentials"); return AuthenticateResult.Fail("Corrupted credentials");
} }
else 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 appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim> var userIdentity = new List<Claim>
{ {
new(ClaimTypes.Name, authCookie.UserName) 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); appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name); AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
return AuthenticateResult.Success(ticket); return AuthenticateResult.Success(ticket);
} }
} }
} }
catch (Exception ex)
{
return AuthenticateResult.Fail("Corrupted credentials");
}
}
return AuthenticateResult.Fail("Invalid credentials"); return AuthenticateResult.Fail("Invalid credentials");
} }
} }
protected override Task HandleChallengeAsync(AuthenticationProperties properties) 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"); Response.Redirect("/Login/Index");
return Task.CompletedTask; 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

@@ -1,14 +1,6 @@
namespace CarCareTracker.Models namespace CarCareTracker.Models
{ {
public class CollisionRecord public class CollisionRecord: GenericRecord
{ {
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
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>();
} }
} }

View File

@@ -4,12 +4,13 @@
{ {
public int Id { get; set; } public int Id { get; set; }
public int VehicleId { 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 int Mileage { get; set; }
public string Description { get; set; } public string Description { get; set; }
public decimal Cost { get; set; } public decimal Cost { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); 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 }; } 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 Gallons { get; set; }
public decimal Cost { get; set; } public decimal Cost { get; set; }
public bool IsFillToFull { get; set; } = true; 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 List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
} }

View File

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

View File

@@ -3,6 +3,7 @@
public class GasRecordInputContainer public class GasRecordInputContainer
{ {
public bool UseKwh { get; set; } public bool UseKwh { get; set; }
public bool UseHours { get; set; }
public GasRecordInput GasRecord { get; set; } public GasRecordInput GasRecord { get; set; }
} }
} }

View File

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

View File

@@ -3,6 +3,7 @@
public class GasRecordViewModelContainer public class GasRecordViewModelContainer
{ {
public bool UseKwh { get; set; } public bool UseKwh { get; set; }
public bool UseHours { get; set; }
public List<GasRecordViewModel> GasRecords { get; set; } = new List<GasRecordViewModel>(); public List<GasRecordViewModel> GasRecords { get; set; } = new List<GasRecordViewModel>();
} }
} }

14
Models/GenericRecord.cs Normal file
View File

@@ -0,0 +1,14 @@
namespace CarCareTracker.Models
{
public class GenericRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
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>();
}
}

90
Models/ImportModel.cs Normal file
View File

@@ -0,0 +1,90 @@
namespace CarCareTracker.Models
{
/// <summary>
/// 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; }
public string FuelConsumed { get; set; }
public string Cost { get; set; }
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

@@ -1,34 +0,0 @@
namespace CarCareTracker.Models
{
/// <summary>
/// Import model used for importing Gas records.
/// </summary>
public class GasRecordImport
{
public DateTime Date { get; set; }
public int Odometer { get; set; }
public decimal FuelConsumed { get; set; }
public decimal Cost { get; set; }
}
/// <summary>
/// Import model used for importing Service and Repair records.
/// </summary>
public class ServiceRecordImport
{
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; }
}
/// <summary>
/// Import model used for importing tax records.
/// </summary>
public class TaxRecordImport
{
public DateTime Date { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public decimal Cost { get; set; }
}
}

View File

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

View File

@@ -4,6 +4,8 @@
{ {
public string UserName { get; set; } public string UserName { get; set; }
public string Password { get; set; } public string Password { get; set; }
public string EmailAddress { get; set; }
public string Token { get; set; }
public bool IsPersistent { get; set; } = false; 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,8 @@
{ {
public int Id { get; set; } public int Id { get; set; }
public int VehicleId { get; set; } public int VehicleId { get; set; }
public string Description { get; set; }
public string NoteText { get; set; } public string NoteText { get; set; }
public bool Pinned { 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 int Mileage { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Notes { 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 ReminderMetric Metric { get; set; } = ReminderMetric.Date;
} }
} }

View File

@@ -8,6 +8,9 @@
public int Mileage { get; set; } public int Mileage { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Notes { 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 ReminderMetric Metric { get; set; } = ReminderMetric.Date;
public ReminderRecord ToReminderRecord() { return new ReminderRecord { public ReminderRecord ToReminderRecord() { return new ReminderRecord {
Id = Id, Id = Id,
@@ -16,6 +19,9 @@
Mileage = Mileage, Mileage = Mileage,
Description = Description, Description = Description,
Metric = Metric, Metric = Metric,
IsRecurring = IsRecurring,
ReminderMileageInterval = ReminderMileageInterval,
ReminderMonthInterval = ReminderMonthInterval,
Notes = Notes }; } Notes = Notes }; }
} }
} }

View File

@@ -13,5 +13,9 @@
/// </summary> /// </summary>
public ReminderMetric Metric { get; set; } = ReminderMetric.Date; public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
public ReminderUrgency Urgency { get; set; } = ReminderUrgency.NotUrgent; 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 namespace CarCareTracker.Models
{ {
public class GasCostForVehicleByMonth public class CostForVehicleByMonth
{ {
public int MonthId { get; set; }
public string MonthName { get; set; } public string MonthName { get; set; }
public decimal Cost { get; set; } public decimal Cost { get; set; }
} }

View File

@@ -6,5 +6,6 @@
public decimal GasRecordSum { get; set; } public decimal GasRecordSum { get; set; }
public decimal TaxRecordSum { get; set; } public decimal TaxRecordSum { get; set; }
public decimal CollisionRecordSum { get; set; } public decimal CollisionRecordSum { get; set; }
public decimal UpgradeRecordSum { get; set; }
} }
} }

View File

@@ -0,0 +1,16 @@
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; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

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

@@ -1,14 +1,6 @@
namespace CarCareTracker.Models namespace CarCareTracker.Models
{ {
public class ServiceRecord public class ServiceRecord: GenericRecord
{ {
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
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>();
} }
} }

View File

@@ -4,12 +4,13 @@
{ {
public int Id { get; set; } public int Id { get; set; }
public int VehicleId { 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 int Mileage { get; set; }
public string Description { get; set; } public string Description { get; set; }
public decimal Cost { get; set; } public decimal Cost { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); 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 }; } 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

@@ -8,6 +8,8 @@
public string Description { get; set; } public string Description { get; set; }
public decimal Cost { get; set; } public decimal Cost { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.OneYear;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
} }
} }

View File

@@ -4,11 +4,22 @@
{ {
public int Id { get; set; } public int Id { get; set; }
public int VehicleId { 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 string Description { get; set; }
public decimal Cost { get; set; } public decimal Cost { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.ThreeMonths;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>(); public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public TaxRecord ToTaxRecord() { return new TaxRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Description = Description, Notes = Notes, Files = Files }; } public TaxRecord ToTaxRecord() { return new TaxRecord {
Id = Id,
VehicleId = VehicleId,
Date = DateTime.Parse(Date),
Cost = Cost,
Description = Description,
Notes = Notes,
IsRecurring = IsRecurring,
RecurringInterval = RecurringInterval,
Files = Files }; }
} }
} }

View File

@@ -0,0 +1,6 @@
namespace CarCareTracker.Models
{
public class UpgradeRecord: GenericRecord
{
}
}

View File

@@ -0,0 +1,16 @@
namespace CarCareTracker.Models
{
public class UpgradeRecordInput
{
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 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

@@ -7,7 +7,23 @@
public bool UseMPG { get; set; } public bool UseMPG { get; set; }
public bool UseDescending { get; set; } public bool UseDescending { get; set; }
public bool EnableAuth { get; set; } public bool EnableAuth { get; set; }
public bool HideZero { get; set; }
public bool UseUKMPG {get;set;}
public bool UseThreeDecimalGasCost { get; set; }
public bool UseMarkDownOnSavedNotes { get; set; }
public bool EnableAutoReminderRefresh { get; set; }
public bool EnableAutoOdometerInsert { get; set; }
public string UserNameHash { get; set; } public string UserNameHash { get; set; }
public string UserPasswordHash { 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

@@ -9,5 +9,6 @@
public string Model { get; set; } public string Model { get; set; }
public string LicensePlate { get; set; } public string LicensePlate { get; set; }
public bool IsElectric { get; set; } = false; public bool IsElectric { get; set; } = false;
public bool UseHours { get; set; } = false;
} }
} }

View File

@@ -1,6 +1,7 @@
using CarCareTracker.External.Implementations; using CarCareTracker.External.Implementations;
using CarCareTracker.External.Interfaces; using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper; using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Middleware; using CarCareTracker.Middleware;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -16,7 +17,26 @@ builder.Services.AddSingleton<IGasRecordDataAccess, GasRecordDataAccess>();
builder.Services.AddSingleton<ICollisionRecordDataAccess, CollisionRecordDataAccess>(); builder.Services.AddSingleton<ICollisionRecordDataAccess, CollisionRecordDataAccess>();
builder.Services.AddSingleton<ITaxRecordDataAccess, TaxRecordDataAccess>(); builder.Services.AddSingleton<ITaxRecordDataAccess, TaxRecordDataAccess>();
builder.Services.AddSingleton<IReminderRecordDataAccess, ReminderRecordDataAccess>(); 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<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")) if (!Directory.Exists("data"))
{ {

View File

@@ -2,9 +2,21 @@
A self-hosted, open-source vehicle service records and maintainence tracker. A self-hosted, open-source vehicle service records and maintainence tracker.
Visit our website: https://lubelogger.com
Support this project on Patreon: https://patreon.com/LubeLogger
## Why ## Why
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintainence. 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>
## Demo
Try it out before you download it! The live demo resets every 20 minutes.
[Live Demo](https://demo.lubelogger.com) Login using username "test" and password "1234"
## Dependencies ## Dependencies
- Bootstrap - Bootstrap
- LiteDB - LiteDB
@@ -12,14 +24,22 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
- SweetAlert2 - SweetAlert2
- CsvHelper - CsvHelper
- Chart.js - Chart.js
- Drawdown
## Docker Setup (Recommended) ## 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. You can also setup SMTP Config here.
4. If using traefik, use docker-compose.traefik.yml
5. Run `docker-compose up`
## Docker Setup (Manual Build)
1. Install Docker 1. Install Docker
2. Clone this repo 2. Clone this repo
3. CHECK culture in Dockerfile, 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 .` 4. Run `docker build -t lubelogger -f Dockerfile .`
5. CHECK docker-compose.yml and make sure the mounting directories look correct. 5. CHECK docker-compose.yml and make sure the mounting directories look correct.
6. If not using traefik, use docker-compose-notraefik.yml 6. If using traefik, use docker-compose.traefik.yml
7. Run `docker-compose up` 7. Run `docker-compose up`
## Additional Docker Instructions ## Additional Docker Instructions

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

@@ -0,0 +1,307 @@
@{
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/vehicle/reminders/send</code>
</div>
<div class="col-3">
Send reminder emails out to collaborators based on specified urgency.
</div>
<div class="col-3">
(must be root user)<br />
urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue]
</div>
</div>
<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 @model string
@{ @{
@@ -9,24 +10,60 @@
@section Scripts { @section Scripts {
<script src="~/js/garage.js" asp-append-version="true"></script> <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="container">
<div class="row mt-2"> <div class="row mt-2">
<div class="d-flex justify-content-center"> <div class="d-flex lubelogger-navbar">
<img src="/defaults/lubelogger_logo.png" /> <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>
</div> </div>
<hr /> <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"> <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> <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>
<li class="nav-item ms-auto" role="presentation"> <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> <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> </li>
@if (enableAuth) @if (User.IsInRole("CookieAuth"))
{ {
<li class="nav-item"> <li class="nav-item dropdown" role="presentation">
<button class="nav-link" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>Logout</button> <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> </li>
} }
</ul> </ul>
@@ -38,7 +75,6 @@
</div> </div>
</div> </div>
<div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0"> <div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0">
</div> </div>
</div> </div>
</div> </div>
@@ -46,11 +82,10 @@
<div class="modal fade" id="addVehicleModal" tabindex="-1" role="dialog"> <div class="modal fade" id="addVehicleModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content" id="addVehicleModalContent"> <div class="modal-content" id="addVehicleModalContent">
</div> </div>
</div> </div>
</div> </div>
<script> <script>
loadGarage(); loadGarage();
loadSettings(); bindWindowResize();
</script> </script>

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