Add Dexie for persistence; user management with dexie; this is the way

This commit is contained in:
Philipp Heckel 2022-03-01 21:23:12 -05:00
parent 8036aa2942
commit 23d275acec
16 changed files with 285 additions and 494 deletions

362
web/package-lock.json generated
View file

@ -11,10 +11,10 @@
"@emotion/styled": "latest", "@emotion/styled": "latest",
"@mui/icons-material": "^5.4.2", "@mui/icons-material": "^5.4.2",
"@mui/material": "latest", "@mui/material": "latest",
"@mui/styles": "^5.4.2", "dexie": "^3.2.1",
"dexie-react-hooks": "^1.1.1",
"react": "latest", "react": "latest",
"react-dom": "latest", "react-dom": "latest",
"react-router-dom": "^6.2.1",
"react-scripts": "^3.0.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": { "node_modules/@mui/system": {
"version": "5.4.2", "version": "5.4.2",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.4.2.tgz", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.4.2.tgz",
@ -5683,15 +5643,6 @@
"node": ">=0.10.0" "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": { "node_modules/css-what": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "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": { "node_modules/diff-sequences": {
"version": "24.9.0", "version": "24.9.0",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", "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", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" "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": { "node_modules/hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" "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": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -9168,11 +9124,6 @@
"node": ">=0.10.0" "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": { "node_modules/is-negative-zero": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@ -10290,88 +10241,6 @@
"node": ">=0.6.0" "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": { "node_modules/jsx-ast-utils": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", "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", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "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": { "node_modules/react-scripts": {
"version": "3.4.4", "version": "3.4.4",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.4.tgz", "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", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" "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": { "node_modules/tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -19518,30 +19358,6 @@
"prop-types": "^15.7.2" "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": { "@mui/system": {
"version": "5.4.2", "version": "5.4.2",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.4.2.tgz", "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": { "css-what": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", "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": { "diff-sequences": {
"version": "24.9.0", "version": "24.9.0",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", "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", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" "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": { "hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" "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": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -24846,11 +24651,6 @@
"is-extglob": "^2.1.1" "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": { "is-negative-zero": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@ -25729,84 +25529,6 @@
"verror": "1.10.0" "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": { "jsx-ast-utils": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", "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", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "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": { "react-scripts": {
"version": "3.4.4", "version": "3.4.4",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.4.tgz", "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", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" "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": { "tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",

View file

@ -12,6 +12,8 @@
"@emotion/styled": "latest", "@emotion/styled": "latest",
"@mui/icons-material": "^5.4.2", "@mui/icons-material": "^5.4.2",
"@mui/material": "latest", "@mui/material": "latest",
"dexie": "^3.2.1",
"dexie-react-hooks": "^1.1.1",
"react": "latest", "react": "latest",
"react-dom": "latest", "react-dom": "latest",
"react-scripts": "^3.0.1" "react-scripts": "^3.0.1"

View file

@ -7,9 +7,11 @@ import {
topicShortUrl, topicShortUrl,
topicUrlJsonPollWithSince topicUrlJsonPollWithSince
} from "./utils"; } from "./utils";
import db from "./db";
class Api { 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 shortUrl = topicShortUrl(baseUrl, topic);
const url = (since) const url = (since)
? topicUrlJsonPollWithSince(baseUrl, topic, since) ? topicUrlJsonPollWithSince(baseUrl, topic, since)
@ -24,7 +26,8 @@ class Api {
return messages; 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); const url = topicUrl(baseUrl, topic);
console.log(`[Api] Publishing message to ${url}`); console.log(`[Api] Publishing message to ${url}`);
await fetch(url, { await fetch(url, {

View file

@ -85,7 +85,7 @@ class Connection {
if (this.since) { if (this.since) {
params.push(`since=${this.since}`); params.push(`since=${this.since}`);
} }
if (this.user !== null) { if (this.user) {
const auth = encodeBase64Url(basicAuth(this.user.username, this.user.password)); const auth = encodeBase64Url(basicAuth(this.user.username, this.user.password));
params.push(`auth=${auth}`); params.push(`auth=${auth}`);
} }

View file

@ -6,7 +6,11 @@ class ConnectionManager {
} }
refresh(subscriptions, users, onNotification) { refresh(subscriptions, users, onNotification) {
if (!subscriptions || !users) {
return;
}
console.log(`[ConnectionManager] Refreshing connections`); console.log(`[ConnectionManager] Refreshing connections`);
console.log(users);
const subscriptionIds = subscriptions.ids(); const subscriptionIds = subscriptions.ids();
const deletedIds = Array.from(this.connections.keys()).filter(id => !subscriptionIds.includes(id)); const deletedIds = Array.from(this.connections.keys()).filter(id => !subscriptionIds.includes(id));
@ -16,7 +20,7 @@ class ConnectionManager {
if (added) { if (added) {
const baseUrl = subscription.baseUrl; const baseUrl = subscription.baseUrl;
const topic = subscription.topic; const topic = subscription.topic;
const user = users.get(baseUrl); const [user] = users.filter(user => user.baseUrl === baseUrl);
const since = subscription.last; const since = subscription.last;
const connection = new Connection(id, baseUrl, topic, user, since, onNotification); const connection = new Connection(id, baseUrl, topic, user, since, onNotification);
this.connections.set(id, connection); this.connections.set(id, connection);

View file

@ -1,7 +1,5 @@
import Subscription from "./Subscription"; import Subscription from "./Subscription";
import Subscriptions from "./Subscriptions"; import Subscriptions from "./Subscriptions";
import Users from "./Users";
import User from "./User";
class Repository { class Repository {
loadSubscriptions() { loadSubscriptions() {
@ -43,40 +41,6 @@ class Repository {
localStorage.setItem('subscriptions', serialized); 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() { loadSelectedSubscriptionId() {
console.log(`[Repository] Loading selected subscription ID from localStorage`); console.log(`[Repository] Loading selected subscription ID from localStorage`);
const selectedSubscriptionId = localStorage.getItem('selectedSubscriptionId'); const selectedSubscriptionId = localStorage.getItem('selectedSubscriptionId');

View file

@ -1,9 +0,0 @@
class User {
constructor(baseUrl, username, password) {
this.baseUrl = baseUrl;
this.username = username;
this.password = password;
}
}
export default User;

View file

@ -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
View 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;

View file

@ -37,7 +37,6 @@ const ActionBar = (props) => {
</Typography> </Typography>
{props.selectedSubscription !== null && <IconSubscribeSettings {props.selectedSubscription !== null && <IconSubscribeSettings
subscription={props.selectedSubscription} subscription={props.selectedSubscription}
users={props.users}
onClearAll={props.onClearAll} onClearAll={props.onClearAll}
onUnsubscribe={props.onUnsubscribe} onUnsubscribe={props.onUnsubscribe}
/>} />}

View file

@ -12,19 +12,20 @@ import connectionManager from "../app/ConnectionManager";
import Subscriptions from "../app/Subscriptions"; import Subscriptions from "../app/Subscriptions";
import Navigation from "./Navigation"; import Navigation from "./Navigation";
import ActionBar from "./ActionBar"; import ActionBar from "./ActionBar";
import Users from "../app/Users";
import notificationManager from "../app/NotificationManager"; import notificationManager from "../app/NotificationManager";
import NoTopics from "./NoTopics"; import NoTopics from "./NoTopics";
import Preferences from "./Preferences"; import Preferences from "./Preferences";
import db from "../app/db";
import {useLiveQuery} from "dexie-react-hooks";
// TODO subscribe dialog: // TODO subscribe dialog:
// - check/use existing user // - check/use existing user
// - add baseUrl // - add baseUrl
// TODO user management
// TODO embed into ntfy server // TODO embed into ntfy server
// TODO make default server functional // TODO make default server functional
// TODO indexeddb for notifications + subscriptions // TODO indexeddb for notifications + subscriptions
// TODO business logic with callbacks // TODO business logic with callbacks
// TODO connection indicator in subscription list
const App = () => { const App = () => {
console.log(`[App] Rendering main view`); console.log(`[App] Rendering main view`);
@ -32,21 +33,18 @@ const App = () => {
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [prefsOpen, setPrefsOpen] = useState(false); const [prefsOpen, setPrefsOpen] = useState(false);
const [subscriptions, setSubscriptions] = useState(new Subscriptions()); const [subscriptions, setSubscriptions] = useState(new Subscriptions());
const [users, setUsers] = useState(new Users());
const [selectedSubscription, setSelectedSubscription] = useState(null); const [selectedSubscription, setSelectedSubscription] = useState(null);
const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted()); const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted());
const users = useLiveQuery(() => db.users.toArray());
const handleSubscriptionClick = (subscriptionId) => { const handleSubscriptionClick = (subscriptionId) => {
setSelectedSubscription(subscriptions.get(subscriptionId)); setSelectedSubscription(subscriptions.get(subscriptionId));
setPrefsOpen(false); setPrefsOpen(false);
} }
const handleSubscribeSubmit = (subscription, user) => { const handleSubscribeSubmit = (subscription) => {
console.log(`[App] New subscription: ${subscription.id}`); console.log(`[App] New subscription: ${subscription.id}`);
if (user !== null) {
setUsers(prev => prev.add(user).clone());
}
setSubscriptions(prev => prev.add(subscription).clone()); setSubscriptions(prev => prev.add(subscription).clone());
setSelectedSubscription(subscription); setSelectedSubscription(subscription);
poll(subscription, user); poll(subscription);
handleRequestPermission(); handleRequestPermission();
}; };
const handleDeleteNotification = (subscriptionId, notificationId) => { const handleDeleteNotification = (subscriptionId, notificationId) => {
@ -80,9 +78,9 @@ const App = () => {
setPrefsOpen(true); setPrefsOpen(true);
setSelectedSubscription(null); setSelectedSubscription(null);
}; };
const poll = (subscription, user) => { const poll = (subscription) => {
const since = subscription.last; const since = subscription.last;
api.poll(subscription.baseUrl, subscription.topic, since, user) api.poll(subscription.baseUrl, subscription.topic, since)
.then(notifications => { .then(notifications => {
setSubscriptions(prev => { setSubscriptions(prev => {
subscription.addNotifications(notifications); subscription.addNotifications(notifications);
@ -94,12 +92,10 @@ const App = () => {
// Define hooks: Note that the order of the hooks is important. The "loading" hooks // Define hooks: Note that the order of the hooks is important. The "loading" hooks
// must be before the "saving" hooks. // must be before the "saving" hooks.
useEffect(() => { useEffect(() => {
// Load subscriptions and users // Load subscriptions
const subscriptions = repository.loadSubscriptions(); const subscriptions = repository.loadSubscriptions();
const selectedSubscriptionId = repository.loadSelectedSubscriptionId(); const selectedSubscriptionId = repository.loadSelectedSubscriptionId();
const users = repository.loadUsers();
setSubscriptions(subscriptions); setSubscriptions(subscriptions);
setUsers(users);
// Set selected subscription // Set selected subscription
const maybeSelectedSubscription = subscriptions.get(selectedSubscriptionId); const maybeSelectedSubscription = subscriptions.get(selectedSubscriptionId);
@ -109,8 +105,7 @@ const App = () => {
// Poll all subscriptions // Poll all subscriptions
subscriptions.forEach((subscriptionId, subscription) => { subscriptions.forEach((subscriptionId, subscription) => {
const user = users.get(subscription.baseUrl); // May be null poll(subscription);
poll(subscription, user);
}); });
}, [/* initial render */]); }, [/* initial render */]);
useEffect(() => { useEffect(() => {
@ -127,7 +122,6 @@ const App = () => {
connectionManager.refresh(subscriptions, users, handleNotification); connectionManager.refresh(subscriptions, users, handleNotification);
}, [subscriptions, users]); }, [subscriptions, users]);
useEffect(() => repository.saveSubscriptions(subscriptions), [subscriptions]); useEffect(() => repository.saveSubscriptions(subscriptions), [subscriptions]);
useEffect(() => repository.saveUsers(users), [users]);
useEffect(() => { useEffect(() => {
const subscriptionId = (selectedSubscription) ? selectedSubscription.id : ""; const subscriptionId = (selectedSubscription) ? selectedSubscription.id : "";
repository.saveSelectedSubscriptionId(subscriptionId) repository.saveSelectedSubscriptionId(subscriptionId)
@ -140,7 +134,6 @@ const App = () => {
<CssBaseline/> <CssBaseline/>
<ActionBar <ActionBar
selectedSubscription={selectedSubscription} selectedSubscription={selectedSubscription}
users={users}
onClearAll={handleDeleteAllNotifications} onClearAll={handleDeleteAllNotifications}
onUnsubscribe={handleUnsubscribe} onUnsubscribe={handleUnsubscribe}
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}

View file

@ -14,7 +14,6 @@ import api from "../app/Api";
const IconSubscribeSettings = (props) => { const IconSubscribeSettings = (props) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const anchorRef = useRef(null); const anchorRef = useRef(null);
const users = props.users;
const handleToggle = () => { const handleToggle = () => {
setOpen((prevOpen) => !prevOpen); setOpen((prevOpen) => !prevOpen);
@ -40,8 +39,7 @@ const IconSubscribeSettings = (props) => {
const handleSendTestMessage = () => { const handleSendTestMessage = () => {
const baseUrl = props.subscription.baseUrl; const baseUrl = props.subscription.baseUrl;
const topic = props.subscription.topic; const topic = props.subscription.topic;
const user = users.get(baseUrl); // May be null api.publish(baseUrl, topic,
api.publish(baseUrl, topic, user,
`This is a test notification sent by the ntfy Web UI at ${new Date().toString()}.`); // FIXME result ignored `This is a test notification sent by the ntfy Web UI at ${new Date().toString()}.`); // FIXME result ignored
setOpen(false); setOpen(false);
} }

View file

@ -57,9 +57,9 @@ const NavList = (props) => {
setSubscribeDialogOpen(false); setSubscribeDialogOpen(false);
setSubscribeDialogKey(prev => prev+1); setSubscribeDialogKey(prev => prev+1);
} }
const handleSubscribeSubmit = (subscription, user) => { const handleSubscribeSubmit = (subscription) => {
handleSubscribeReset(); handleSubscribeReset();
props.onSubscribeSubmit(subscription, user); props.onSubscribeSubmit(subscription);
} }
const showSubscriptionsList = props.subscriptions.size() > 0; const showSubscriptionsList = props.subscriptions.size() > 0;
const showGrantPermissionsBox = props.subscriptions.size() > 0 && !props.notificationsGranted; const showGrantPermissionsBox = props.subscriptions.size() > 0 && !props.notificationsGranted;

View file

@ -1,6 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import {useState} from 'react'; import {useEffect, useState} from 'react';
import {FormControl, Select, Stack, Table, TableBody, TableCell, TableHead, TableRow} from "@mui/material"; import {
CardActions,
CardContent,
FormControl,
Select,
Stack,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
useMediaQuery
} from "@mui/material";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import repository from "../app/Repository"; import repository from "../app/Repository";
@ -11,6 +23,15 @@ import IconButton from "@mui/material/IconButton";
import Container from "@mui/material/Container"; import Container from "@mui/material/Container";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem"; 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) => { const Preferences = (props) => {
return ( return (
@ -26,7 +47,7 @@ const Preferences = (props) => {
const Notifications = (props) => { const Notifications = (props) => {
return ( return (
<Paper sx={{p: 3}}> <Card sx={{p: 3}}>
<Typography variant="h5"> <Typography variant="h5">
Notifications Notifications
</Typography> </Typography>
@ -34,7 +55,7 @@ const Notifications = (props) => {
<MinPriority/> <MinPriority/>
<DeleteAfter/> <DeleteAfter/>
</PrefGroup> </PrefGroup>
</Paper> </Card>
); );
}; };
@ -66,7 +87,7 @@ const DeleteAfter = () => {
repository.setDeleteAfter(ev.target.value); repository.setDeleteAfter(ev.target.value);
} }
return ( return (
<Pref title="Minimum priority"> <Pref title="Delete notifications">
<FormControl fullWidth variant="standard" sx={{ m: 1 }}> <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={deleteAfter} onChange={handleChange}> <Select value={deleteAfter} onChange={handleChange}>
<MenuItem value={0}>Never</MenuItem> <MenuItem value={0}>Never</MenuItem>
@ -139,22 +160,81 @@ const DefaultServer = (props) => {
}; };
const Users = (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 ( return (
<Paper sx={{p: 3}}> <Card sx={{p: 3}}>
<CardContent>
<Typography variant="h5"> <Typography variant="h5">
Manage users Manage users
</Typography> </Typography>
<Paragraph> <Paragraph>
You may manage users for your protected topics here. Please note that since this is a client Add/remove users for your protected topics here. Please note that username and password are
application only, username and password are stored in the browser's local storage. stored in the browser's local storage.
</Paragraph> </Paragraph>
<UserTable/> {users?.length > 0 && <UserTable users={users}/>}
</Paper> </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 UserTable = (props) => {
const users = repository.loadUsers(); 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 ( return (
<Table size="small"> <Table size="small">
<TableHead> <TableHead>
@ -165,27 +245,106 @@ const UserTable = () => {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{users.map((user, i) => ( {props.users?.map(user => (
<TableRow <TableRow
key={i} key={user.baseUrl}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
> >
<TableCell component="th" scope="row">{user.username}</TableCell> <TableCell component="th" scope="row">{user.username}</TableCell>
<TableCell>{user.baseUrl}</TableCell> <TableCell>{user.baseUrl}</TableCell>
<TableCell align="right"> <TableCell align="right">
<IconButton> <IconButton onClick={() => handleEditClick(user)}>
<EditIcon/> <EditIcon/>
</IconButton> </IconButton>
<IconButton> <IconButton onClick={() => handleDeleteClick(user)}>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
<UserDialog
key={`userEditDialog${dialogKey}`}
open={dialogOpen}
user={dialogUser}
users={props.users}
onCancel={handleDialogCancel}
onSubmit={handleDialogSubmit}
/>
</Table> </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; export default Preferences;

View file

@ -12,8 +12,8 @@ import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/mate
import theme from "./theme"; import theme from "./theme";
import api from "../app/Api"; import api from "../app/Api";
import {topicUrl, validTopic, validUrl} from "../app/utils"; import {topicUrl, validTopic, validUrl} from "../app/utils";
import User from "../app/User";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import db from "../app/db";
const defaultBaseUrl = "http://127.0.0.1" const defaultBaseUrl = "http://127.0.0.1"
//const defaultBaseUrl = "https://ntfy.sh" //const defaultBaseUrl = "https://ntfy.sh"
@ -23,10 +23,10 @@ const SubscribeDialog = (props) => {
const [topic, setTopic] = useState(""); const [topic, setTopic] = useState("");
const [showLoginPage, setShowLoginPage] = useState(false); const [showLoginPage, setShowLoginPage] = useState(false);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSuccess = (user) => { const handleSuccess = () => {
const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME
const subscription = new Subscription(actualBaseUrl, topic); const subscription = new Subscription(actualBaseUrl, topic);
props.onSuccess(subscription, user); props.onSuccess(subscription);
} }
return ( return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}> <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
@ -65,7 +65,7 @@ const SubscribePage = (props) => {
return; return;
} }
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for anonymous user`); console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for anonymous user`);
props.onSuccess(null); props.onSuccess();
}; };
const handleUseAnotherChanged = (e) => { const handleUseAnotherChanged = (e) => {
props.setBaseUrl(""); props.setBaseUrl("");
@ -129,7 +129,7 @@ const LoginPage = (props) => {
const baseUrl = (props.baseUrl) ? props.baseUrl : defaultBaseUrl; const baseUrl = (props.baseUrl) ? props.baseUrl : defaultBaseUrl;
const topic = props.topic; const topic = props.topic;
const handleLogin = async () => { const handleLogin = async () => {
const user = new User(baseUrl, username, password); const user = {baseUrl, username, password};
const success = await api.auth(baseUrl, topic, user); const success = await api.auth(baseUrl, topic, user);
if (!success) { if (!success) {
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
@ -137,7 +137,8 @@ const LoginPage = (props) => {
return; return;
} }
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
props.onSuccess(user); db.users.put(user);
props.onSuccess();
}; };
return ( return (
<> <>

View file

@ -1,7 +1,7 @@
import {styled} from "@mui/styles";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import theme from "./theme"; import theme from "./theme";
import Container from "@mui/material/Container"; import Container from "@mui/material/Container";
import {styled} from "@mui/material";
export const Paragraph = styled(Typography)({ export const Paragraph = styled(Typography)({
paddingTop: 8, paddingTop: 8,