Add Dexie for persistence; user management with dexie; this is the way
This commit is contained in:
		
							parent
							
								
									8036aa2942
								
							
						
					
					
						commit
						23d275acec
					
				
					 16 changed files with 285 additions and 494 deletions
				
			
		
							
								
								
									
										362
									
								
								web/package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										362
									
								
								web/package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -11,10 +11,10 @@ | |||
|         "@emotion/styled": "latest", | ||||
|         "@mui/icons-material": "^5.4.2", | ||||
|         "@mui/material": "latest", | ||||
|         "@mui/styles": "^5.4.2", | ||||
|         "dexie": "^3.2.1", | ||||
|         "dexie-react-hooks": "^1.1.1", | ||||
|         "react": "latest", | ||||
|         "react-dom": "latest", | ||||
|         "react-router-dom": "^6.2.1", | ||||
|         "react-scripts": "^3.0.1" | ||||
|       } | ||||
|     }, | ||||
|  | @ -2364,46 +2364,6 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@mui/styles": { | ||||
|       "version": "5.4.2", | ||||
|       "resolved": "https://registry.npmjs.org/@mui/styles/-/styles-5.4.2.tgz", | ||||
|       "integrity": "sha512-BX75fNHmRF51yove9dBkH28gpSFjClOPDEnUwLTghPYN913OsqViS/iuCd61dxzygtEEmmeYuWfQjxu/F6vF5g==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.17.0", | ||||
|         "@emotion/hash": "^0.8.0", | ||||
|         "@mui/private-theming": "^5.4.2", | ||||
|         "@mui/types": "^7.1.2", | ||||
|         "@mui/utils": "^5.4.2", | ||||
|         "clsx": "^1.1.1", | ||||
|         "csstype": "^3.0.10", | ||||
|         "hoist-non-react-statics": "^3.3.2", | ||||
|         "jss": "^10.8.2", | ||||
|         "jss-plugin-camel-case": "^10.8.2", | ||||
|         "jss-plugin-default-unit": "^10.8.2", | ||||
|         "jss-plugin-global": "^10.8.2", | ||||
|         "jss-plugin-nested": "^10.8.2", | ||||
|         "jss-plugin-props-sort": "^10.8.2", | ||||
|         "jss-plugin-rule-value-function": "^10.8.2", | ||||
|         "jss-plugin-vendor-prefixer": "^10.8.2", | ||||
|         "prop-types": "^15.7.2" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=12.0.0" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "type": "opencollective", | ||||
|         "url": "https://opencollective.com/mui" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@types/react": "^16.8.6 || ^17.0.0", | ||||
|         "react": "^17.0.0" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "@types/react": { | ||||
|           "optional": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@mui/system": { | ||||
|       "version": "5.4.2", | ||||
|       "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.4.2.tgz", | ||||
|  | @ -5683,15 +5643,6 @@ | |||
|         "node": ">=0.10.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/css-vendor": { | ||||
|       "version": "2.0.8", | ||||
|       "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", | ||||
|       "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.8.3", | ||||
|         "is-in-browser": "^1.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/css-what": { | ||||
|       "version": "5.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", | ||||
|  | @ -6174,6 +6125,24 @@ | |||
|       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", | ||||
|       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" | ||||
|     }, | ||||
|     "node_modules/dexie": { | ||||
|       "version": "3.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.1.tgz", | ||||
|       "integrity": "sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g==", | ||||
|       "engines": { | ||||
|         "node": ">=6.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/dexie-react-hooks": { | ||||
|       "version": "1.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.1.tgz", | ||||
|       "integrity": "sha512-Cam5JP6PxHN564RvWEoe8cqLhosW0O4CAZ9XEVYeGHJBa6KEJlOpd9CUpV3kmU9dm2MrW97/lk7qkf1xpij7gA==", | ||||
|       "peerDependencies": { | ||||
|         "@types/react": ">=16", | ||||
|         "dexie": ">=3.1.0-alpha.1 <5.0.0", | ||||
|         "react": ">=16" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/diff-sequences": { | ||||
|       "version": "24.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", | ||||
|  | @ -8364,14 +8333,6 @@ | |||
|       "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", | ||||
|       "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" | ||||
|     }, | ||||
|     "node_modules/history": { | ||||
|       "version": "5.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", | ||||
|       "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.7.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/hmac-drbg": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", | ||||
|  | @ -8571,11 +8532,6 @@ | |||
|       "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", | ||||
|       "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" | ||||
|     }, | ||||
|     "node_modules/hyphenate-style-name": { | ||||
|       "version": "1.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", | ||||
|       "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" | ||||
|     }, | ||||
|     "node_modules/iconv-lite": { | ||||
|       "version": "0.4.24", | ||||
|       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", | ||||
|  | @ -9168,11 +9124,6 @@ | |||
|         "node": ">=0.10.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/is-in-browser": { | ||||
|       "version": "1.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", | ||||
|       "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" | ||||
|     }, | ||||
|     "node_modules/is-negative-zero": { | ||||
|       "version": "2.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", | ||||
|  | @ -10290,88 +10241,6 @@ | |||
|         "node": ">=0.6.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jss": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss/-/jss-10.9.0.tgz", | ||||
|       "integrity": "sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "csstype": "^3.0.2", | ||||
|         "is-in-browser": "^1.1.3", | ||||
|         "tiny-warning": "^1.0.2" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "type": "opencollective", | ||||
|         "url": "https://opencollective.com/jss" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jss-plugin-camel-case": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz", | ||||
|       "integrity": "sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "hyphenate-style-name": "^1.0.3", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jss-plugin-default-unit": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz", | ||||
|       "integrity": "sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jss-plugin-global": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz", | ||||
|       "integrity": "sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jss-plugin-nested": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz", | ||||
|       "integrity": "sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0", | ||||
|         "tiny-warning": "^1.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jss-plugin-props-sort": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz", | ||||
|       "integrity": "sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jss-plugin-rule-value-function": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz", | ||||
|       "integrity": "sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0", | ||||
|         "tiny-warning": "^1.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jss-plugin-vendor-prefixer": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz", | ||||
|       "integrity": "sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA==", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "css-vendor": "^2.0.8", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jsx-ast-utils": { | ||||
|       "version": "2.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", | ||||
|  | @ -13824,30 +13693,6 @@ | |||
|       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", | ||||
|       "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" | ||||
|     }, | ||||
|     "node_modules/react-router": { | ||||
|       "version": "6.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz", | ||||
|       "integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==", | ||||
|       "dependencies": { | ||||
|         "history": "^5.2.0" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "react": ">=16.8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/react-router-dom": { | ||||
|       "version": "6.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz", | ||||
|       "integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==", | ||||
|       "dependencies": { | ||||
|         "history": "^5.2.0", | ||||
|         "react-router": "6.2.1" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "react": ">=16.8", | ||||
|         "react-dom": ">=16.8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/react-scripts": { | ||||
|       "version": "3.4.4", | ||||
|       "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.4.tgz", | ||||
|  | @ -16357,11 +16202,6 @@ | |||
|       "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", | ||||
|       "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" | ||||
|     }, | ||||
|     "node_modules/tiny-warning": { | ||||
|       "version": "1.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", | ||||
|       "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" | ||||
|     }, | ||||
|     "node_modules/tmp": { | ||||
|       "version": "0.0.33", | ||||
|       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", | ||||
|  | @ -19518,30 +19358,6 @@ | |||
|         "prop-types": "^15.7.2" | ||||
|       } | ||||
|     }, | ||||
|     "@mui/styles": { | ||||
|       "version": "5.4.2", | ||||
|       "resolved": "https://registry.npmjs.org/@mui/styles/-/styles-5.4.2.tgz", | ||||
|       "integrity": "sha512-BX75fNHmRF51yove9dBkH28gpSFjClOPDEnUwLTghPYN913OsqViS/iuCd61dxzygtEEmmeYuWfQjxu/F6vF5g==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.17.0", | ||||
|         "@emotion/hash": "^0.8.0", | ||||
|         "@mui/private-theming": "^5.4.2", | ||||
|         "@mui/types": "^7.1.2", | ||||
|         "@mui/utils": "^5.4.2", | ||||
|         "clsx": "^1.1.1", | ||||
|         "csstype": "^3.0.10", | ||||
|         "hoist-non-react-statics": "^3.3.2", | ||||
|         "jss": "^10.8.2", | ||||
|         "jss-plugin-camel-case": "^10.8.2", | ||||
|         "jss-plugin-default-unit": "^10.8.2", | ||||
|         "jss-plugin-global": "^10.8.2", | ||||
|         "jss-plugin-nested": "^10.8.2", | ||||
|         "jss-plugin-props-sort": "^10.8.2", | ||||
|         "jss-plugin-rule-value-function": "^10.8.2", | ||||
|         "jss-plugin-vendor-prefixer": "^10.8.2", | ||||
|         "prop-types": "^15.7.2" | ||||
|       } | ||||
|     }, | ||||
|     "@mui/system": { | ||||
|       "version": "5.4.2", | ||||
|       "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.4.2.tgz", | ||||
|  | @ -22155,15 +21971,6 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "css-vendor": { | ||||
|       "version": "2.0.8", | ||||
|       "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", | ||||
|       "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.8.3", | ||||
|         "is-in-browser": "^1.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "css-what": { | ||||
|       "version": "5.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", | ||||
|  | @ -22542,6 +22349,17 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "dexie": { | ||||
|       "version": "3.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.1.tgz", | ||||
|       "integrity": "sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g==" | ||||
|     }, | ||||
|     "dexie-react-hooks": { | ||||
|       "version": "1.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.1.tgz", | ||||
|       "integrity": "sha512-Cam5JP6PxHN564RvWEoe8cqLhosW0O4CAZ9XEVYeGHJBa6KEJlOpd9CUpV3kmU9dm2MrW97/lk7qkf1xpij7gA==", | ||||
|       "requires": {} | ||||
|     }, | ||||
|     "diff-sequences": { | ||||
|       "version": "24.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", | ||||
|  | @ -24243,14 +24061,6 @@ | |||
|       "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", | ||||
|       "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" | ||||
|     }, | ||||
|     "history": { | ||||
|       "version": "5.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", | ||||
|       "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.7.6" | ||||
|       } | ||||
|     }, | ||||
|     "hmac-drbg": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", | ||||
|  | @ -24418,11 +24228,6 @@ | |||
|       "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", | ||||
|       "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" | ||||
|     }, | ||||
|     "hyphenate-style-name": { | ||||
|       "version": "1.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", | ||||
|       "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" | ||||
|     }, | ||||
|     "iconv-lite": { | ||||
|       "version": "0.4.24", | ||||
|       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", | ||||
|  | @ -24846,11 +24651,6 @@ | |||
|         "is-extglob": "^2.1.1" | ||||
|       } | ||||
|     }, | ||||
|     "is-in-browser": { | ||||
|       "version": "1.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", | ||||
|       "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" | ||||
|     }, | ||||
|     "is-negative-zero": { | ||||
|       "version": "2.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", | ||||
|  | @ -25729,84 +25529,6 @@ | |||
|         "verror": "1.10.0" | ||||
|       } | ||||
|     }, | ||||
|     "jss": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss/-/jss-10.9.0.tgz", | ||||
|       "integrity": "sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "csstype": "^3.0.2", | ||||
|         "is-in-browser": "^1.1.3", | ||||
|         "tiny-warning": "^1.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "jss-plugin-camel-case": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz", | ||||
|       "integrity": "sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "hyphenate-style-name": "^1.0.3", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "jss-plugin-default-unit": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz", | ||||
|       "integrity": "sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "jss-plugin-global": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz", | ||||
|       "integrity": "sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "jss-plugin-nested": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz", | ||||
|       "integrity": "sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0", | ||||
|         "tiny-warning": "^1.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "jss-plugin-props-sort": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz", | ||||
|       "integrity": "sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "jss-plugin-rule-value-function": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz", | ||||
|       "integrity": "sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "jss": "10.9.0", | ||||
|         "tiny-warning": "^1.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "jss-plugin-vendor-prefixer": { | ||||
|       "version": "10.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz", | ||||
|       "integrity": "sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.3.1", | ||||
|         "css-vendor": "^2.0.8", | ||||
|         "jss": "10.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "jsx-ast-utils": { | ||||
|       "version": "2.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", | ||||
|  | @ -28599,23 +28321,6 @@ | |||
|       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", | ||||
|       "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" | ||||
|     }, | ||||
|     "react-router": { | ||||
|       "version": "6.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz", | ||||
|       "integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==", | ||||
|       "requires": { | ||||
|         "history": "^5.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "react-router-dom": { | ||||
|       "version": "6.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz", | ||||
|       "integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==", | ||||
|       "requires": { | ||||
|         "history": "^5.2.0", | ||||
|         "react-router": "6.2.1" | ||||
|       } | ||||
|     }, | ||||
|     "react-scripts": { | ||||
|       "version": "3.4.4", | ||||
|       "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.4.tgz", | ||||
|  | @ -30620,11 +30325,6 @@ | |||
|       "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", | ||||
|       "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" | ||||
|     }, | ||||
|     "tiny-warning": { | ||||
|       "version": "1.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", | ||||
|       "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" | ||||
|     }, | ||||
|     "tmp": { | ||||
|       "version": "0.0.33", | ||||
|       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", | ||||
|  |  | |||
|  | @ -12,6 +12,8 @@ | |||
|     "@emotion/styled": "latest", | ||||
|     "@mui/icons-material": "^5.4.2", | ||||
|     "@mui/material": "latest", | ||||
|     "dexie": "^3.2.1", | ||||
|     "dexie-react-hooks": "^1.1.1", | ||||
|     "react": "latest", | ||||
|     "react-dom": "latest", | ||||
|     "react-scripts": "^3.0.1" | ||||
|  |  | |||
|  | @ -7,9 +7,11 @@ import { | |||
|     topicShortUrl, | ||||
|     topicUrlJsonPollWithSince | ||||
| } from "./utils"; | ||||
| import db from "./db"; | ||||
| 
 | ||||
| class Api { | ||||
|     async poll(baseUrl, topic, since, user) { | ||||
|     async poll(baseUrl, topic, since) { | ||||
|         const user = await db.users.get(baseUrl); | ||||
|         const shortUrl = topicShortUrl(baseUrl, topic); | ||||
|         const url = (since) | ||||
|             ? topicUrlJsonPollWithSince(baseUrl, topic, since) | ||||
|  | @ -24,7 +26,8 @@ class Api { | |||
|         return messages; | ||||
|     } | ||||
| 
 | ||||
|     async publish(baseUrl, topic, user, message) { | ||||
|     async publish(baseUrl, topic, message) { | ||||
|         const user = await db.users.get(baseUrl); | ||||
|         const url = topicUrl(baseUrl, topic); | ||||
|         console.log(`[Api] Publishing message to ${url}`); | ||||
|         await fetch(url, { | ||||
|  |  | |||
|  | @ -85,7 +85,7 @@ class Connection { | |||
|         if (this.since) { | ||||
|             params.push(`since=${this.since}`); | ||||
|         } | ||||
|         if (this.user !== null) { | ||||
|         if (this.user) { | ||||
|             const auth = encodeBase64Url(basicAuth(this.user.username, this.user.password)); | ||||
|             params.push(`auth=${auth}`); | ||||
|         } | ||||
|  |  | |||
|  | @ -6,7 +6,11 @@ class ConnectionManager { | |||
|     } | ||||
| 
 | ||||
|     refresh(subscriptions, users, onNotification) { | ||||
|         if (!subscriptions || !users) { | ||||
|             return; | ||||
|         } | ||||
|         console.log(`[ConnectionManager] Refreshing connections`); | ||||
|         console.log(users); | ||||
|         const subscriptionIds = subscriptions.ids(); | ||||
|         const deletedIds = Array.from(this.connections.keys()).filter(id => !subscriptionIds.includes(id)); | ||||
| 
 | ||||
|  | @ -16,7 +20,7 @@ class ConnectionManager { | |||
|             if (added) { | ||||
|                 const baseUrl = subscription.baseUrl; | ||||
|                 const topic = subscription.topic; | ||||
|                 const user = users.get(baseUrl); | ||||
|                 const [user] = users.filter(user => user.baseUrl === baseUrl); | ||||
|                 const since = subscription.last; | ||||
|                 const connection = new Connection(id, baseUrl, topic, user, since, onNotification); | ||||
|                 this.connections.set(id, connection); | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| import Subscription from "./Subscription"; | ||||
| import Subscriptions from "./Subscriptions"; | ||||
| import Users from "./Users"; | ||||
| import User from "./User"; | ||||
| 
 | ||||
| class Repository { | ||||
|     loadSubscriptions() { | ||||
|  | @ -43,40 +41,6 @@ class Repository { | |||
|         localStorage.setItem('subscriptions', serialized); | ||||
|     } | ||||
| 
 | ||||
|     loadUsers() { | ||||
|         console.log(`[Repository] Loading users from localStorage`); | ||||
|         const users = new Users(); | ||||
|         users.loaded = true; | ||||
|         const serialized = localStorage.getItem('users'); | ||||
|         if (serialized === null) { | ||||
|             return users; | ||||
|         } | ||||
|         try { | ||||
|             JSON.parse(serialized).forEach(u => { | ||||
|                 users.add(new User(u.baseUrl, u.username, u.password)); | ||||
|             }); | ||||
|             return users; | ||||
|         } catch (e) { | ||||
|             console.log(`[Repository] Unable to deserialize users: ${e.message}`); | ||||
|             return users; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     saveUsers(users) { | ||||
|         if (!users.loaded) { | ||||
|             return; // Avoid saving invalid state, triggered by initial useEffect hook
 | ||||
|         } | ||||
|         console.log(`[Repository] Saving users to localStorage`); | ||||
|         const serialized = JSON.stringify(users.map(user => { | ||||
|             return { | ||||
|                 baseUrl: user.baseUrl, | ||||
|                 username: user.username, | ||||
|                 password: user.password | ||||
|             } | ||||
|         })); | ||||
|         localStorage.setItem('users', serialized); | ||||
|     } | ||||
| 
 | ||||
|     loadSelectedSubscriptionId() { | ||||
|         console.log(`[Repository] Loading selected subscription ID from localStorage`); | ||||
|         const selectedSubscriptionId = localStorage.getItem('selectedSubscriptionId'); | ||||
|  |  | |||
|  | @ -1,9 +0,0 @@ | |||
| class User { | ||||
|     constructor(baseUrl, username, password) { | ||||
|         this.baseUrl = baseUrl; | ||||
|         this.username = username; | ||||
|         this.password = password; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default User; | ||||
|  | @ -1,38 +0,0 @@ | |||
| class Users { | ||||
|     constructor() { | ||||
|         this.loaded = false; // FIXME I hate this
 | ||||
|         this.users = new Map(); | ||||
|     } | ||||
| 
 | ||||
|     add(user) { | ||||
|         this.users.set(user.baseUrl, user); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     get(baseUrl) { | ||||
|         const user = this.users.get(baseUrl); | ||||
|         return (user) ? user : null; | ||||
|     } | ||||
| 
 | ||||
|     update(user) { | ||||
|         return this.add(user); | ||||
|     } | ||||
| 
 | ||||
|     remove(baseUrl) { | ||||
|         this.users.delete(baseUrl); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     map(cb) { | ||||
|         return Array.from(this.users.values()).map(cb); | ||||
|     } | ||||
| 
 | ||||
|     clone() { | ||||
|         const c = new Users(); | ||||
|         c.loaded = this.loaded; | ||||
|         c.users = new Map(this.users); | ||||
|         return c; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default Users; | ||||
							
								
								
									
										15
									
								
								web/src/app/db.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								web/src/app/db.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| import Dexie from 'dexie'; | ||||
| 
 | ||||
| // Uses Dexie.js
 | ||||
| // https://dexie.org/docs/API-Reference#quick-reference
 | ||||
| //
 | ||||
| // Notes:
 | ||||
| // - As per docs, we only declare the indexable columns, not all columns
 | ||||
| 
 | ||||
| const db = new Dexie('ntfy'); | ||||
| 
 | ||||
| db.version(1).stores({ | ||||
|     users: '&baseUrl, username', | ||||
| }); | ||||
| 
 | ||||
| export default db; | ||||
|  | @ -37,7 +37,6 @@ const ActionBar = (props) => { | |||
|                 </Typography> | ||||
|                 {props.selectedSubscription !== null && <IconSubscribeSettings | ||||
|                     subscription={props.selectedSubscription} | ||||
|                     users={props.users} | ||||
|                     onClearAll={props.onClearAll} | ||||
|                     onUnsubscribe={props.onUnsubscribe} | ||||
|                 />} | ||||
|  |  | |||
|  | @ -12,19 +12,20 @@ import connectionManager from "../app/ConnectionManager"; | |||
| import Subscriptions from "../app/Subscriptions"; | ||||
| import Navigation from "./Navigation"; | ||||
| import ActionBar from "./ActionBar"; | ||||
| import Users from "../app/Users"; | ||||
| import notificationManager from "../app/NotificationManager"; | ||||
| import NoTopics from "./NoTopics"; | ||||
| import Preferences from "./Preferences"; | ||||
| import db from "../app/db"; | ||||
| import {useLiveQuery} from "dexie-react-hooks"; | ||||
| 
 | ||||
| // TODO subscribe dialog:
 | ||||
| //  - check/use existing user
 | ||||
| //  - add baseUrl
 | ||||
| // TODO user management
 | ||||
| // TODO embed into ntfy server
 | ||||
| // TODO make default server functional
 | ||||
| // TODO indexeddb for notifications + subscriptions
 | ||||
| // TODO business logic with callbacks
 | ||||
| // TODO connection indicator in subscription list
 | ||||
| 
 | ||||
| const App = () => { | ||||
|     console.log(`[App] Rendering main view`); | ||||
|  | @ -32,21 +33,18 @@ const App = () => { | |||
|     const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); | ||||
|     const [prefsOpen, setPrefsOpen] = useState(false); | ||||
|     const [subscriptions, setSubscriptions] = useState(new Subscriptions()); | ||||
|     const [users, setUsers] = useState(new Users()); | ||||
|     const [selectedSubscription, setSelectedSubscription] = useState(null); | ||||
|     const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted()); | ||||
|     const users = useLiveQuery(() => db.users.toArray()); | ||||
|     const handleSubscriptionClick = (subscriptionId) => { | ||||
|         setSelectedSubscription(subscriptions.get(subscriptionId)); | ||||
|         setPrefsOpen(false); | ||||
|     } | ||||
|     const handleSubscribeSubmit = (subscription, user) => { | ||||
|     const handleSubscribeSubmit = (subscription) => { | ||||
|         console.log(`[App] New subscription: ${subscription.id}`); | ||||
|         if (user !== null) { | ||||
|             setUsers(prev => prev.add(user).clone()); | ||||
|         } | ||||
|         setSubscriptions(prev => prev.add(subscription).clone()); | ||||
|         setSelectedSubscription(subscription); | ||||
|         poll(subscription, user); | ||||
|         poll(subscription); | ||||
|         handleRequestPermission(); | ||||
|     }; | ||||
|     const handleDeleteNotification = (subscriptionId, notificationId) => { | ||||
|  | @ -80,9 +78,9 @@ const App = () => { | |||
|         setPrefsOpen(true); | ||||
|         setSelectedSubscription(null); | ||||
|     }; | ||||
|     const poll = (subscription, user) => { | ||||
|     const poll = (subscription) => { | ||||
|         const since = subscription.last; | ||||
|         api.poll(subscription.baseUrl, subscription.topic, since, user) | ||||
|         api.poll(subscription.baseUrl, subscription.topic, since) | ||||
|             .then(notifications => { | ||||
|                 setSubscriptions(prev => { | ||||
|                     subscription.addNotifications(notifications); | ||||
|  | @ -94,12 +92,10 @@ const App = () => { | |||
|     // Define hooks: Note that the order of the hooks is important. The "loading" hooks
 | ||||
|     // must be before the "saving" hooks.
 | ||||
|     useEffect(() => { | ||||
|         // Load subscriptions and users
 | ||||
|         // Load subscriptions
 | ||||
|         const subscriptions = repository.loadSubscriptions(); | ||||
|         const selectedSubscriptionId = repository.loadSelectedSubscriptionId(); | ||||
|         const users = repository.loadUsers(); | ||||
|         setSubscriptions(subscriptions); | ||||
|         setUsers(users); | ||||
| 
 | ||||
|         // Set selected subscription
 | ||||
|         const maybeSelectedSubscription = subscriptions.get(selectedSubscriptionId); | ||||
|  | @ -109,8 +105,7 @@ const App = () => { | |||
| 
 | ||||
|         // Poll all subscriptions
 | ||||
|         subscriptions.forEach((subscriptionId, subscription) => { | ||||
|             const user = users.get(subscription.baseUrl); // May be null
 | ||||
|             poll(subscription, user); | ||||
|             poll(subscription); | ||||
|         }); | ||||
|     }, [/* initial render */]); | ||||
|     useEffect(() => { | ||||
|  | @ -127,7 +122,6 @@ const App = () => { | |||
|         connectionManager.refresh(subscriptions, users, handleNotification); | ||||
|     }, [subscriptions, users]); | ||||
|     useEffect(() => repository.saveSubscriptions(subscriptions), [subscriptions]); | ||||
|     useEffect(() => repository.saveUsers(users), [users]); | ||||
|     useEffect(() => { | ||||
|         const subscriptionId = (selectedSubscription) ? selectedSubscription.id : ""; | ||||
|         repository.saveSelectedSubscriptionId(subscriptionId) | ||||
|  | @ -140,7 +134,6 @@ const App = () => { | |||
|                 <CssBaseline/> | ||||
|                 <ActionBar | ||||
|                     selectedSubscription={selectedSubscription} | ||||
|                     users={users} | ||||
|                     onClearAll={handleDeleteAllNotifications} | ||||
|                     onUnsubscribe={handleUnsubscribe} | ||||
|                     onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ import api from "../app/Api"; | |||
| const IconSubscribeSettings = (props) => { | ||||
|     const [open, setOpen] = useState(false); | ||||
|     const anchorRef = useRef(null); | ||||
|     const users = props.users; | ||||
| 
 | ||||
|     const handleToggle = () => { | ||||
|         setOpen((prevOpen) => !prevOpen); | ||||
|  | @ -40,8 +39,7 @@ const IconSubscribeSettings = (props) => { | |||
|     const handleSendTestMessage = () => { | ||||
|         const baseUrl = props.subscription.baseUrl; | ||||
|         const topic = props.subscription.topic; | ||||
|         const user = users.get(baseUrl); // May be null
 | ||||
|         api.publish(baseUrl, topic, user, | ||||
|         api.publish(baseUrl, topic, | ||||
|             `This is a test notification sent by the ntfy Web UI at ${new Date().toString()}.`); // FIXME result ignored
 | ||||
|         setOpen(false); | ||||
|     } | ||||
|  |  | |||
|  | @ -57,9 +57,9 @@ const NavList = (props) => { | |||
|         setSubscribeDialogOpen(false); | ||||
|         setSubscribeDialogKey(prev => prev+1); | ||||
|     } | ||||
|     const handleSubscribeSubmit = (subscription, user) => { | ||||
|     const handleSubscribeSubmit = (subscription) => { | ||||
|         handleSubscribeReset(); | ||||
|         props.onSubscribeSubmit(subscription, user); | ||||
|         props.onSubscribeSubmit(subscription); | ||||
|     } | ||||
|     const showSubscriptionsList = props.subscriptions.size() > 0; | ||||
|     const showGrantPermissionsBox = props.subscriptions.size() > 0 && !props.notificationsGranted; | ||||
|  |  | |||
|  | @ -1,6 +1,18 @@ | |||
| import * as React from 'react'; | ||||
| import {useState} from 'react'; | ||||
| import {FormControl, Select, Stack, Table, TableBody, TableCell, TableHead, TableRow} from "@mui/material"; | ||||
| import {useEffect, useState} from 'react'; | ||||
| import { | ||||
|     CardActions, | ||||
|     CardContent, | ||||
|     FormControl, | ||||
|     Select, | ||||
|     Stack, | ||||
|     Table, | ||||
|     TableBody, | ||||
|     TableCell, | ||||
|     TableHead, | ||||
|     TableRow, | ||||
|     useMediaQuery | ||||
| } from "@mui/material"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import Paper from "@mui/material/Paper"; | ||||
| import repository from "../app/Repository"; | ||||
|  | @ -11,6 +23,15 @@ import IconButton from "@mui/material/IconButton"; | |||
| import Container from "@mui/material/Container"; | ||||
| import TextField from "@mui/material/TextField"; | ||||
| import MenuItem from "@mui/material/MenuItem"; | ||||
| import Card from "@mui/material/Card"; | ||||
| import Button from "@mui/material/Button"; | ||||
| import db from "../app/db"; | ||||
| import {useLiveQuery} from "dexie-react-hooks"; | ||||
| import theme from "./theme"; | ||||
| import Dialog from "@mui/material/Dialog"; | ||||
| import DialogTitle from "@mui/material/DialogTitle"; | ||||
| import DialogContent from "@mui/material/DialogContent"; | ||||
| import DialogActions from "@mui/material/DialogActions"; | ||||
| 
 | ||||
| const Preferences = (props) => { | ||||
|     return ( | ||||
|  | @ -26,7 +47,7 @@ const Preferences = (props) => { | |||
| 
 | ||||
| const Notifications = (props) => { | ||||
|     return ( | ||||
|         <Paper sx={{p: 3}}> | ||||
|         <Card sx={{p: 3}}> | ||||
|             <Typography variant="h5"> | ||||
|                 Notifications | ||||
|             </Typography> | ||||
|  | @ -34,7 +55,7 @@ const Notifications = (props) => { | |||
|                 <MinPriority/> | ||||
|                 <DeleteAfter/> | ||||
|             </PrefGroup> | ||||
|         </Paper> | ||||
|         </Card> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
|  | @ -66,7 +87,7 @@ const DeleteAfter = () => { | |||
|         repository.setDeleteAfter(ev.target.value); | ||||
|     } | ||||
|     return ( | ||||
|         <Pref title="Minimum priority"> | ||||
|         <Pref title="Delete notifications"> | ||||
|             <FormControl fullWidth variant="standard" sx={{ m: 1 }}> | ||||
|                 <Select value={deleteAfter} onChange={handleChange}> | ||||
|                     <MenuItem value={0}>Never</MenuItem> | ||||
|  | @ -139,22 +160,81 @@ const DefaultServer = (props) => { | |||
| }; | ||||
| 
 | ||||
| const Users = (props) => { | ||||
|     const [dialogKey, setDialogKey] = useState(0); | ||||
|     const [dialogOpen, setDialogOpen] = useState(false); | ||||
|     const users = useLiveQuery(() => db.users.toArray()); | ||||
|     const handleAddClick = () => { | ||||
|         setDialogKey(prev => prev+1); | ||||
|         setDialogOpen(true); | ||||
|     }; | ||||
|     const handleDialogCancel = () => { | ||||
|         setDialogOpen(false); | ||||
|     }; | ||||
|     const handleDialogSubmit = async (user) => { | ||||
|         setDialogOpen(false); | ||||
|         try { | ||||
|             await db.users.add(user); | ||||
|             console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`); | ||||
|         } catch (e) { | ||||
|             console.log(`[Preferences] Error adding user.`, e); | ||||
|         } | ||||
|     }; | ||||
|     return ( | ||||
|         <Paper sx={{p: 3}}> | ||||
|         <Card sx={{p: 3}}> | ||||
|             <CardContent> | ||||
|                 <Typography variant="h5"> | ||||
|                     Manage users | ||||
|                 </Typography> | ||||
|                 <Paragraph> | ||||
|                 You may manage users for your protected topics here. Please note that since this is a client | ||||
|                 application only, username and password are stored in the browser's local storage. | ||||
|                     Add/remove users for your protected topics here. Please note that username and password are | ||||
|                     stored in the browser's local storage. | ||||
|                 </Paragraph> | ||||
|             <UserTable/> | ||||
|         </Paper> | ||||
|                 {users?.length > 0 && <UserTable users={users}/>} | ||||
|             </CardContent> | ||||
|             <CardActions> | ||||
|                 <Button onClick={handleAddClick}>Add user</Button> | ||||
|                 <UserDialog | ||||
|                     key={`userAddDialog${dialogKey}`} | ||||
|                     open={dialogOpen} | ||||
|                     user={null} | ||||
|                     users={users} | ||||
|                     onCancel={handleDialogCancel} | ||||
|                     onSubmit={handleDialogSubmit} | ||||
|                 /> | ||||
|             </CardActions> | ||||
|         </Card> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const UserTable = () => { | ||||
|     const users = repository.loadUsers(); | ||||
| const UserTable = (props) => { | ||||
|     const [dialogKey, setDialogKey] = useState(0); | ||||
|     const [dialogOpen, setDialogOpen] = useState(false); | ||||
|     const [dialogUser, setDialogUser] = useState(null); | ||||
|     const handleEditClick = (user) => { | ||||
|         setDialogKey(prev => prev+1); | ||||
|         setDialogUser(user); | ||||
|         setDialogOpen(true); | ||||
|     }; | ||||
|     const handleDialogCancel = () => { | ||||
|         setDialogOpen(false); | ||||
|     }; | ||||
|     const handleDialogSubmit = async (user) => { | ||||
|         setDialogOpen(false); | ||||
|         try { | ||||
|             await db.users.put(user); // put() is an upsert
 | ||||
|             console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`); | ||||
|         } catch (e) { | ||||
|             console.log(`[Preferences] Error updating user.`, e); | ||||
|         } | ||||
|     }; | ||||
|     const handleDeleteClick = async (user) => { | ||||
|         try { | ||||
|             await db.users.delete(user.baseUrl); | ||||
|             console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`); | ||||
|         } catch (e) { | ||||
|             console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e); | ||||
|         } | ||||
|     }; | ||||
|     return ( | ||||
|         <Table size="small"> | ||||
|             <TableHead> | ||||
|  | @ -165,27 +245,106 @@ const UserTable = () => { | |||
|                 </TableRow> | ||||
|             </TableHead> | ||||
|             <TableBody> | ||||
|                     {users.map((user, i) => ( | ||||
|                 {props.users?.map(user => ( | ||||
|                     <TableRow | ||||
|                             key={i} | ||||
|                         key={user.baseUrl} | ||||
|                         sx={{ '&:last-child td, &:last-child th': { border: 0 } }} | ||||
|                     > | ||||
|                         <TableCell component="th" scope="row">{user.username}</TableCell> | ||||
|                         <TableCell>{user.baseUrl}</TableCell> | ||||
|                         <TableCell align="right"> | ||||
|                                 <IconButton> | ||||
|                             <IconButton onClick={() => handleEditClick(user)}> | ||||
|                                 <EditIcon/> | ||||
|                             </IconButton> | ||||
|                                 <IconButton> | ||||
|                             <IconButton onClick={() => handleDeleteClick(user)}> | ||||
|                                 <CloseIcon /> | ||||
|                             </IconButton> | ||||
|                         </TableCell> | ||||
|                     </TableRow> | ||||
|                 ))} | ||||
|             </TableBody> | ||||
|             <UserDialog | ||||
|                 key={`userEditDialog${dialogKey}`} | ||||
|                 open={dialogOpen} | ||||
|                 user={dialogUser} | ||||
|                 users={props.users} | ||||
|                 onCancel={handleDialogCancel} | ||||
|                 onSubmit={handleDialogSubmit} | ||||
|             /> | ||||
|         </Table> | ||||
| 
 | ||||
|     ); | ||||
| } | ||||
| }; | ||||
| 
 | ||||
| const UserDialog = (props) => { | ||||
|     const [baseUrl, setBaseUrl] = useState(""); | ||||
|     const [username, setUsername] = useState(""); | ||||
|     const [password, setPassword] = useState(""); | ||||
|     const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||
|     const editMode = props.user !== null; | ||||
|     const addButtonEnabled = (() => { | ||||
|         if (editMode) { | ||||
|             return username.length > 0 && password.length > 0; | ||||
|         } | ||||
|         const baseUrlExists = props.users?.map(user => user.baseUrl).includes(baseUrl); | ||||
|         return !baseUrlExists && username.length > 0 && password.length > 0; | ||||
|     })(); | ||||
|     const handleSubmit = async () => { | ||||
|         props.onSubmit({ | ||||
|             baseUrl: baseUrl, | ||||
|             username: username, | ||||
|             password: password | ||||
|         }) | ||||
|     }; | ||||
|     useEffect(() => { | ||||
|         if (editMode) { | ||||
|             setBaseUrl(props.user.baseUrl); | ||||
|             setUsername(props.user.username); | ||||
|             setPassword(props.user.password); | ||||
|         } | ||||
|     }, []); | ||||
|     return ( | ||||
|         <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}> | ||||
|             <DialogTitle>{editMode ? "Edit user" : "Add user"}</DialogTitle> | ||||
|             <DialogContent> | ||||
|                 {!editMode && <TextField | ||||
|                     autoFocus | ||||
|                     margin="dense" | ||||
|                     id="baseUrl" | ||||
|                     label="Service URL, e.g. https://ntfy.sh" | ||||
|                     value={baseUrl} | ||||
|                     onChange={ev => setBaseUrl(ev.target.value)} | ||||
|                     type="url" | ||||
|                     fullWidth | ||||
|                     variant="standard" | ||||
|                 />} | ||||
|                 <TextField | ||||
|                     autoFocus={editMode} | ||||
|                     margin="dense" | ||||
|                     id="username" | ||||
|                     label="Username, e.g. phil" | ||||
|                     value={username} | ||||
|                     onChange={ev => setUsername(ev.target.value)} | ||||
|                     type="text" | ||||
|                     fullWidth | ||||
|                     variant="standard" | ||||
|                 /> | ||||
|                 <TextField | ||||
|                     margin="dense" | ||||
|                     id="password" | ||||
|                     label="Password" | ||||
|                     type="password" | ||||
|                     value={password} | ||||
|                     onChange={ev => setPassword(ev.target.value)} | ||||
|                     fullWidth | ||||
|                     variant="standard" | ||||
|                 /> | ||||
|             </DialogContent> | ||||
|             <DialogActions> | ||||
|                 <Button onClick={props.onCancel}>Cancel</Button> | ||||
|                 <Button onClick={handleSubmit} disabled={!addButtonEnabled}>{editMode ? "Save" : "Add"}</Button> | ||||
|             </DialogActions> | ||||
|         </Dialog> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default Preferences; | ||||
|  |  | |||
|  | @ -12,8 +12,8 @@ import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/mate | |||
| import theme from "./theme"; | ||||
| import api from "../app/Api"; | ||||
| import {topicUrl, validTopic, validUrl} from "../app/utils"; | ||||
| import User from "../app/User"; | ||||
| import Box from "@mui/material/Box"; | ||||
| import db from "../app/db"; | ||||
| 
 | ||||
| const defaultBaseUrl = "http://127.0.0.1" | ||||
| //const defaultBaseUrl = "https://ntfy.sh"
 | ||||
|  | @ -23,10 +23,10 @@ const SubscribeDialog = (props) => { | |||
|     const [topic, setTopic] = useState(""); | ||||
|     const [showLoginPage, setShowLoginPage] = useState(false); | ||||
|     const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||
|     const handleSuccess = (user) => { | ||||
|     const handleSuccess = () => { | ||||
|         const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME
 | ||||
|         const subscription = new Subscription(actualBaseUrl, topic); | ||||
|         props.onSuccess(subscription, user); | ||||
|         props.onSuccess(subscription); | ||||
|     } | ||||
|     return ( | ||||
|         <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}> | ||||
|  | @ -65,7 +65,7 @@ const SubscribePage = (props) => { | |||
|             return; | ||||
|         } | ||||
|         console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for anonymous user`); | ||||
|         props.onSuccess(null); | ||||
|         props.onSuccess(); | ||||
|     }; | ||||
|     const handleUseAnotherChanged = (e) => { | ||||
|         props.setBaseUrl(""); | ||||
|  | @ -129,7 +129,7 @@ const LoginPage = (props) => { | |||
|     const baseUrl = (props.baseUrl) ? props.baseUrl : defaultBaseUrl; | ||||
|     const topic = props.topic; | ||||
|     const handleLogin = async () => { | ||||
|         const user = new User(baseUrl, username, password); | ||||
|         const user = {baseUrl, username, password}; | ||||
|         const success = await api.auth(baseUrl, topic, user); | ||||
|         if (!success) { | ||||
|             console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); | ||||
|  | @ -137,7 +137,8 @@ const LoginPage = (props) => { | |||
|             return; | ||||
|         } | ||||
|         console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); | ||||
|         props.onSuccess(user); | ||||
|         db.users.put(user); | ||||
|         props.onSuccess(); | ||||
|     }; | ||||
|     return ( | ||||
|         <> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import {styled} from "@mui/styles"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import theme from "./theme"; | ||||
| import Container from "@mui/material/Container"; | ||||
| import {styled} from "@mui/material"; | ||||
| 
 | ||||
| export const Paragraph = styled(Typography)({ | ||||
|   paddingTop: 8, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue