├── .gitignore
├── .idea
├── jsLibraryMappings.xml
├── misc.xml
├── modules.xml
├── restsheet.iml
├── vcs.xml
└── workspace.xml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── apiRoutes.js
├── app.js
├── assets
├── google-logo.svg
├── logo.svg
└── plus-outline.svg
├── bin
├── pm2.config.js
└── start-stein.js
├── controllers
├── addStorage.js
├── appendRow.js
├── checkAuth.js
├── deleteRow.js
├── editRow.js
├── getGoogleAuthorization.js
├── index.js
├── logInMiddleware.js
├── logInRedirect.js
├── logout.js
├── objectDoesMatch.js
├── passportAuthCallback.js
├── readSheet.js
├── removeStorage.js
├── retrieveSheet.js
├── search.js
├── searchSheet.js
├── toggleSheetAuth.js
├── updateRequestCount.js
└── views
│ ├── dashboard.js
│ ├── initialLogin.js
│ └── login.js
├── helpers
├── authentication
│ ├── configuration.js
│ └── passportInit.js
└── db.js
├── index.js
├── interfaceRoutes.js
├── models
├── admin.js
├── storage.js
└── user.js
├── package.json
├── views
├── dashboard.ejs
├── googleLogin.ejs
├── header.ejs
└── initialLogin.ejs
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .env
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/restsheet.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | NODE_EN
108 | errorhandler
109 | "error
110 | we ma
111 | console.
112 | env
113 | dotenv
114 | dotenc
115 | process.env.
116 | ./
117 | [^require]."/
118 | now
119 | oauth2clien
120 | return
121 | passport-oauth2-refre
122 | googleOAuth2Client.
123 | googleAuthLib.OAuth2Client
124 | mongoose.connec
125 | schema
126 | mongoose =
127 | mongoose.conn
128 | mongoose
129 | log`
130 | log
131 | monthEnd
132 | retr
133 | 30
134 | process
135 | develop
136 | produ
137 |
138 |
139 | req.body
140 |
141 |
142 | C:\Users\HOME-PC.HOME\Desktop\webdev\restsheet\
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | true
215 |
216 | true
217 | true
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 | AngularJS
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 | 1505403383195
301 |
302 |
303 | 1505403383195
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 | 1541836384588
425 |
426 |
427 |
428 | 1541836384588
429 |
430 |
431 | 1541962485609
432 |
433 |
434 |
435 | 1541962485609
436 |
437 |
438 | 1542047214019
439 |
440 |
441 |
442 | 1542047214019
443 |
444 |
445 | 1542102877347
446 |
447 |
448 |
449 | 1542102877347
450 |
451 |
452 | 1542173939328
453 |
454 |
455 |
456 | 1542173939328
457 |
458 |
459 | 1542179718522
460 |
461 |
462 |
463 | 1542179718522
464 |
465 |
466 | 1554315725343
467 |
468 |
469 |
470 | 1554315725343
471 |
472 |
473 | 1554320494366
474 |
475 |
476 |
477 | 1554320494366
478 |
479 |
480 | 1554557184324
481 |
482 |
483 |
484 | 1554557184324
485 |
486 |
487 | 1554583165970
488 |
489 |
490 |
491 | 1554583165971
492 |
493 |
494 | 1554708536308
495 |
496 |
497 |
498 | 1554708536308
499 |
500 |
501 | 1554757966917
502 |
503 |
504 |
505 | 1554757966917
506 |
507 |
508 | 1554758852989
509 |
510 |
511 |
512 | 1554758852989
513 |
514 |
515 | 1555566025248
516 |
517 |
518 |
519 | 1555566025248
520 |
521 |
522 | 1555618840755
523 |
524 |
525 |
526 | 1555618840755
527 |
528 |
529 | 1555790153830
530 |
531 |
532 |
533 | 1555790153831
534 |
535 |
536 | 1556608477669
537 |
538 |
539 |
540 | 1556608477669
541 |
542 |
543 | 1556657163868
544 |
545 |
546 |
547 | 1556657163868
548 |
549 |
550 | 1556710418939
551 |
552 |
553 |
554 | 1556710418939
555 |
556 |
557 | 1556911994093
558 |
559 |
560 |
561 | 1556911994093
562 |
563 |
564 | 1557125914763
565 |
566 |
567 |
568 | 1557125914763
569 |
570 |
571 | 1557553708119
572 |
573 |
574 |
575 | 1557553708119
576 |
577 |
578 | 1557557467678
579 |
580 |
581 |
582 | 1557557467678
583 |
584 |
585 | 1558373655678
586 |
587 |
588 |
589 | 1558373655678
590 |
591 |
592 | 1558431345484
593 |
594 |
595 |
596 | 1558431345485
597 |
598 |
599 | 1558522030001
600 |
601 |
602 |
603 | 1558522030001
604 |
605 |
606 | 1558537018008
607 |
608 |
609 |
610 | 1558537018008
611 |
612 |
613 | 1558593148648
614 |
615 |
616 |
617 | 1558593148648
618 |
619 |
620 | 1558594781978
621 |
622 |
623 |
624 | 1558594781978
625 |
626 |
627 | 1558608605193
628 |
629 |
630 |
631 | 1558608605193
632 |
633 |
634 | 1558628090505
635 |
636 |
637 |
638 | 1558628090505
639 |
640 |
641 | 1559151113985
642 |
643 |
644 |
645 | 1559151113985
646 |
647 |
648 | 1559385664109
649 |
650 |
651 |
652 | 1559385664109
653 |
654 |
655 | 1559386251811
656 |
657 |
658 |
659 | 1559386251811
660 |
661 |
662 | 1559804725644
663 |
664 |
665 |
666 | 1559804725646
667 |
668 |
669 | 1559842648401
670 |
671 |
672 |
673 | 1559842648401
674 |
675 |
676 | 1559843340536
677 |
678 |
679 |
680 | 1559843340536
681 |
682 |
683 | 1559846148049
684 |
685 |
686 |
687 | 1559846148049
688 |
689 |
690 | 1559924623350
691 |
692 |
693 |
694 | 1559924623350
695 |
696 |
697 | 1559929437966
698 |
699 |
700 |
701 | 1559929437966
702 |
703 |
704 | 1560014360323
705 |
706 |
707 |
708 | 1560014360323
709 |
710 |
711 | 1560447115422
712 |
713 |
714 |
715 | 1560447115424
716 |
717 |
718 | 1560525750759
719 |
720 |
721 |
722 | 1560525750759
723 |
724 |
725 | 1560538401847
726 |
727 |
728 |
729 | 1560538401848
730 |
731 |
732 | 1560694545815
733 |
734 |
735 |
736 | 1560694545815
737 |
738 |
739 | 1561378148242
740 |
741 |
742 |
743 | 1561378148243
744 |
745 |
746 | 1561788796403
747 |
748 |
749 |
750 | 1561788796403
751 |
752 |
753 | 1562607492374
754 |
755 |
756 |
757 | 1562607492375
758 |
759 |
760 | 1565108489531
761 |
762 |
763 |
764 | 1565108489532
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 |
785 |
786 |
787 |
788 |
789 |
790 |
791 |
792 |
793 |
794 |
795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 |
814 |
815 |
816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
833 |
834 |
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 |
859 |
860 |
861 |
862 |
863 |
864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
874 |
875 |
876 |
877 |
878 |
879 |
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 |
893 |
894 |
895 |
896 |
897 |
898 |
899 |
900 |
901 |
902 |
903 |
904 |
905 |
906 |
907 |
908 |
909 |
910 |
911 |
912 |
913 |
914 |
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 |
923 |
924 |
925 |
926 |
927 |
928 |
929 |
930 |
931 |
932 |
933 |
934 |
935 |
936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 |
944 |
945 |
946 |
947 |
948 |
949 |
950 |
951 |
952 |
953 |
954 |
955 |
956 |
957 |
958 |
959 |
960 |
961 |
962 |
963 |
964 |
965 |
966 |
967 |
968 |
969 |
970 |
971 |
972 |
973 |
974 |
975 |
976 |
977 |
978 |
979 |
980 |
981 |
982 |
983 |
984 |
985 |
986 |
987 |
988 |
989 |
990 |
991 |
992 |
993 |
994 |
995 |
996 |
997 |
998 |
999 |
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 |
1008 |
1009 |
1010 |
1011 |
1012 |
1013 |
1014 |
1015 |
1016 |
1017 |
1018 |
1019 |
1020 |
1021 |
1022 |
1023 |
1024 |
1025 |
1026 |
1027 |
1028 |
1029 |
1030 |
1031 |
1032 |
1033 |
1034 |
1035 |
1036 |
1037 |
1038 |
1039 |
1040 |
1041 |
1042 |
1043 |
1044 |
1045 |
1046 |
1047 |
1048 |
1049 |
1050 |
1051 |
1052 |
1053 |
1054 |
1055 |
1056 |
1057 |
1058 |
1059 |
1060 |
1061 |
1062 |
1063 |
1064 |
1065 |
1066 |
1067 |
1068 |
1069 |
1070 |
1071 |
1072 |
1073 |
1074 |
1075 |
1076 |
1077 |
1078 |
1079 |
1080 |
1081 |
1082 |
1083 |
1084 |
1085 |
1086 |
1087 |
1088 |
1089 |
1090 |
1091 |
1092 |
1093 |
1094 |
1095 |
1096 |
1097 |
1098 |
1099 |
1100 |
1101 |
1102 |
1103 |
1104 |
1105 |
1106 |
1107 |
1108 |
1109 |
1110 |
1111 |
1112 |
1113 |
1114 |
1115 |
1116 |
1117 |
1118 |
1119 |
1120 |
1121 |
1122 |
1123 |
1124 |
1125 |
1126 |
1127 |
1128 |
1129 |
1130 |
1131 |
1132 |
1133 |
1134 |
1135 |
1136 |
1137 |
1138 |
1139 |
1140 |
1141 |
1142 |
1143 |
1144 |
1145 |
1146 |
1147 |
1148 |
1149 |
1150 |
1151 |
1152 |
1153 |
1154 |
1155 |
1156 |
1157 |
1158 |
1159 |
1160 |
1161 |
1162 |
1163 |
1164 |
1165 |
1166 |
1167 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at shivensinha4@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | First off, thank you for considering contributing to Stein.
4 |
5 | Stein is an open source project and we would love to receive contributions from our community — you!
6 |
7 | There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, creating tests, submitting bug reports and feature requests, or writing code which can be incorporated into Stein itself. We greatly value all contributions.
8 |
9 | ## Working on Stein
10 |
11 | The process of setting up Stein for development isn't different. To run the server in development mode, just run
12 |
13 | ```
14 | $ npm run dev
15 | ```
16 |
17 | This starts the server with Nodemon, to restart the server automatically on changes. It also indicates to server to switch to the development environment.
18 |
19 | As a result, certain security measures, such as allowing cookies only over HTTPS, are disabled for convenience.
20 |
21 | ## Branching Guide
22 |
23 | The `master` branch contains the latest changes, and is not guaranteed to be stable. It represents WIP for the next release. Stable versions are tagged using SemVer.
24 |
25 | First you'll need to fork the repository.
26 |
27 | On your fork, create a branch and work on it. Submit your pull requests to the master, if any instruction is not explicitly specified for your changes by the maintainers.
28 |
29 | ## Commit messages
30 |
31 | We have a handful of simple standards for commit messages to keep complete track of changes.
32 |
33 | - **1st line**: Max 90 character summary written in past tense
34 | - **2nd line**: [Always blank]
35 | - **3rd line**: refs/closes #000 or no issue
36 | - **4th line**: A list of extra details or anything else worth mentioning
37 |
38 | ## Pull Requests
39 |
40 | Please provide plenty of context and reasoning around your changes, to help us merge quickly. Closing an already open issue is our preferred workflow. If your PR gets out of date, we may ask you to rebase as you are more familiar with your changes than we will be.
41 |
42 | ## Tests
43 |
44 | The automated tests are currently in development. Changes have to be manually verified at the moment ⚒
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Shiven Sinha
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Ship fast and manage your data with ease. Connect to Google Sheets.
10 |
11 |
12 | Kick off projects, design custom Google Forms, and manage your content in a familiar interface.
13 |
14 | Power your project with the fully open source Stein.
15 |
16 |
17 |
18 | SteinHQ.com |
19 | Documentation
20 |
21 |
22 | ## Setup
23 |
24 | There are two ways you can get started with the Stein API:
25 |
26 | - Use the [hosted service](https://steinhq.com) to get a free & reliable API in a couple of clicks.
27 | - Self-host an instance of Stein. Find the related documentation [here](https://docs.steinhq.com/hosting-introduction).
28 |
29 | ## Examples
30 |
31 | Details and examples on the complete functionality can be found in the [documentation](https://docs.steinhq.com). Here are a few to get you started!
32 |
33 | All the data and the errors are communicated using JSON. You may perform common operations on your sheets such as read, search, write, etc.
34 |
35 | Structure your sheet as shown below, with the first row populated with column names.
36 |
37 |
38 |
39 |
40 |
41 | A read operation on the sheet will return an array of the rows.
42 |
43 | ```
44 | [
45 | {
46 | "title":"Why the Best Things in Life Can’t Be Planned",
47 | "content":"Thales of Miletus, considered ...",
48 | "link":"https://medium.com/...",
49 | "author":"Zat Rana"
50 | },
51 | ...
52 | ]
53 | ```
54 |
55 | ### Using the core API
56 |
57 | Since Stein is a REST API, there are no limitations as to which languages you can use. For this example, let's use the [Stein JavaScript Client](https://github.com/SteinHQ/JS-Client/) to obtain the data:
58 |
59 | ```javascript
60 | const SteinStore = require("stein-js-client");
61 |
62 | // Instantiate store for spreadsheet API URL
63 | const store = new SteinStore(
64 | "https://api.steinhq.com/v1/storages/5cc158079ec99a2f484dcb40"
65 | );
66 |
67 | // Read Sheet1 of spreadsheet
68 | store.read("Sheet1").then(data => {
69 | console.log(data);
70 | });
71 |
72 | // Logs object like ↓
73 | // [{title:"Why the Best Things in Life Can’t Be Planned",content:"Thales of Miletus, considered ...",link:"https://medium.com/...",author:"Zat Rana"}, {...}, ...]
74 | ```
75 |
76 | ### Using plain HTML
77 |
78 | To simply display the data on a webpage, we don't even need JS! Using [Stein Expedite](https://docs.steinhq.com/expedite-introduction),
79 |
80 | ```html
81 |
82 |
85 |
86 |
{{title}}
87 |
By {{author}}
88 |
89 | {{content}}
90 |
91 |
Read on Medium
92 |
93 |
94 | ```
95 |
96 | Here's a minimal output of the above code.
97 |
98 |
99 |
100 |
101 |
102 | ## Contributing
103 |
104 | Stein is completely open-source software, and the best part about structuring it this way is that everyone gets to own, understand, and improve it.
105 |
106 | The main purpose of this repository is to continue to evolve the Stein Core API, making it faster and easier to use. We are grateful to the community for contributing fixes and improvements.
107 |
108 | ### [Code Of Conduct](./CODE_OF_CONDUCT.md)
109 |
110 | All participants are expected to adhere to the Code of Conduct.
111 |
112 | ### [Contributing Guidelines](./CONTRIBUTING.md)
113 |
114 | Read our contributing guide to learn about what contributions we are looking for and how to propose them.
115 |
116 | ## Built With
117 |
118 | - [Node.js](https://github.com/nodejs/node) + [Express](https://github.com/expressjs/express): The back-end API is an Express app. It responds to requests RESTfully in JSON.
119 | - [MongoDB](https://github.com/mongodb/mongo): The store for data Stein needs to function (OAuth tokens, API lists, etc.)
120 |
121 | ## Partners
122 |
123 | Stein officially partners with the following companies, and thanks them for their support!
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | For any queries regarding partnerships, reach out to [SteinHQ](mailto:hello@steinhq.com).
136 |
137 | ## License
138 |
139 | The Stein core project is [MIT licensed](./LICENSE).
140 |
--------------------------------------------------------------------------------
/apiRoutes.js:
--------------------------------------------------------------------------------
1 | const controllers = require("./controllers");
2 |
3 | module.exports = app => {
4 | /*
5 | -----
6 | Storage Public API Routes
7 | -----
8 | */
9 |
10 | /*
11 | Route for giving JSON results for read & search
12 | URL structure: /?search={"key":"value", ...}
13 | */
14 | app.get("/v1/storages/:id/:sheet", controllers.storage.readSheet);
15 |
16 | /*
17 | Append a new row
18 | POST body: [{row object}, ...]
19 | */
20 | app.post("/v1/storages/:id/:sheet", controllers.storage.appendRow);
21 |
22 | /*
23 | Edit row(s)
24 | POST body -> {
25 | condition: {column:value, ...},
26 | set: {column: value, ...},
27 | limit: INTEGER (optional)
28 | }
29 | */
30 | app.put("/v1/storages/:id/:sheet", controllers.storage.editRow);
31 |
32 | /*
33 | Delete row(s)
34 | DELETE body -> {
35 | condition: {column: value, ...},
36 | limit: INTEGER (optional)
37 | }
38 | */
39 | app.delete("/v1/storages/:id/:sheet", controllers.storage.deleteRow);
40 | };
41 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express"),
2 | routes = require("./apiRoutes"),
3 | mongoose = require("./helpers/db"),
4 | path = require("path"),
5 | dotenv = require("dotenv"),
6 | bodyParser = require("body-parser"),
7 | cors = require("cors");
8 |
9 | dotenv.config({ path: path.resolve(__dirname, ".env") });
10 |
11 | const app = express();
12 |
13 | if (process.env.NODE_ENV === "development") {
14 | const responseTime = require("response-time");
15 | app.use(responseTime((req, res, time) => console.log("Took time", time)));
16 | }
17 |
18 | app.use(cors());
19 | app.use(bodyParser.json({ type: () => true }));
20 | app.use(bodyParser.urlencoded({ extended: false }));
21 |
22 | // Routes
23 | routes(app);
24 |
25 | app.use((error, req, res, next) => {
26 | console.error(error);
27 |
28 | // If error has status code specified
29 | if (error.code) {
30 | let message = { error: error.message };
31 | // In case of errors returned by Google Sheets API
32 | if (error.errors) {
33 | message.error = error.errors[0].message;
34 | }
35 |
36 | return res.status(error.code).json(message);
37 | }
38 |
39 | // If error has only message or has no details
40 | res.status(500).json({ error: error.message || "An error occurred" });
41 | });
42 |
43 | module.exports.app = app;
44 | module.exports.mongoose = mongoose;
45 |
--------------------------------------------------------------------------------
/assets/google-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
--------------------------------------------------------------------------------
/assets/plus-outline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | plus
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/bin/pm2.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | name: "stein-core-server",
5 | script: path.resolve(__dirname, "../index.js"),
6 | instances: "max",
7 | output: path.resolve(__dirname, "../out.log"),
8 | error: path.resolve(__dirname, "../error.log"),
9 | exec_mode: "cluster",
10 | env_production: {
11 | NODE_ENV: "production"
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/bin/start-stein.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const pm2 = require("pm2"),
4 | config = require("./pm2.config");
5 |
6 | let [, , ...args] = process.argv;
7 |
8 | function errBack(err, apps) {
9 | if (err) {
10 | console.error(err);
11 | throw err;
12 | }
13 |
14 | exitHandler();
15 | }
16 |
17 | pm2.connect(function(err) {
18 | if (err) {
19 | console.error(err);
20 | return process.exit(2);
21 | }
22 |
23 | if (args[0] === "start") {
24 | pm2.start(config, errBack);
25 | } else if (args[0] === "stop") {
26 | pm2.killDaemon(errBack);
27 | }
28 | });
29 |
30 | function exitHandler(options, exitCode) {
31 | pm2.disconnect();
32 | process.exit();
33 | }
34 |
35 | //do something when app is closing
36 | process.on("exit", exitHandler);
37 |
38 | //catches ctrl+c event
39 | process.on("SIGINT", exitHandler);
40 |
41 | // catches "kill pid"
42 | process.on("SIGUSR1", exitHandler);
43 | process.on("SIGUSR2", exitHandler);
44 |
45 | //catches uncaught exceptions
46 | process.on("uncaughtException", exitHandler);
47 |
--------------------------------------------------------------------------------
/controllers/addStorage.js:
--------------------------------------------------------------------------------
1 | // Add a sheet to be used as a storage
2 | const Storage = require("../models/storage"),
3 | googleAuthLib = require("google-auth-library"),
4 | googleOAuthConfig = require("../helpers/authentication/configuration").google,
5 | { google } = require("googleapis");
6 |
7 | module.exports = (req, res, next) => {
8 | const sheets = google.sheets("v4"),
9 | oauth2Client = new googleAuthLib.OAuth2Client(
10 | googleOAuthConfig.clientID,
11 | googleOAuthConfig.clientSecret,
12 | googleOAuthConfig.callbackURL
13 | );
14 |
15 | oauth2Client.credentials = {
16 | access_token: req.user.accessToken,
17 | refresh_token: req.user.refreshToken
18 | };
19 |
20 | return sheets.spreadsheets
21 | .get({
22 | auth: oauth2Client,
23 | spreadsheetId: req.body.id
24 | })
25 | .then(response => {
26 | // Add a storage document
27 | const store = new Storage();
28 | store.googleId = req.body.id;
29 | store.dateCreated = Date.now();
30 | store.userGoogleId = req.user.googleId;
31 | store.title = response.data.properties.title;
32 | return store.save({ new: true });
33 | })
34 | .then(store => {
35 | // Add sheet to user's list
36 | const userAssignment = {};
37 | userAssignment[store._id] = store.googleId;
38 | req.user.storages.push(store._id);
39 | return req.user.save({ new: true });
40 | })
41 | .catch(err => {
42 | next(err);
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/controllers/appendRow.js:
--------------------------------------------------------------------------------
1 | // Append a row to the sheet
2 | const Storage = require("../models/storage"),
3 | authConfig = require("../helpers/authentication/configuration"),
4 | googleAuthLib = require("google-auth-library"),
5 | { google } = require("googleapis"),
6 | User = require("../models/user");
7 |
8 | module.exports = (req, res, next) => {
9 | const sheets = google.sheets("v4"),
10 | oAuth2Client = new googleAuthLib.OAuth2Client(
11 | authConfig.google.clientID,
12 | authConfig.google.clientSecret
13 | );
14 |
15 | // Find spreadsheet in storage, then refresh the token of owner
16 | Storage.findById(req.params.id)
17 | .then(result => {
18 | // Refresh user's access token if necessary
19 | User.refreshAccessCode(result.userGoogleId).then(validatedUser => {
20 | // Pass the valid user and result to the function which reads and parses the sheets, so as to supply the googleId of the sheet with the appropriate OAuth details.
21 | appendRow(validatedUser, result);
22 | });
23 | })
24 | .catch(err => {
25 | res.send(err);
26 | });
27 |
28 | const appendRow = (validatedUser, queriedSheetDetails) => {
29 | oAuth2Client.setCredentials({
30 | access_token: validatedUser.accessToken,
31 | refresh_token: validatedUser.refreshToken
32 | });
33 |
34 | // Get the keys
35 | sheets.spreadsheets.values
36 | .get({
37 | auth: oAuth2Client,
38 | spreadsheetId: queriedSheetDetails.googleId,
39 | range: req.params.sheet + "!1:1"
40 | })
41 | .then(response => {
42 | const keyArray = response.data.values[0];
43 |
44 | // Sort the query as per key
45 | const toAppend = req.body,
46 | allRows = [];
47 |
48 | for (let index = 0; index < toAppend.length; index++) {
49 | const currentRowObj = toAppend[index],
50 | currentRow = [];
51 |
52 | // iterate over the keys in order, add the respective values to currentRow
53 | for (let keyCount = 0; keyCount < keyArray.length; keyCount++) {
54 | const currentKey = keyArray[keyCount];
55 | currentRow.push(currentRowObj[currentKey]); // Push the respective value to the row array
56 | }
57 |
58 | allRows.push(currentRow); // Add this row's array to allRows
59 | }
60 |
61 | // Now finally append it to the sheet
62 | const body = { values: allRows };
63 | sheets.spreadsheets.values
64 | .append({
65 | auth: oAuth2Client,
66 | spreadsheetId: queriedSheetDetails.googleId,
67 | range: req.params.sheet,
68 | valueInputOption: "RAW",
69 | resource: body
70 | })
71 | .then(response => {
72 | res.json({ updatedRange: response.data.updates.updatedRange });
73 | })
74 | .catch(err => {
75 | next(err);
76 | });
77 | })
78 | .catch(next);
79 | };
80 | };
81 |
--------------------------------------------------------------------------------
/controllers/checkAuth.js:
--------------------------------------------------------------------------------
1 | const Storage = require("../models/storage"),
2 | auth = require("basic-auth");
3 |
4 | const apiNotFoundError = { code: 404, message: "API does not exist" };
5 |
6 | module.exports = (req, res, next) => {
7 | getStorage(req, res)
8 | .then(result => {
9 | // If sheet doesn't exist, send 404
10 | if (!result) {
11 | return next(apiNotFoundError);
12 | }
13 |
14 | // First check if Basic HTTP Auth is enabled on sheet
15 | if (typeof result.basicHttpAuth !== "string") {
16 | res.locals.sheetIdDbResult = result;
17 | return next();
18 | }
19 | // Store correct credentials as variables
20 | const correctRaw = result.basicHttpAuth.split(":"),
21 | correctName = correctRaw[0],
22 | correctPassword = correctRaw[1];
23 |
24 | // Look for simple http credentials in req
25 | const received = auth(req);
26 |
27 | // Test the credentials
28 | if (
29 | received &&
30 | received.name === correctName &&
31 | received.pass === correctPassword
32 | ) {
33 | res.locals.sheetIdDbResult = result;
34 | return next();
35 | } else {
36 | res.status(401).json({ error: "Unauthorized" });
37 | }
38 |
39 | // Now do other auth, like IP and all
40 | })
41 | .catch(err => {
42 | next(apiNotFoundError);
43 | console.error(err);
44 | });
45 | };
46 |
47 | function getStorage(req, res) {
48 | // If the storage had been already fetched and set by some other middleware, no need to do that again
49 | if (res.locals.sheetIdDbResult) {
50 | return Promise.resolve(res.locals.sheetIdDbResult);
51 | }
52 |
53 | return Storage.findById(req.params.id);
54 | }
55 |
--------------------------------------------------------------------------------
/controllers/deleteRow.js:
--------------------------------------------------------------------------------
1 | // deletes rows which match a condition
2 | const googleAuthLib = require("google-auth-library"),
3 | { google } = require("googleapis"),
4 | objectDoesMatch = require("./objectDoesMatch"),
5 | authConfig = require("../helpers/authentication/configuration"),
6 | retrieveSheet = require("./retrieveSheet"),
7 | User = require("../models/user");
8 |
9 | module.exports = (req, res, next) => {
10 | // Get the full response of sheet and then delete
11 | User.refreshAccessCode(res.locals.sheetIdDbResult.userGoogleId).then(
12 | validUser => {
13 | const query = { sheet: req.params.sheet, limit: req.query.limit };
14 |
15 | // Pass the valid user and result to the function which reads and parses the sheets, so as to supply the googleId of the sheet with the appropriate OAuth details.
16 | retrieveSheet(validUser, res.locals.sheetIdDbResult, query, res.locals.rowLimit)
17 | .then(data => {
18 | getIdAndForward(data, req, res, next);
19 | })
20 | .catch(next);
21 | }
22 | );
23 | };
24 |
25 | // Gets google id and valid user and forwards it for further processing
26 | const getIdAndForward = (parsedSheet, req, res, errorHandler) => {
27 | // Refresh user's access token if necessary
28 | User.refreshAccessCode(res.locals.sheetIdDbResult.userGoogleId).then(
29 | validUser => {
30 | // Pass the valid user and result to the function which reads and parses the sheets, so as to supply the googleId of the sheet with the appropriate OAuth details.
31 | afterSheetGoogleId(
32 | validUser,
33 | res.locals.sheetIdDbResult,
34 | parsedSheet,
35 | req,
36 | res,
37 | errorHandler
38 | );
39 | }
40 | );
41 | };
42 |
43 | // After getting the google id and valid user, make further changes
44 | const afterSheetGoogleId = (
45 | validatedUser,
46 | sheetDbResult,
47 | parsedSheet,
48 | req,
49 | res,
50 | errorHandler
51 | ) => {
52 | // User data init
53 | const sheets = google.sheets("v4"),
54 | oAuth2Client = new googleAuthLib.OAuth2Client(
55 | authConfig.google.clientID,
56 | authConfig.google.clientSecret
57 | );
58 |
59 | oAuth2Client.setCredentials({
60 | access_token: validatedUser.accessToken,
61 | refresh_token: validatedUser.refreshToken
62 | });
63 |
64 | // POSTed data
65 | const condition = req.body.condition,
66 | limit = req.body.limit;
67 |
68 | // Loop through all rows, check if it matches. If it does, add the row number (range) to allRanges
69 | const allRanges = [];
70 | for (let rowCount = 0; rowCount < parsedSheet.length; rowCount++) {
71 | const currentRow = parsedSheet[rowCount];
72 | // If row passes condition, add the row number to allRanges
73 | if (objectDoesMatch(condition, currentRow)) {
74 | allRanges.push(rowCount + 1 + 1); // Added one due to one based indexing, and one due to the first row consisting of keys
75 | }
76 | }
77 |
78 | const sheetQueryBody = {
79 | auth: oAuth2Client,
80 | spreadsheetId: sheetDbResult.googleId,
81 | ranges: []
82 | };
83 |
84 | // Create appropriate ranges in format -> Sheet2!1:1
85 | for (let rangeIndex = 0; rangeIndex < allRanges.length; rangeIndex++) {
86 | const currentRange = allRanges[rangeIndex],
87 | parsedRange = req.params.sheet + "!" + currentRange + ":" + currentRange;
88 |
89 | sheetQueryBody.ranges.push(parsedRange);
90 | // Check limit
91 | if (sheetQueryBody.ranges.length >= limit) {
92 | break;
93 | }
94 | }
95 |
96 | // Make a query to sheets API
97 | sheets.spreadsheets.values
98 | .batchClear(sheetQueryBody)
99 | .then(result => {
100 | res.json({
101 | clearedRowsCount: result.data.clearedRanges
102 | ? result.data.clearedRanges.length
103 | : 0
104 | });
105 | })
106 | .catch(errorHandler);
107 | };
108 |
--------------------------------------------------------------------------------
/controllers/editRow.js:
--------------------------------------------------------------------------------
1 | // edit a row which matches given values
2 | const googleAuthLib = require("google-auth-library"),
3 | { google } = require("googleapis"),
4 | authConfig = require("../helpers/authentication/configuration"),
5 | objectDoesMatch = require("./objectDoesMatch"),
6 | retrieveSheet = require("./retrieveSheet"),
7 | User = require("../models/user");
8 |
9 | module.exports = (req, res, next) => {
10 | // Get the full response of sheet and then edit
11 | User.refreshAccessCode(res.locals.sheetIdDbResult.userGoogleId).then(
12 | validUser => {
13 | const query = { sheet: req.params.sheet, limit: req.query.limit };
14 |
15 | // Pass the valid user and result to the function which reads and parses the sheets, so as to supply the googleId of the sheet with the appropriate OAuth details.
16 | retrieveSheet(validUser, res.locals.sheetIdDbResult, query, res.locals.rowLimit)
17 | .then(data => {
18 | getIdAndForward(data, req, res, next);
19 | })
20 | .catch(next);
21 | }
22 | );
23 | };
24 |
25 | // After getting the googleId of sheet and a valid user
26 | function afterSheetGoogleId(
27 | validatedUser,
28 | sheetDbResult,
29 | parsedSheet,
30 | req,
31 | res,
32 | errorHandler
33 | ) {
34 | // User data init
35 | const sheets = google.sheets("v4"),
36 | oAuth2Client = new googleAuthLib.OAuth2Client(
37 | authConfig.google.clientID,
38 | authConfig.google.clientSecret
39 | );
40 |
41 | oAuth2Client.setCredentials({
42 | access_token: validatedUser.accessToken,
43 | refresh_token: validatedUser.refreshToken
44 | });
45 |
46 | // POSTed data
47 | const condition = req.body.condition,
48 | toSet = req.body.set,
49 | limit = req.body.limit ? req.body.limit : parsedSheet.length - 1;
50 |
51 | // Loop through all rows, check if it matches. If it does, add the row number (range) to allRanges
52 | const allRanges = [];
53 | for (let rowCount = 0; rowCount < parsedSheet.length; rowCount++) {
54 | const currentRow = parsedSheet[rowCount];
55 | // If row passes condition, add the row number to allRanges
56 | if (objectDoesMatch(condition, currentRow)) {
57 | allRanges.push(rowCount + 1 + 1); // Added one due to one based indexing, and one due to the first row being of keys
58 | }
59 | // Check limit
60 | if (allRanges.length >= limit) {
61 | break;
62 | }
63 | }
64 |
65 | const sheetQueryBody = {
66 | valueInputOption: "RAW",
67 | data: []
68 | };
69 |
70 | /* Create appropriate data in format -> {
71 | range: RANGE,
72 | values: [[VALUES]]
73 | } */
74 |
75 | const valueArray = [],
76 | keyOrder = Object.keys(parsedSheet[0]);
77 |
78 | // Get array of values to be set in row in correct order
79 | for (let keyIndex = 0; keyIndex < keyOrder.length; keyIndex++) {
80 | const currentKey = keyOrder[keyIndex];
81 | valueArray.push(toSet[currentKey]);
82 | }
83 |
84 | for (let i = 0; i < allRanges.length; i++) {
85 | const rowNum = allRanges[i],
86 | currentRange = req.params.sheet + "!" + rowNum + ":" + rowNum;
87 | // Eg: Sheet2!1:1, this yields only one row, so the length of the first dimension of the values array will always be 1
88 | const dataElem = {
89 | range: currentRange,
90 | values: [valueArray]
91 | };
92 |
93 | sheetQueryBody.data.push(dataElem);
94 | }
95 |
96 | // Now query the sheets API
97 | sheets.spreadsheets.values
98 | .batchUpdate({
99 | auth: oAuth2Client,
100 | spreadsheetId: sheetDbResult.googleId,
101 | resource: sheetQueryBody
102 | })
103 | .then(result =>
104 | res.json({ totalUpdatedRows: result.data.totalUpdatedRows || 0 })
105 | )
106 | .catch(errorHandler);
107 | }
108 |
109 | // Gets google id and valid user and forwards it for further processing
110 | const getIdAndForward = (parsedSheet, req, res, errorHandler) => {
111 | // refresh user's access token if necessary
112 | User.refreshAccessCode(res.locals.sheetIdDbResult.userGoogleId).then(
113 | validUser => {
114 | // Pass the valid user and result to the function which reads and parses the sheets, so as to supply the googleId of the sheet with the appropriate OAuth details.
115 | afterSheetGoogleId(
116 | validUser,
117 | res.locals.sheetIdDbResult,
118 | parsedSheet,
119 | req,
120 | res,
121 | errorHandler
122 | );
123 | }
124 | );
125 | };
126 |
--------------------------------------------------------------------------------
/controllers/getGoogleAuthorization.js:
--------------------------------------------------------------------------------
1 | // gets google OAuth authorization
2 | const passport = require("passport");
3 |
4 | module.exports = passport.authenticate("google", {
5 | scope: ["profile", "email", "https://www.googleapis.com/auth/spreadsheets"],
6 | accessType: "offline",
7 | prompt: "consent"
8 | });
9 |
--------------------------------------------------------------------------------
/controllers/index.js:
--------------------------------------------------------------------------------
1 | const logInMiddleware = require("./logInMiddleware"),
2 | logInRedirect = require("./logInRedirect"),
3 | firstIndexRender = require("./views/login"),
4 | viewProfile = require("./views/dashboard"),
5 | initialLogin = require("./views/initialLogin"),
6 | addStorage = require("./addStorage"),
7 | checkAuth = require("./checkAuth"),
8 | readSheet = require("./readSheet"),
9 | searchSheet = require("./searchSheet"),
10 | removeStorage = require("./removeStorage"),
11 | toggleSheetAuth = require("./toggleSheetAuth"),
12 | getGoogleAuthorization = require("./getGoogleAuthorization"),
13 | appendRow = require("./appendRow"),
14 | editRow = require("./editRow"),
15 | deleteRow = require("./deleteRow"),
16 | updateRequestCount = require("./updateRequestCount"),
17 | passportAuthCallback = require("./passportAuthCallback"),
18 | ow = require("ow"),
19 | logout = require("./logout");
20 |
21 | // actual controllers
22 | module.exports = {
23 | /*---- Interface routes ----*/
24 | initialLogin,
25 | login: firstIndexRender, // renders index.js
26 | logout,
27 |
28 | dashboard: [logInRedirect, viewProfile], // shows the profile page
29 |
30 | addStorage: [
31 | logInMiddleware,
32 | (req, res, next) =>
33 | addStorage(req, res, next).then(() => res.status(200).json({ ok: true }))
34 | ], // adds a spreadsheet to be used as an API, just provide id
35 |
36 | toggleSheetAuth: [logInMiddleware, toggleSheetAuth], // enables/disables auth for sheet
37 |
38 | removeStorage: [
39 | logInMiddleware,
40 | (req, res, next) =>
41 | removeStorage(req, res, next).then(() =>
42 | res.status(200).json({ ok: true })
43 | )
44 | ], // removes a spreadsheet
45 |
46 | /*---- Core API routes ----*/
47 |
48 | storage: {
49 | readSheet: [checkAuth, updateRequestCount, readSheet], // reads a sheet
50 | searchSheet: [checkAuth, updateRequestCount, searchSheet], // searches a sheet
51 | appendRow: [
52 | checkAuth,
53 | (req, res, next) => {
54 | ow(req.body, ow.array.minLength(1).ofType(ow.object));
55 | next();
56 | },
57 | updateRequestCount,
58 | appendRow
59 | ], // add new row(s)
60 | editRow: [
61 | checkAuth,
62 | (req, res, next) => {
63 | ow(
64 | req.body,
65 | ow.object.partialShape({
66 | condition: ow.object,
67 | set: ow.object.nonEmpty
68 | })
69 | );
70 | next();
71 | },
72 | updateRequestCount,
73 | editRow
74 | ], // edit row(s)
75 | deleteRow: [
76 | checkAuth,
77 | (req, res, next) => {
78 | ow(
79 | req.body,
80 | ow.object.partialShape({
81 | condition: ow.object
82 | })
83 | );
84 | next();
85 | },
86 | updateRequestCount,
87 | deleteRow
88 | ]
89 | },
90 |
91 | googleAuth: {
92 | authorize: getGoogleAuthorization,
93 | callback: passportAuthCallback
94 | }
95 | };
96 |
--------------------------------------------------------------------------------
/controllers/logInMiddleware.js:
--------------------------------------------------------------------------------
1 | // Send HTTP Code unauthenticated if applicable
2 | module.exports = (req, res, next) => {
3 | // If the user is authenticated in the session, carry on
4 | if (req.isAuthenticated()) {
5 | return next();
6 | }
7 |
8 | // If the user isn't logged in, send error code
9 | res.sendStatus(401);
10 | };
11 |
--------------------------------------------------------------------------------
/controllers/logInRedirect.js:
--------------------------------------------------------------------------------
1 | // Middleware for routes to make sure a user is logged in, redirect if not
2 | module.exports = (req, res, next) => {
3 | // If the user is authenticated in the session, carry on
4 | if (req.isAuthenticated()) {
5 | return next();
6 | }
7 |
8 | // If the user isn't logged in, redirect to the home page
9 | res.redirect("/");
10 | };
11 |
--------------------------------------------------------------------------------
/controllers/logout.js:
--------------------------------------------------------------------------------
1 | // logs out a user
2 | module.exports = (req, res) => {
3 | req.session.initialLoggedIn = false;
4 | req.logout();
5 | res.redirect("/");
6 | };
7 |
--------------------------------------------------------------------------------
/controllers/objectDoesMatch.js:
--------------------------------------------------------------------------------
1 | // Compares an object to another
2 |
3 | module.exports = (query, obj) => {
4 | let match = true;
5 | for (let key in query) {
6 | if (obj[key] !== query[key]) {
7 | match = false;
8 | break;
9 | }
10 | }
11 | return match;
12 | };
13 |
--------------------------------------------------------------------------------
/controllers/passportAuthCallback.js:
--------------------------------------------------------------------------------
1 | const passport = require("passport");
2 |
3 | module.exports = passport.authenticate("google", {
4 | successRedirect: "/dashboard",
5 | failureRedirect: "/"
6 | });
7 |
--------------------------------------------------------------------------------
/controllers/readSheet.js:
--------------------------------------------------------------------------------
1 | // reads a user's sheet
2 | const retrieveSheet = require("./retrieveSheet"),
3 | searchSheet = require("./searchSheet"),
4 | User = require("../models/user");
5 |
6 | module.exports = (req, res, next) => {
7 | // If it's a search request, delegate to search handler
8 | if (req.query.search) {
9 | return searchSheet(req, res, next);
10 | }
11 |
12 | // Refresh user's access token if necessary
13 | User.refreshAccessCode(res.locals.sheetIdDbResult.userGoogleId).then(
14 | validUser => {
15 | const query = {
16 | sheet: req.params.sheet,
17 | offset: req.query.offset,
18 | limit: req.query.limit
19 | };
20 |
21 | // Pass the valid user and result to the function which reads and parses the sheets, so as to supply the googleId of the sheet with the appropriate OAuth details.
22 | retrieveSheet(validUser, res.locals.sheetIdDbResult, query, res.locals.rowLimit)
23 | .then(data => {
24 | // Convert undefined values to null
25 | for (let row in data) {
26 | if (data.hasOwnProperty(row)) {
27 | Object.keys(data[row]).map(key => {
28 | data[row][key] = data[row][key] ? data[row][key] : null;
29 | });
30 | }
31 | }
32 | res.json(data);
33 | })
34 | .catch(next);
35 | }
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/controllers/removeStorage.js:
--------------------------------------------------------------------------------
1 | const Storage = require("../models/storage");
2 |
3 | module.exports = (req, res, next) => {
4 | return Storage.findById(req.params.id)
5 | .deleteOne()
6 | .then(() => {
7 | // Remove storage from user list
8 | req.user.storages = req.user.storages.filter(
9 | storage => storage._id !== req.params.id
10 | );
11 |
12 | return req.user.save();
13 | })
14 | .catch(err => next(err));
15 | };
16 |
--------------------------------------------------------------------------------
/controllers/retrieveSheet.js:
--------------------------------------------------------------------------------
1 | const googleAuthLib = require("google-auth-library"),
2 | googleOAuthConfig = require("../helpers/authentication/configuration").google,
3 | { google } = require("googleapis");
4 |
5 | // result argument is what db returned on query
6 | module.exports = (validUser, result, query, rowLimit = Infinity) => {
7 | const sheets = google.sheets("v4"),
8 | oauth2Client = new googleAuthLib.OAuth2Client(
9 | googleOAuthConfig.clientID,
10 | googleOAuthConfig.clientSecret,
11 | googleOAuthConfig.callbackURL
12 | );
13 |
14 | oauth2Client.credentials = {
15 | access_token: validUser.accessToken,
16 | refresh_token: validUser.refreshToken
17 | };
18 |
19 | return new Promise((resolve, reject) => {
20 | // Get data from spreadsheet
21 | sheets.spreadsheets.values
22 | .get({
23 | auth: oauth2Client,
24 | spreadsheetId: result.googleId,
25 | range: query.sheet
26 | })
27 | .then(response => {
28 | if (response.data.values.length > rowLimit) {
29 | reject({
30 | code: 429,
31 | message:
32 | "As per your subscription, you have exceeded the number of rows allowed in your sheet. Upgrade your plan to resume services."
33 | });
34 | }
35 |
36 | // Now parse the data into array of objects
37 | const parsed = [],
38 | values = response.data.values;
39 |
40 | const keys = values[0],
41 | offset = parseInt(query.offset) || 0,
42 | limit = parseInt(query.limit) || values.length - 1;
43 |
44 | for (
45 | let i = 1 + offset;
46 | i < values.length && i <= offset + limit;
47 | i++
48 | ) {
49 | const tempRow = {},
50 | tempValues = values[i];
51 |
52 | // Assign all values to keys (columns) of the row
53 | for (let keyIndex = 0; keyIndex < keys.length; keyIndex++) {
54 | tempRow[keys[keyIndex]] = tempValues[keyIndex];
55 | }
56 |
57 | // If not empty, add to array of parsed rows
58 | if (Object.entries(tempRow).length > 0) {
59 | parsed.push(tempRow);
60 | }
61 | }
62 |
63 | resolve(parsed);
64 | })
65 | .catch(err => reject(err));
66 | });
67 | };
68 |
--------------------------------------------------------------------------------
/controllers/search.js:
--------------------------------------------------------------------------------
1 | // Searches an array of objects according to property(ies)
2 |
3 | const objectDoesMatch = require("./objectDoesMatch");
4 |
5 | module.exports = (query, data, limit, offset) => {
6 | const answers = [];
7 | for (let i = 0; i < data.length && answers.length < limit + offset; i++) {
8 | if (objectDoesMatch(query, data[i])) {
9 | answers.push(data[i]);
10 | }
11 | }
12 |
13 | return answers.slice(offset);
14 | };
15 |
--------------------------------------------------------------------------------
/controllers/searchSheet.js:
--------------------------------------------------------------------------------
1 | // searches a sheet as per key:value pair
2 | const search = require("./search"),
3 | retrieveSheet = require("./retrieveSheet"),
4 | User = require("../models/user");
5 |
6 | module.exports = (req, res, next) => {
7 | // get the full response of sheet and then search
8 | User.refreshAccessCode(res.locals.sheetIdDbResult.userGoogleId).then(
9 | validUser => {
10 | const query = {sheet: req.params.sheet};
11 |
12 | // pass the valid user and result to the function which reads and parses the sheets, so as to supply the googleId of the sheet with the appropriate OAuth details.
13 | retrieveSheet(validUser, res.locals.sheetIdDbResult, query, res.locals.rowLimit)
14 | .then(data => {
15 | respond(data, req, res);
16 | })
17 | .catch(next);
18 | }
19 | );
20 | };
21 |
22 | const respond = (fullData, req, res) => {
23 | const offset = parseInt(req.query.offset) || 0,
24 | limit = parseInt(req.query.limit) || fullData.length;
25 |
26 | res.json(search(JSON.parse(req.query.search), fullData, limit, offset));
27 | };
28 |
--------------------------------------------------------------------------------
/controllers/toggleSheetAuth.js:
--------------------------------------------------------------------------------
1 | const Storage = require("../models/storage");
2 |
3 | module.exports = (req, res, next) => {
4 | let query = { $unset: { basicHttpAuth: 1 } };
5 |
6 | if (
7 | req.body.basicAuthToken &&
8 | req.body.basicAuthToken.length <= 255 &&
9 | req.body.basicAuthToken.split(":").length === 2
10 | ) {
11 | query = { $set: { basicHttpAuth: req.body.basicAuthToken } };
12 | }
13 |
14 | Storage.findById(req.params.id)
15 | .updateOne(query)
16 | .then(() => res.sendStatus(200))
17 | .catch(err => next(err));
18 | };
19 |
--------------------------------------------------------------------------------
/controllers/updateRequestCount.js:
--------------------------------------------------------------------------------
1 | module.exports = (req, res, next) => {
2 | const user = res.locals.user;
3 |
4 | if (user && user.monthEnd) {
5 | const currentDate = new Date();
6 |
7 | // If passed the end date of accounting month, update it. Else, just add one to count
8 | if (currentDate > user.monthEnd) {
9 | user.monthEnd.setFullYear(currentDate.getFullYear());
10 | user.monthEnd.setMonth(
11 | currentDate.getDate() >= user.monthEnd.getDate()
12 | ? currentDate.getMonth() + 1
13 | : currentDate.getMonth()
14 | );
15 | user.markModified("monthEnd");
16 | user.requestCount = 1;
17 | } else {
18 | user.requestCount += 1;
19 | }
20 |
21 | return user
22 | .save()
23 | .then(() => next())
24 | .catch(() => next());
25 | }
26 |
27 | return next();
28 | };
29 |
--------------------------------------------------------------------------------
/controllers/views/dashboard.js:
--------------------------------------------------------------------------------
1 | // Shows the profile page
2 | const User = require("../../models/user");
3 |
4 | module.exports = (req, res, next) => {
5 | if (req.session.initialLoggedIn) {
6 | User.findById(req.user._id)
7 | .populate({
8 | path: "storages"
9 | })
10 | .then(user =>
11 | res.render("dashboard", {
12 | user
13 | })
14 | )
15 | .catch(err => next(err));
16 | } else {
17 | res.redirect("/");
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/controllers/views/initialLogin.js:
--------------------------------------------------------------------------------
1 | const Admin = require("../../models/admin");
2 |
3 | module.exports = (req, res) => {
4 | const { username, password } = req.body;
5 |
6 | Admin.countDocuments().then(count => {
7 | // Create an admin user if this is the first attempt
8 | if (count === 0) {
9 | const newAdmin = new Admin({
10 | username,
11 | password: Admin.generateHash(password)
12 | });
13 | return newAdmin.save().then(() => logInAndRespond(req, res));
14 | }
15 |
16 | // If not first, then check credentials
17 | Admin.findOne({ username })
18 | .then(admin => {
19 | if (!admin || !admin.validPassword(password)) {
20 | return res.status(403).json({ error: "Wrong credentials provided" });
21 | }
22 |
23 | logInAndRespond(req, res);
24 | })
25 | .catch(error => res.status(500).json({ error: error.message }));
26 | });
27 | };
28 |
29 | function logInAndRespond(req, res) {
30 | req.session.initialLoggedIn = true;
31 | res.sendStatus(200);
32 | }
33 |
--------------------------------------------------------------------------------
/controllers/views/login.js:
--------------------------------------------------------------------------------
1 | const Admin = require("../../models/admin");
2 |
3 | module.exports = (req, res) => {
4 | if (req.session.initialLoggedIn) {
5 | if (req.isAuthenticated()) {
6 | res.redirect("dashboard");
7 | } else {
8 | res.render("googleLogin.ejs");
9 | }
10 | } else {
11 | Admin.countDocuments().then(count => {
12 | const options = { signUp: false };
13 |
14 | if (count === 0) {
15 | // Indicate to template that the user needs to sign up
16 | options.signUp = true;
17 | }
18 |
19 | res.render("initialLogin.ejs", options);
20 | });
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/helpers/authentication/configuration.js:
--------------------------------------------------------------------------------
1 | const dotenv = require("dotenv"),
2 | path = require("path");
3 |
4 | dotenv.config({ path: path.resolve(__dirname, "../../.env") });
5 |
6 | module.exports = {
7 | google: {
8 | clientSecret: process.env.STEIN_CLIENT_SECRET,
9 | clientID: process.env.STEIN_CLIENT_ID,
10 | callbackURL: process.env.STEIN_CALLBACK_URL
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/helpers/authentication/passportInit.js:
--------------------------------------------------------------------------------
1 | const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy,
2 | authConfig = require("./configuration"),
3 | User = require("../../models/user");
4 |
5 | module.exports = passport => {
6 | passport.serializeUser((user, done) => {
7 | done(null, user.id);
8 | });
9 |
10 | passport.deserializeUser((id, done) => {
11 | User.findById(id, (err, user) => {
12 | done(err, user);
13 | });
14 | });
15 |
16 | const Strategy = new GoogleStrategy(
17 | {
18 | clientID: authConfig.google.clientID,
19 | clientSecret: authConfig.google.clientSecret,
20 | callbackURL: authConfig.google.callbackURL
21 | },
22 | (accessToken, refreshToken, params, profile, done) => {
23 | User.findOrCreate(accessToken, refreshToken, params, profile).then(
24 | ({ error, user }) => {
25 | return done(error, user);
26 | }
27 | );
28 | }
29 | );
30 |
31 | passport.use(Strategy);
32 | };
33 |
--------------------------------------------------------------------------------
/helpers/db.js:
--------------------------------------------------------------------------------
1 | const Mongoose = require("mongoose").Mongoose,
2 | dotenv = require("dotenv"),
3 | path = require("path");
4 |
5 | dotenv.config({ path: path.resolve(__dirname, "../.env") });
6 |
7 | const dbConnection = new Mongoose();
8 |
9 | dbConnection.Promise = require("bluebird");
10 | dbConnection.connect(process.env.STEIN_MONGO_URL, { useNewUrlParser: true });
11 |
12 | module.exports = dbConnection;
13 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const app = require("./app").app,
2 | crypto = require("crypto"),
3 | express = require("express"),
4 | path = require("path"),
5 | interfaceRoutes = require("./interfaceRoutes"),
6 | passport = require("passport"),
7 | passportInit = require("./helpers/authentication/passportInit"),
8 | session = require("express-session"),
9 | cookieParser = require("cookie-parser");
10 |
11 | const MongoStore = require("connect-mongo")(session),
12 | sessionSecret =
13 | process.env.STEIN_SESSION_SECRET || crypto.randomBytes(6).toString("hex");
14 |
15 | let sessionOptions = {
16 | secret: sessionSecret,
17 | saveUninitialized: false,
18 | resave: false,
19 | store: new MongoStore({ url: process.env.STEIN_MONGO_URL }),
20 | cookie: { expires: false }
21 | };
22 |
23 | // set the view engine to ejs
24 | app.set("view engine", "ejs");
25 | app.set("views", path.join(__dirname, "/views"));
26 |
27 | app.use(cookieParser(sessionSecret));
28 | app.use(session(sessionOptions));
29 | app.use(passport.initialize());
30 | app.use(passport.session());
31 | app.use("/assets", express.static(path.resolve(__dirname, "./assets")));
32 |
33 | // Init passport
34 | passportInit(passport);
35 |
36 | interfaceRoutes(app);
37 |
38 | app.use((error, req, res, next) => {
39 | console.error(error);
40 |
41 | // If error has status code specified
42 | if (error.code) {
43 | let message = { error: error.message };
44 | // In case of errors returned by Google Sheets API
45 | if (error.errors) {
46 | message.error = error.errors[0].message;
47 | }
48 |
49 | return res.status(error.code).json(message);
50 | }
51 |
52 | // If error has only message or has no details
53 | res.status(500).json({ error: error.message || "An error occurred" });
54 | });
55 |
56 | const port = process.env.STEIN_PORT || 3000;
57 | app.listen(port, () => console.log(`Listening on ${port}`));
58 |
--------------------------------------------------------------------------------
/interfaceRoutes.js:
--------------------------------------------------------------------------------
1 | const controllers = require("./controllers");
2 |
3 | module.exports = app => {
4 | /*
5 | -----
6 | UI API Routes
7 | (Yes, I realise that they are against the accepted URL naming conventions of RESTful APIs, but these routes are of the least concern)
8 | -----
9 | */
10 |
11 | // Route for home page
12 | app.get("/", controllers.login);
13 |
14 | // Check initial login credentials
15 | app.post("/initial-login", controllers.initialLogin);
16 |
17 | // Route for dashboard UI
18 | app.get("/dashboard", controllers.dashboard);
19 |
20 | // Route for adding sheet
21 | app.post("/storages", controllers.addStorage);
22 |
23 | // Route for enabling/disabling auth for sheet
24 | app.put("/storage/:id", controllers.toggleSheetAuth);
25 |
26 | // Route for removing sheet
27 | app.delete("/storages/:id", controllers.removeStorage);
28 |
29 | // Route for logging out
30 | app.get("/logout", controllers.logout);
31 |
32 | /*
33 | -----
34 | Google Authentication Routes
35 | -----
36 | */
37 |
38 | app.get("/auth/google", controllers.googleAuth.authorize);
39 |
40 | // The callback after google has authenticated the user
41 | app.get("/auth/google/callback", controllers.googleAuth.callback);
42 | };
43 |
--------------------------------------------------------------------------------
/models/admin.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("../helpers/db"),
2 | bcrypt = require("bcrypt");
3 |
4 | const schema = mongoose.Schema({
5 | username: { type: String, required: true },
6 | password: { type: String, required: true }
7 | });
8 |
9 | schema.statics.generateHash = function(password) {
10 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
11 | };
12 |
13 | schema.methods.validPassword = function(password) {
14 | return bcrypt.compareSync(password, this.password);
15 | };
16 |
17 | module.exports = mongoose.model("Admin", schema);
18 |
--------------------------------------------------------------------------------
/models/storage.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("../helpers/db");
2 |
3 | const schema = mongoose.Schema({
4 | googleId: {
5 | type: String,
6 | required: true
7 | },
8 | userGoogleId: {
9 | type: String,
10 | required: true
11 | },
12 | basicHttpAuth: String,
13 | dateCreated: Date,
14 | title: {
15 | type: String,
16 | required: true
17 | }
18 | });
19 |
20 | module.exports = mongoose.model("Storage", schema);
21 |
--------------------------------------------------------------------------------
/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("../helpers/db"),
2 | googleAuthLib = require("google-auth-library"),
3 | googleOAuthConfig = require("../helpers/authentication/configuration").google;
4 |
5 | const schema = mongoose.Schema({
6 | name: String,
7 | email: String,
8 | dateRegistered: Date,
9 | googleId: String,
10 | storages: [{ type: mongoose.Schema.Types.ObjectId, ref: "Storage" }],
11 | refreshToken: String,
12 | accessToken: String,
13 | expiresAt: Date
14 | });
15 |
16 | schema.statics.findOrCreate = function(
17 | accessToken,
18 | refreshToken,
19 | params,
20 | profile
21 | ) {
22 | return new Promise(resolve => {
23 | const userObj = new this();
24 | this.findOne({ googleId: profile.id }, (err, result) => {
25 | if (!result) {
26 | userObj.googleId = profile.id;
27 | userObj.name = profile.displayName;
28 | userObj.email = profile.emails[0].value;
29 | userObj.basicHttpAuth = false;
30 | userObj.dateRegistered = Date.now();
31 | const now = new Date();
32 | userObj.expiresAt = now.setTime(now.getTime() + 1000 * 3599);
33 | userObj.refreshToken = refreshToken;
34 | userObj.accessToken = accessToken;
35 | userObj.save((error, user) => resolve({ error, user }));
36 | } else {
37 | result.refreshToken = refreshToken;
38 | result
39 | .save()
40 | .then(() => this.refreshAccessCode(result.googleId))
41 | .then(result => resolve({ error: null, user: result }));
42 | }
43 | });
44 | });
45 | };
46 |
47 | schema.statics.refreshAccessCode = function(googleId) {
48 | return new Promise((resolve, reject) => {
49 | this.findOne({ googleId }, (err, result) => {
50 | const now = new Date();
51 | if (now.getTime() < result.expiresAt) {
52 | const oauth2Client = new googleAuthLib.OAuth2Client(
53 | googleOAuthConfig.clientID,
54 | googleOAuthConfig.clientSecret,
55 | googleOAuthConfig.callbackURL
56 | );
57 |
58 | oauth2Client.setCredentials({
59 | refresh_token: result.refreshToken
60 | });
61 |
62 | oauth2Client.getAccessToken().then(response => {
63 | this.findOneAndUpdate(
64 | { googleId },
65 | {
66 | $set: {
67 | accessToken: response.token,
68 | expiresAt: now.setTime(now.getTime() + 1000 * 3590) // A bit less than 1 hour
69 | }
70 | },
71 | { new: true }
72 | )
73 | .then(doc => resolve(doc))
74 | .catch(err => reject(err));
75 | });
76 | } else {
77 | resolve(result);
78 | }
79 | });
80 | });
81 | };
82 |
83 | module.exports = mongoose.model("User", schema);
84 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stein-core",
3 | "version": "0.0.2",
4 | "homepage": "https://steinhq.com/",
5 | "description": "Use Google Sheets as a no-setup data store",
6 | "main": "index.js",
7 | "scripts": {
8 | "start": "pm2 start bin/pm2.config.js --env production",
9 | "kill": "pm2 kill",
10 | "dev": "cross-env NODE_ENV=development nodemon index.js"
11 | },
12 | "bin": {
13 | "stein": "./bin/start-stein.js"
14 | },
15 | "keywords": [
16 | "google sheets",
17 | "database",
18 | "REST",
19 | "api",
20 | "form"
21 | ],
22 | "repository": {
23 | "type": "git",
24 | "url": "git://github.com/SteinHQ/Stein.git"
25 | },
26 | "author": "Stein ",
27 | "license": "MIT",
28 | "dependencies": {
29 | "basic-auth": "^2.0.1",
30 | "bcrypt": "^3.0.6",
31 | "bluebird": "^3.5.1",
32 | "body-parser": "^1.18.2",
33 | "connect-mongo": "^2.0.3",
34 | "cookie-parser": "^1.4.3",
35 | "cors": "^2.8.4",
36 | "dotenv": "^8.0.0",
37 | "ejs": "^2.6.1",
38 | "express": "^4.16.2",
39 | "express-session": "^1.15.6",
40 | "google-auth-library": "^2.0.0",
41 | "googleapis": "^34.0.0",
42 | "mongoose": "^5.3.0",
43 | "ow": "^0.12.0",
44 | "passport": "^0.4.0",
45 | "passport-google-oauth": "2.0.0",
46 | "pm2": "^3.5.1",
47 | "request": "^2.88.0"
48 | },
49 | "devDependencies": {
50 | "cross-env": "^5.2.0",
51 | "errorhandler": "^1.5.0",
52 | "nodemon": "^1.18.10",
53 | "response-time": "^2.3.2"
54 | },
55 | "eslintConfig": {
56 | "rules": {
57 | "no-unused-vars": [
58 | "error",
59 | {
60 | "argsIgnorePattern": "next"
61 | }
62 | ]
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/views/dashboard.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include('header'); -%>
4 |
5 |
6 |
Stein Dashboard
7 |
Log out
8 |
9 | <%= user.name.split(' ').slice(-1) %>'s Sheets
10 |
11 |
12 |
13 | <% if (!user.storages.length) { %>
14 |
15 | Start by adding a sheet
16 |
17 | <% } %>
18 |
19 | <% for (let i = 0; i < user.storages.length; i++) { %>
20 |
21 |
22 | <%= user.storages[i].title %>
23 |
24 |
25 | /v1/storages/<%= user.storages[i]._id %>
26 |
27 | <% if (user.storages[i].basicHttpAuth) { %>
28 |
29 | Authentication:
30 | Basic <%= Buffer.from(user.storages[i].basicHttpAuth).toString('base64') %><%= '='.repeat(user.storages[i].basicHttpAuth % 3) %>
31 |
32 | <% } %>
33 |
45 |
46 | <% } %>
47 |
48 |
49 |
68 |
69 |
70 |
71 |
72 |
Add Sheet
73 |
78 |
Add
79 |
80 |
81 |
82 |
83 |
84 |
85 | Add Sheet
86 |
87 |
214 |
215 |
--------------------------------------------------------------------------------
/views/googleLogin.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include('header'); -%>
4 |
5 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/views/header.ejs:
--------------------------------------------------------------------------------
1 |
2 | Stein Dashboard
3 |
4 |
5 |
6 |
261 |
--------------------------------------------------------------------------------
/views/initialLogin.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include('header'); -%>
4 |
5 |
18 |
19 |
20 |
47 |
48 |
--------------------------------------------------------------------------------