This commit is contained in:
2026-01-18 20:56:17 +07:00
parent 02247f9018
commit ae61ece0b0
15 changed files with 1730 additions and 404 deletions

View File

@@ -15,7 +15,6 @@
"@unhead/vue": "^2.1.1", "@unhead/vue": "^2.1.1",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"firebase": "^12.8.0",
"firebase-admin": "^13.6.0", "firebase-admin": "^13.6.0",
"hono": "^4.11.3", "hono": "^4.11.3",
"is-mobile": "^5.0.0", "is-mobile": "^5.0.0",
@@ -258,94 +257,24 @@
"@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="], "@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="],
"@firebase/ai": ["@firebase/ai@2.7.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-types": "0.x" } }, "sha512-PwpCz+TtAMWICM7uQNO0mkSPpUKwrMV4NSwHkbVKDvPKoaQmSlO96vIz+Suw2Ao1EaUUsxYb5LGImHWt/fSnRQ=="],
"@firebase/analytics": ["@firebase/analytics@0.10.19", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg=="],
"@firebase/analytics-compat": ["@firebase/analytics-compat@0.2.25", "", { "dependencies": { "@firebase/analytics": "0.10.19", "@firebase/analytics-types": "0.8.3", "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q=="],
"@firebase/analytics-types": ["@firebase/analytics-types@0.8.3", "", {}, "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg=="],
"@firebase/app": ["@firebase/app@0.14.7", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "idb": "7.1.1", "tslib": "^2.1.0" } }, "sha512-o3ZfnOx0AWBD5n/36p2zPoB0rDDxQP8H/A60zDLvvfRLtW8b3LfCyV97GKpJaAVV1JMMl/BC89EDzMyzxFZxTw=="],
"@firebase/app-check": ["@firebase/app-check@0.11.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w=="],
"@firebase/app-check-compat": ["@firebase/app-check-compat@0.4.0", "", { "dependencies": { "@firebase/app-check": "0.11.0", "@firebase/app-check-types": "0.5.3", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g=="],
"@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.3", "", {}, "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A=="], "@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.3", "", {}, "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A=="],
"@firebase/app-check-types": ["@firebase/app-check-types@0.5.3", "", {}, "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng=="],
"@firebase/app-compat": ["@firebase/app-compat@0.5.7", "", { "dependencies": { "@firebase/app": "0.14.7", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-MO+jfap8IBZQ+K8L2QCiHObyMgpYHrxo4Hc7iJgfb9hjGRW/z1y6LWVdT9wBBK+VJ7cRP2DjAiWQP+thu53hHA=="],
"@firebase/app-types": ["@firebase/app-types@0.9.3", "", {}, "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw=="], "@firebase/app-types": ["@firebase/app-types@0.9.3", "", {}, "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw=="],
"@firebase/auth": ["@firebase/auth@1.12.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@react-native-async-storage/async-storage": "^2.2.0" }, "optionalPeers": ["@react-native-async-storage/async-storage"] }, "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA=="],
"@firebase/auth-compat": ["@firebase/auth-compat@0.6.2", "", { "dependencies": { "@firebase/auth": "1.12.0", "@firebase/auth-types": "0.13.0", "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-8UhCzF6pav9bw/eXA8Zy1QAKssPRYEYXaWagie1ewLTwHkXv6bKp/j6/IwzSYQP67sy/BMFXIFaCCsoXzFLr7A=="],
"@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.4", "", {}, "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA=="], "@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.4", "", {}, "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA=="],
"@firebase/auth-types": ["@firebase/auth-types@0.13.0", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg=="],
"@firebase/component": ["@firebase/component@0.7.0", "", { "dependencies": { "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg=="], "@firebase/component": ["@firebase/component@0.7.0", "", { "dependencies": { "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg=="],
"@firebase/data-connect": ["@firebase/data-connect@0.3.12", "", { "dependencies": { "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-baPddcoNLj/+vYo+HSJidJUdr5W4OkhT109c5qhR8T1dJoZcyJpkv/dFpYlw/VJ3dV66vI8GHQFrmAZw/xUS4g=="],
"@firebase/database": ["@firebase/database@1.1.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg=="], "@firebase/database": ["@firebase/database@1.1.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg=="],
"@firebase/database-compat": ["@firebase/database-compat@2.1.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/database": "1.1.0", "@firebase/database-types": "1.0.16", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg=="], "@firebase/database-compat": ["@firebase/database-compat@2.1.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/database": "1.1.0", "@firebase/database-types": "1.0.16", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg=="],
"@firebase/database-types": ["@firebase/database-types@1.0.16", "", { "dependencies": { "@firebase/app-types": "0.9.3", "@firebase/util": "1.13.0" } }, "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw=="], "@firebase/database-types": ["@firebase/database-types@1.0.16", "", { "dependencies": { "@firebase/app-types": "0.9.3", "@firebase/util": "1.13.0" } }, "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw=="],
"@firebase/firestore": ["@firebase/firestore@4.10.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "@firebase/webchannel-wrapper": "1.0.5", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-fgF6EbpoagGWh5Vwfu/7/jYgBFwUCwTlPNVF/aSjHcoEDRXpRsIqVfAFTp1LD+dWAUcAKEK3h+osk8spMJXtxA=="],
"@firebase/firestore-compat": ["@firebase/firestore-compat@0.4.4", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/firestore": "4.10.0", "@firebase/firestore-types": "3.0.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-JvxxIgi+D5v9BecjLA1YomdyF7LA6CXhJuVK10b4GtRrB3m2O2hT1jJWbKYZYHUAjTaajkvnos+4U5VNxqkI2w=="],
"@firebase/firestore-types": ["@firebase/firestore-types@3.0.3", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q=="],
"@firebase/functions": ["@firebase/functions@0.13.1", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/messaging-interop-types": "0.2.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw=="],
"@firebase/functions-compat": ["@firebase/functions-compat@0.4.1", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/functions": "0.13.1", "@firebase/functions-types": "0.6.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ=="],
"@firebase/functions-types": ["@firebase/functions-types@0.6.3", "", {}, "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg=="],
"@firebase/installations": ["@firebase/installations@0.6.19", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", "idb": "7.1.1", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q=="],
"@firebase/installations-compat": ["@firebase/installations-compat@0.2.19", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/installations-types": "0.5.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ=="],
"@firebase/installations-types": ["@firebase/installations-types@0.5.3", "", { "peerDependencies": { "@firebase/app-types": "0.x" } }, "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA=="],
"@firebase/logger": ["@firebase/logger@0.5.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g=="], "@firebase/logger": ["@firebase/logger@0.5.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g=="],
"@firebase/messaging": ["@firebase/messaging@0.12.23", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/messaging-interop-types": "0.2.3", "@firebase/util": "1.13.0", "idb": "7.1.1", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg=="],
"@firebase/messaging-compat": ["@firebase/messaging-compat@0.2.23", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/messaging": "0.12.23", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg=="],
"@firebase/messaging-interop-types": ["@firebase/messaging-interop-types@0.2.3", "", {}, "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q=="],
"@firebase/performance": ["@firebase/performance@0.7.9", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0", "web-vitals": "^4.2.4" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ=="],
"@firebase/performance-compat": ["@firebase/performance-compat@0.2.22", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/performance": "0.7.9", "@firebase/performance-types": "0.2.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg=="],
"@firebase/performance-types": ["@firebase/performance-types@0.2.3", "", {}, "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ=="],
"@firebase/remote-config": ["@firebase/remote-config@0.8.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-sJz7C2VACeE257Z/3kY9Ap2WXbFsgsDLfaGfZmmToKAK39ipXxFan+vzB9CSbF6mP7bzjyzEnqPcMXhAnYE6fQ=="],
"@firebase/remote-config-compat": ["@firebase/remote-config-compat@0.2.21", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/remote-config": "0.8.0", "@firebase/remote-config-types": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-9+lm0eUycxbu8GO25JfJe4s6R2xlDqlVt0CR6CvN9E6B4AFArEV4qfLoDVRgIEB7nHKwvH2nYRocPWfmjRQTnw=="],
"@firebase/remote-config-types": ["@firebase/remote-config-types@0.5.0", "", {}, "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg=="],
"@firebase/storage": ["@firebase/storage@0.14.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA=="],
"@firebase/storage-compat": ["@firebase/storage-compat@0.4.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/storage": "0.14.0", "@firebase/storage-types": "0.8.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g=="],
"@firebase/storage-types": ["@firebase/storage-types@0.8.3", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg=="],
"@firebase/util": ["@firebase/util@1.13.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ=="], "@firebase/util": ["@firebase/util@1.13.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ=="],
"@firebase/webchannel-wrapper": ["@firebase/webchannel-wrapper@1.0.5", "", {}, "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw=="],
"@google-cloud/firestore": ["@google-cloud/firestore@7.11.6", "", { "dependencies": { "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", "google-gax": "^4.3.3", "protobufjs": "^7.2.6" } }, "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw=="], "@google-cloud/firestore": ["@google-cloud/firestore@7.11.6", "", { "dependencies": { "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", "google-gax": "^4.3.3", "protobufjs": "^7.2.6" } }, "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw=="],
"@google-cloud/paginator": ["@google-cloud/paginator@5.0.2", "", { "dependencies": { "arrify": "^2.0.0", "extend": "^3.0.2" } }, "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg=="], "@google-cloud/paginator": ["@google-cloud/paginator@5.0.2", "", { "dependencies": { "arrify": "^2.0.0", "extend": "^3.0.2" } }, "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg=="],
@@ -356,7 +285,7 @@
"@google-cloud/storage": ["@google-cloud/storage@7.18.0", "", { "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", "@google-cloud/promisify": "<4.1.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", "fast-xml-parser": "^4.4.1", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", "mime": "^3.0.0", "p-limit": "^3.0.1", "retry-request": "^7.0.0", "teeny-request": "^9.0.0", "uuid": "^8.0.0" } }, "sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ=="], "@google-cloud/storage": ["@google-cloud/storage@7.18.0", "", { "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", "@google-cloud/promisify": "<4.1.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", "fast-xml-parser": "^4.4.1", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", "mime": "^3.0.0", "p-limit": "^3.0.1", "retry-request": "^7.0.0", "teeny-request": "^9.0.0", "uuid": "^8.0.0" } }, "sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.9.15", "", { "dependencies": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" } }, "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
@@ -896,8 +825,6 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"firebase": ["firebase@12.8.0", "", { "dependencies": { "@firebase/ai": "2.7.0", "@firebase/analytics": "0.10.19", "@firebase/analytics-compat": "0.2.25", "@firebase/app": "0.14.7", "@firebase/app-check": "0.11.0", "@firebase/app-check-compat": "0.4.0", "@firebase/app-compat": "0.5.7", "@firebase/app-types": "0.9.3", "@firebase/auth": "1.12.0", "@firebase/auth-compat": "0.6.2", "@firebase/data-connect": "0.3.12", "@firebase/database": "1.1.0", "@firebase/database-compat": "2.1.0", "@firebase/firestore": "4.10.0", "@firebase/firestore-compat": "0.4.4", "@firebase/functions": "0.13.1", "@firebase/functions-compat": "0.4.1", "@firebase/installations": "0.6.19", "@firebase/installations-compat": "0.2.19", "@firebase/messaging": "0.12.23", "@firebase/messaging-compat": "0.2.23", "@firebase/performance": "0.7.9", "@firebase/performance-compat": "0.2.22", "@firebase/remote-config": "0.8.0", "@firebase/remote-config-compat": "0.2.21", "@firebase/storage": "0.14.0", "@firebase/storage-compat": "0.4.0", "@firebase/util": "1.13.0" } }, "sha512-S1tCIR3ENecee0tY2cfTHfMkXqkitHfbsvqpCtvsT0Zi9vDB7A4CodAjHfHCjVvu/XtGy1LHLjOasVcF10rCVw=="],
"firebase-admin": ["firebase-admin@13.6.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "^2.0.0", "@firebase/database-types": "^1.0.6", "@types/node": "^22.8.7", "farmhash-modern": "^1.1.0", "fast-deep-equal": "^3.1.1", "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", "uuid": "^11.0.2" }, "optionalDependencies": { "@google-cloud/firestore": "^7.11.0", "@google-cloud/storage": "^7.14.0" } }, "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw=="], "firebase-admin": ["firebase-admin@13.6.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "^2.0.0", "@firebase/database-types": "^1.0.6", "@types/node": "^22.8.7", "farmhash-modern": "^1.1.0", "fast-deep-equal": "^3.1.1", "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", "uuid": "^11.0.2" }, "optionalDependencies": { "@google-cloud/firestore": "^7.11.0", "@google-cloud/storage": "^7.14.0" } }, "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw=="],
"form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="], "form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="],
@@ -952,8 +879,6 @@
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
@@ -1184,8 +1109,6 @@
"vue-router": ["vue-router@4.6.4", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="], "vue-router": ["vue-router@4.6.4", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="],
"web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
@@ -1234,6 +1157,8 @@
"@google-cloud/storage/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "@google-cloud/storage/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
"@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
@@ -1262,8 +1187,6 @@
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"google-gax/@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
"google-gax/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "google-gax/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
@@ -1308,8 +1231,6 @@
"firebase-admin/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "firebase-admin/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"google-gax/@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
"lru-memoizer/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "lru-memoizer/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],

518
docs.json Normal file
View File

@@ -0,0 +1,518 @@
{
"swagger": "2.0",
"info": {
"description": "This is the API server for Stream application.",
"title": "Stream API",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"url": "http://www.swagger.io/support",
"email": "support@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0"
},
"host": "localhost:8080",
"basePath": "/",
"paths": {
"/payments": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Create a new payment",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"payment"
],
"summary": "Create Payment",
"parameters": [
{
"description": "Payment Info",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/payment.CreatePaymentRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/plans": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get all active plans",
"produces": [
"application/json"
],
"tags": [
"plan"
],
"summary": "List Plans",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/model.Plan"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/videos": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get paginated videos",
"produces": [
"application/json"
],
"tags": [
"video"
],
"summary": "List Videos",
"parameters": [
{
"type": "integer",
"default": 1,
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"default": 10,
"description": "Page size",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Create video record after upload",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"video"
],
"summary": "Create Video",
"parameters": [
{
"description": "Video Info",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/video.CreateVideoRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.Video"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/videos/upload-url": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Generate presigned URL for video upload",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"video"
],
"summary": "Get Upload URL",
"parameters": [
{
"description": "File Info",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/video.UploadURLRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/videos/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get video details by ID",
"produces": [
"application/json"
],
"tags": [
"video"
],
"summary": "Get Video",
"parameters": [
{
"type": "string",
"description": "Video ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.Video"
}
}
}
]
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
}
},
"definitions": {
"model.Plan": {
"type": "object",
"properties": {
"cycle": {
"type": "string"
},
"description": {
"type": "string"
},
"duration_limit": {
"type": "integer"
},
"features": {
"type": "string"
},
"id": {
"type": "string"
},
"is_active": {
"type": "boolean"
},
"name": {
"type": "string"
},
"price": {
"type": "number"
},
"quality_limit": {
"type": "string"
},
"storage_limit": {
"type": "integer"
},
"upload_limit": {
"type": "integer"
}
}
},
"model.Video": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"description": {
"type": "string"
},
"duration": {
"type": "integer"
},
"format": {
"type": "string"
},
"hls_path": {
"type": "string"
},
"hls_token": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"processing_status": {
"type": "string"
},
"size": {
"type": "integer"
},
"status": {
"type": "string"
},
"storage_type": {
"type": "string"
},
"thumbnail": {
"type": "string"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"user_id": {
"type": "string"
},
"views": {
"type": "integer"
}
}
},
"payment.CreatePaymentRequest": {
"type": "object",
"required": [
"amount",
"plan_id"
],
"properties": {
"amount": {
"type": "number"
},
"plan_id": {
"type": "string"
}
}
},
"response.Response": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"data": {},
"message": {
"type": "string"
}
}
},
"video.CreateVideoRequest": {
"type": "object",
"required": [
"size",
"title",
"url"
],
"properties": {
"description": {
"type": "string"
},
"duration": {
"description": "Maybe client knows, or we process later",
"type": "integer"
},
"format": {
"type": "string"
},
"size": {
"type": "integer"
},
"title": {
"type": "string"
},
"url": {
"description": "The S3 Key or Full URL",
"type": "string"
}
}
},
"video.UploadURLRequest": {
"type": "object",
"required": [
"content_type",
"filename",
"size"
],
"properties": {
"content_type": {
"type": "string"
},
"filename": {
"type": "string"
},
"size": {
"type": "integer"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}

View File

@@ -19,7 +19,6 @@
"@unhead/vue": "^2.1.1", "@unhead/vue": "^2.1.1",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"firebase": "^12.8.0",
"firebase-admin": "^13.6.0", "firebase-admin": "^13.6.0",
"hono": "^4.11.3", "hono": "^4.11.3",
"is-mobile": "^5.0.0", "is-mobile": "^5.0.0",

678
src/api/client.ts Normal file
View File

@@ -0,0 +1,678 @@
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
import { customFetch } from "@httpClientAdapter";
export interface AuthForgotPasswordRequest {
email: string;
}
export interface AuthLoginRequest {
email: string;
password: string;
}
export interface AuthRegisterRequest {
email: string;
/** @minLength 6 */
password: string;
username: string;
}
export interface AuthResetPasswordRequest {
/** @minLength 6 */
new_password: string;
token: string;
}
export interface ModelPlan {
cycle?: string;
description?: string;
duration_limit?: number;
features?: string;
id?: string;
is_active?: boolean;
name?: string;
price?: number;
quality_limit?: string;
storage_limit?: number;
upload_limit?: number;
}
export interface ModelUser {
avatar?: string;
created_at?: string;
email?: string;
google_id?: string;
id?: string;
password?: string;
plan_id?: string;
role?: string;
storage_used?: number;
updated_at?: string;
username?: string;
}
export interface ModelVideo {
created_at?: string;
description?: string;
duration?: number;
format?: string;
hls_path?: string;
hls_token?: string;
id?: string;
name?: string;
processing_status?: string;
size?: number;
status?: string;
storage_type?: string;
thumbnail?: string;
title?: string;
updated_at?: string;
url?: string;
user_id?: string;
views?: number;
}
export interface PaymentCreatePaymentRequest {
amount: number;
plan_id: string;
}
export interface ResponseResponse {
code?: number;
message?: string;
}
export interface VideoCreateVideoRequest {
description?: string;
/** Maybe client knows, or we process later */
duration?: number;
format?: string;
size: number;
title: string;
/** The S3 Key or Full URL */
url: string;
}
export interface VideoUploadURLRequest {
content_type: string;
filename: string;
size: number;
}
export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
export interface FullRequestParams extends Omit<RequestInit, "body"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat;
/** request body */
body?: unknown;
/** base url */
baseUrl?: string;
/** request cancellation token */
cancelToken?: CancelToken;
}
export type RequestParams = Omit<
FullRequestParams,
"body" | "method" | "query" | "path"
>;
export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string;
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
securityWorker?: (
securityData: SecurityDataType | null,
) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: typeof fetch;
}
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
extends Response {
data: D;
error: E;
}
type CancelToken = Symbol | string | number;
export enum ContentType {
Json = "application/json",
JsonApi = "application/vnd.api+json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = "";
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
fetch(...fetchParams);
private baseApiParams: RequestParams = {
credentials: "same-origin",
headers: {},
redirect: "follow",
referrerPolicy: "no-referrer",
};
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig);
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
};
protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
}
protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key]);
}
protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key];
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
}
protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {};
const keys = Object.keys(query).filter(
(key) => "undefined" !== typeof query[key],
);
return keys
.map((key) =>
Array.isArray(query[key])
? this.addArrayQueryParam(query, key)
: this.addQueryParam(query, key),
)
.join("&");
}
protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery);
return queryString ? `?${queryString}` : "";
}
private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input: any) =>
input !== null && (typeof input === "object" || typeof input === "string")
? JSON.stringify(input)
: input,
[ContentType.JsonApi]: (input: any) =>
input !== null && (typeof input === "object" || typeof input === "string")
? JSON.stringify(input)
: input,
[ContentType.Text]: (input: any) =>
input !== null && typeof input !== "string"
? JSON.stringify(input)
: input,
[ContentType.FormData]: (input: any) => {
if (input instanceof FormData) {
return input;
}
return Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
formData.append(
key,
property instanceof Blob
? property
: typeof property === "object" && property !== null
? JSON.stringify(property)
: `${property}`,
);
return formData;
}, new FormData());
},
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
};
protected mergeRequestParams(
params1: RequestParams,
params2?: RequestParams,
): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}
protected createAbortSignal = (
cancelToken: CancelToken,
): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
return abortController.signal;
}
return void 0;
}
const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController);
return abortController.signal;
};
public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
abortController.abort();
this.abortControllers.delete(cancelToken);
}
};
public request = async <T = any, E = any>({
body,
secure,
path,
type,
query,
format,
baseUrl,
cancelToken,
...params
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
const secureParams =
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
this.securityWorker &&
(await this.securityWorker(this.securityData))) ||
{};
const requestParams = this.mergeRequestParams(params, secureParams);
const queryString = query && this.toQueryString(query);
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
const responseFormat = format || requestParams.format;
return this.customFetch(
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
{
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData
? { "Content-Type": type }
: {}),
},
signal:
(cancelToken
? this.createAbortSignal(cancelToken)
: requestParams.signal) || null,
body:
typeof body === "undefined" || body === null
? null
: payloadFormatter(body)
},
).then(async (response) => {
const r = response as HttpResponse<T, E>;
r.data = null as unknown as T;
r.error = null as unknown as E;
const responseToParse = responseFormat ? response.clone() : response;
const data = !responseFormat
? r
: await responseToParse[responseFormat]()
.then((data) => {
if (r.ok) {
r.data = data;
} else {
r.error = data;
}
return r;
})
.catch((e) => {
r.error = e;
return r;
});
if (cancelToken) {
this.abortControllers.delete(cancelToken);
}
if (!response.ok) throw data;
return data;
});
};
}
/**
* @title Stream API
* @version 1.0
* @license Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)
* @termsOfService http://swagger.io/terms/
* @contact API Support <support@swagger.io> (http://www.swagger.io/support)
*
* This is the API server for Stream application.
*/
export class Api<
SecurityDataType extends unknown,
> extends HttpClient<SecurityDataType> {
auth = {
/**
* @description Request password reset link
*
* @tags auth
* @name ForgotPasswordCreate
* @summary Forgot Password
* @request POST:/auth/forgot-password
*/
forgotPasswordCreate: (
request: AuthForgotPasswordRequest,
params: RequestParams = {},
) =>
this.request<ResponseResponse, ResponseResponse>({
path: `/auth/forgot-password`,
method: "POST",
body: request,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* @description Callback for Google Login
*
* @tags auth
* @name GoogleCallbackList
* @summary Google Callback
* @request GET:/auth/google/callback
*/
googleCallbackList: (params: RequestParams = {}) =>
this.request<
ResponseResponse & {
data?: ModelUser;
},
ResponseResponse
>({
path: `/auth/google/callback`,
method: "GET",
...params,
}),
/**
* @description Redirect to Google for Login
*
* @tags auth
* @name GoogleLoginList
* @summary Google Login
* @request GET:/auth/google/login
*/
googleLoginList: (params: RequestParams = {}) =>
this.request<any, void>({
path: `/auth/google/login`,
method: "GET",
...params,
}),
/**
* @description Login with email and password
*
* @tags auth
* @name LoginCreate
* @summary Login
* @request POST:/auth/login
*/
loginCreate: (request: AuthLoginRequest, params: RequestParams = {}) =>
this.request<
ResponseResponse & {
data?: ModelUser;
},
ResponseResponse
>({
path: `/auth/login`,
method: "POST",
body: request,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* @description Logout user and clear cookies
*
* @tags auth
* @name LogoutCreate
* @summary Logout
* @request POST:/auth/logout
*/
logoutCreate: (params: RequestParams = {}) =>
this.request<ResponseResponse, any>({
path: `/auth/logout`,
method: "POST",
type: ContentType.Json,
format: "json",
...params,
}),
/**
* @description Register a new user
*
* @tags auth
* @name RegisterCreate
* @summary Register
* @request POST:/auth/register
*/
registerCreate: (
request: AuthRegisterRequest,
params: RequestParams = {},
) =>
this.request<ResponseResponse, ResponseResponse>({
path: `/auth/register`,
method: "POST",
body: request,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* @description Reset password using token
*
* @tags auth
* @name ResetPasswordCreate
* @summary Reset Password
* @request POST:/auth/reset-password
*/
resetPasswordCreate: (
request: AuthResetPasswordRequest,
params: RequestParams = {},
) =>
this.request<ResponseResponse, ResponseResponse>({
path: `/auth/reset-password`,
method: "POST",
body: request,
type: ContentType.Json,
format: "json",
...params,
}),
};
payments = {
/**
* @description Create a new payment
*
* @tags payment
* @name PaymentsCreate
* @summary Create Payment
* @request POST:/payments
* @secure
*/
paymentsCreate: (
request: PaymentCreatePaymentRequest,
params: RequestParams = {},
) =>
this.request<ResponseResponse, ResponseResponse>({
path: `/payments`,
method: "POST",
body: request,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),
};
plans = {
/**
* @description Get all active plans
*
* @tags plan
* @name PlansList
* @summary List Plans
* @request GET:/plans
* @secure
*/
plansList: (params: RequestParams = {}) =>
this.request<
ResponseResponse & {
data?: ModelPlan[];
},
ResponseResponse
>({
path: `/plans`,
method: "GET",
secure: true,
format: "json",
...params,
}),
};
videos = {
/**
* @description Get paginated videos
*
* @tags video
* @name VideosList
* @summary List Videos
* @request GET:/videos
* @secure
*/
videosList: (
query?: {
/**
* Page number
* @default 1
*/
page?: number;
/**
* Page size
* @default 10
*/
limit?: number;
},
params: RequestParams = {},
) =>
this.request<ResponseResponse, ResponseResponse>({
path: `/videos`,
method: "GET",
query: query,
secure: true,
format: "json",
...params,
}),
/**
* @description Create video record after upload
*
* @tags video
* @name VideosCreate
* @summary Create Video
* @request POST:/videos
* @secure
*/
videosCreate: (
request: VideoCreateVideoRequest,
params: RequestParams = {},
) =>
this.request<
ResponseResponse & {
data?: ModelVideo;
},
ResponseResponse
>({
path: `/videos`,
method: "POST",
body: request,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* @description Generate presigned URL for video upload
*
* @tags video
* @name UploadUrlCreate
* @summary Get Upload URL
* @request POST:/videos/upload-url
* @secure
*/
uploadUrlCreate: (
request: VideoUploadURLRequest,
params: RequestParams = {},
) =>
this.request<ResponseResponse, ResponseResponse>({
path: `/videos/upload-url`,
method: "POST",
body: request,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* @description Get video details by ID
*
* @tags video
* @name VideosDetail
* @summary Get Video
* @request GET:/videos/{id}
* @secure
*/
videosDetail: (id: string, params: RequestParams = {}) =>
this.request<
ResponseResponse & {
data?: ModelVideo;
},
ResponseResponse
>({
path: `/videos/${id}`,
method: "GET",
secure: true,
format: "json",
...params,
}),
};
}
export const client = new Api({
baseUrl: 'http://localhost:8080',
customFetch
});

View File

@@ -1,65 +1,6 @@
import { TinyRpcClientAdapter, TinyRpcError } from "@hiogawa/tiny-rpc"; export const customFetch = (url: string, options: RequestInit) => {
import { Result } from "@hiogawa/utils"; return fetch(url, {
...options,
const GET_PAYLOAD_PARAM = "payload"; credentials: "include",
});
export function httpClientAdapter(opts: {
url: string;
pathsForGET?: string[];
headers?: () => Promise<Record<string, string>> | Record<string, string>;
}): TinyRpcClientAdapter {
return {
send: async (data) => {
const url = [opts.url, data.path].join("/");
const payload = JSON.stringify(data.args);
const method = opts.pathsForGET?.includes(data.path)
? "GET"
: "POST";
const extraHeaders = opts.headers ? await opts.headers() : {};
let req: Request;
if (method === "GET") {
req = new Request(
url +
"?" +
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload }),
{
headers: extraHeaders
}
);
} else {
req = new Request(url, {
method: "POST",
body: payload,
headers: {
"content-type": "application/json; charset=utf-8",
...extraHeaders
},
credentials: "include",
});
}
let res: Response;
res = await fetch(req);
if (!res.ok) {
// throw new Error(`HTTP error: ${res.status}`);
throw new Error(
JSON.stringify({
status: res.status,
statusText: res.statusText,
data: { message: await res.text() },
internal: true,
})
);
// throw TinyRpcError.deserialize(res.status);
}
const result: Result<unknown, unknown> = JSON.parse(
await res.text()
);
if (!result.ok) {
throw TinyRpcError.deserialize(result.value);
}
return result.value;
},
};
} }

View File

@@ -1,70 +1,19 @@
import { TinyRpcClientAdapter, TinyRpcError } from "@hiogawa/tiny-rpc";
import { Result } from "@hiogawa/utils";
import { tryGetContext } from "hono/context-storage"; import { tryGetContext } from "hono/context-storage";
const GET_PAYLOAD_PARAM = "payload"; export const customFetch = async (url: string, options: RequestInit) => {
options.credentials = "include";
export function httpClientAdapter(opts: { if (!options.headers) {
url: string; options.headers = {};
pathsForGET?: string[]; }
headers?: () => Promise<Record<string, string>> | Record<string, string>; if (import.meta.env.SSR) {
}): TinyRpcClientAdapter { const c = tryGetContext<any>();
return { if (!c) {
send: async (data) => { throw new Error("Hono context not found in SSR");
const url = [opts.url, data.path].join("/"); }
const payload = JSON.stringify(data.args); Object.entries(c.req.header()).forEach(([k, v]) => {
const method = opts.pathsForGET?.includes(data.path) Object.assign(options.headers!, { [k]: v });
? "GET" });
: "POST"; return await c.get("fetch")(url, options);
let req: Request; }
if (method === "GET") { return fetch(url, options);
req = new Request(
url +
"?" +
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload })
);
} else {
req = new Request(url, {
method: "POST",
body: payload,
headers: {
"content-type": "application/json; charset=utf-8",
},
credentials: "include",
});
}
let res: Response;
if (import.meta.env.SSR) {
const c = tryGetContext<any>();
if (!c) {
throw new Error("Hono context not found in SSR");
}
Object.entries(c.req.header()).forEach(([k, v]) => {
req.headers.append(k, v);
});
res = await c.get("fetch")(req);
} else {
res = await fetch(req);
}
if (!res.ok) {
// throw new Error(`HTTP error: ${res.status}`);
throw new Error(
JSON.stringify({
status: res.status,
statusText: res.statusText,
data: { message: await res.text() },
internal: true,
})
);
// throw TinyRpcError.deserialize(res.status);
}
const result: Result<unknown, unknown> = JSON.parse(
await res.text()
);
if (!result.ok) {
throw TinyRpcError.deserialize(result.value);
}
return result.value;
},
};
} }

View File

@@ -1,30 +0,0 @@
import {
proxyTinyRpc,
TinyRpcClientAdapter,
TinyRpcError,
} from "@hiogawa/tiny-rpc";
import type { RpcRoutes } from "./rpc";
import { Result } from "@hiogawa/utils";
import { httpClientAdapter } from "@httpClientAdapter";
// console.log("httpClientAdapter module:", httpClientAdapter.toString());
declare let __host__: string;
const endpoint = "/rpc";
const url = import.meta.env.SSR ? "http://localhost" : "";
import { auth } from "../lib/firebase";
export const client = proxyTinyRpc<RpcRoutes>({
adapter: httpClientAdapter({
url: url + endpoint,
pathsForGET: [],
headers: async () => {
if (import.meta.env.SSR) return {}; // No client auth on server for now
const user = auth.currentUser;
if (user) {
// Force refresh if needed or just get token
const token = await user.getIdToken();
return { Authorization: `Bearer ${token}` };
}
return {};
}
}),
});

View File

@@ -1,47 +0,0 @@
import { initializeApp } from "firebase/app";
import { createUserWithEmailAndPassword, getAuth, GoogleAuthProvider, sendPasswordResetEmail, signInWithEmailAndPassword, signInWithPopup } from "firebase/auth";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyBTr0L5qxdrVEtWuP2oAicJXQvVyeXkMts",
authDomain: "trello-7ea39.firebaseapp.com",
projectId: "trello-7ea39",
storageBucket: "trello-7ea39.firebasestorage.app",
messagingSenderId: "321067890572",
appId: "1:321067890572:web:e34e1e657125d37be688a9"
};
// Initialize Firebase
const appFirebase = initializeApp(firebaseConfig);
const provider = new GoogleAuthProvider();
export const auth = getAuth(appFirebase);
export const googleAuth = () => signInWithPopup(auth, provider).then((result) => {
console.log('User signed in:', result.user);
return result;
})
export const emailAuth = (username: string, password: string) => {
return signInWithEmailAndPassword(auth, username, password)
}
export const forgotPassword = (email: string) => {
return sendPasswordResetEmail(auth, email)
.then(() => {
console.log('Password reset email sent');
})
.catch((error) => {
console.error('Error sending password reset email:', error);
throw error;
});
}
export const signUp = (email: string, password: string) => {
return createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
console.log('User signed up:', userCredential.user);
return userCredential.user;
})
.catch((error) => {
console.error('Error signing up:', error);
throw error;
});
}

View File

@@ -1,12 +0,0 @@
// import { initializeApp, getApps, cert } from 'firebase-admin/app';
// import { getAuth } from 'firebase-admin/auth';
// import certJson from './cert.json';
// const firebaseAdminConfig = {
// credential: cert(certJson as any)
// };
// if (getApps().length === 0) {
// initializeApp(firebaseAdminConfig);
// }
// export const adminAuth = getAuth();

View File

@@ -76,7 +76,7 @@ const routes: RouteData[] = [
{ {
path: "upload", path: "upload",
name: "upload", name: "upload",
component: () => import("./add/Add.vue"), component: () => import("./upload/Upload.vue"),
meta: { meta: {
head: { head: {
title: 'Upload - Holistream', title: 'Upload - Holistream',
@@ -86,7 +86,7 @@ const routes: RouteData[] = [
{ {
path: "video", path: "video",
name: "video", name: "video",
component: () => import("./add/Add.vue"), component: () => import("./video/Videos.vue"),
meta: { meta: {
head: { head: {
title: 'Videos - Holistream', title: 'Videos - Holistream',
@@ -99,7 +99,7 @@ const routes: RouteData[] = [
{ {
path: "plans", path: "plans",
name: "plans", name: "plans",
component: () => import("./add/Add.vue"), component: () => import("./plans/Plans.vue"),
meta: { meta: {
head: { head: {
title: 'Plans & Billing', title: 'Plans & Billing',
@@ -130,28 +130,28 @@ const routes: RouteData[] = [
}, },
]; ];
const createAppRouter = () => { const createAppRouter = () => {
const router = createRouter({ const router = createRouter({
history: import.meta.env.SSR history: import.meta.env.SSR
? createMemoryHistory() // server ? createMemoryHistory() // server
: createWebHistory(), // client : createWebHistory(), // client
routes, routes,
}); });
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const auth = useAuthStore(); const auth = useAuthStore();
const head = inject(headSymbol); const head = inject(headSymbol);
(head as any).push(to.meta.head || {}); (head as any).push(to.meta.head || {});
if (to.matched.some((record) => record.meta.requiresAuth)) { if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!auth.user) { if (!auth.user) {
next({ name: "login" }); next({ name: "login" });
} else {
next();
}
} else { } else {
next(); next();
} }
} else { });
next(); return router;
}
});
return router;
} }
export default createAppRouter; export default createAppRouter;

117
src/routes/plans/Plans.vue Normal file
View File

@@ -0,0 +1,117 @@
<template>
<div class="p-6">
<h1 class="text-3xl font-bold mb-6">Choose Your Plan</h1>
<div v-if="loading" class="flex justify-center">
<div class="i-svg-spinners-180-ring-with-bg text-4xl"></div>
</div>
<div v-else-if="error" class="text-red-500">
{{ error }}
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div v-for="plan in plans" :key="plan.id" class="border rounded-lg p-6 shadow-md bg-white dark:bg-gray-800 flex flex-col">
<h2 class="text-xl font-semibold mb-2">{{ plan.name }}</h2>
<div class="text-3xl font-bold mb-4">${{ plan.price }}<span class="text-sm font-normal text-gray-500">/{{ plan.cycle }}</span></div>
<p class="text-gray-600 dark:text-gray-300 mb-6 flex-grow">{{ plan.description }}</p>
<ul class="mb-6 space-y-2">
<li class="flex items-center">
<span class="i-heroicons-check-circle text-green-500 mr-2"></span>
<span>Storage: {{ formatBytes(plan.storage_limit || 0) }}</span>
</li>
<li class="flex items-center">
<span class="i-heroicons-check-circle text-green-500 mr-2"></span>
<span>Max Duration: {{ formatDuration(plan.duration_limit || 0) }}</span>
</li>
<li class="flex items-center">
<span class="i-heroicons-check-circle text-green-500 mr-2"></span>
<span>Uploads: {{ plan.upload_limit }} / day</span>
</li>
</ul>
<button
@click="subscribe(plan)"
class="w-full py-2 px-4 bg-primary-600 hover:bg-primary-700 text-white rounded transition-colors disabled:opacity-50"
:disabled="subscribing === plan.id"
>
<span v-if="subscribing === plan.id" class="i-svg-spinners-180-ring-with-bg mr-2"></span>
{{ subscribing === plan.id ? 'Processing...' : 'Subscribe' }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { client, type ModelPlan } from '@/api/client';
const plans = ref<ModelPlan[]>([]);
const loading = ref(true);
const error = ref<string | null>(null);
const subscribing = ref<string | null>(null);
const fetchPlans = async () => {
loading.value = true;
error.value = null;
try {
const response = await client.plans.plansList();
if (response.data && Array.isArray(response.data)) {
plans.value = response.data;
} else {
// Fallback or handle unexpected structure?
// Based on client.ts it returns response.data as ModelPlan[] directly in the custom wrapper?
// Wait, client.ts says: r.data = data. So if the response matches schema, it's inside data.
// Let's re-read client.ts.
// plansList defined as request<ResponseResponse & { data?: ModelPlan[] }>
// So yes, response.data which is the body, and inside that, there is a data property.
// wait, let's check client.ts generated code again.
// plansList returns Promise<HttpResponse<...>>
// HttpResponse has .data property which IS the body.
// The body type is ResponseResponse & { data?: ModelPlan[] }
// So we access response.data.data
plans.value = response.data.data || [];
}
} catch (err: any) {
console.error(err);
error.value = err.message || 'Failed to load plans';
} finally {
loading.value = false;
}
};
const subscribe = async (plan: ModelPlan) => {
if (!plan.id) return;
subscribing.value = plan.id;
try {
// Mock payment for now as per plan, or call API if ready
// client.payments.paymentsCreate({ amount: plan.price || 0, plan_id: plan.id });
await client.payments.paymentsCreate({
amount: plan.price || 0,
plan_id: plan.id
});
alert(`Successfully subscribed to ${plan.name}`);
} catch (err: any) {
console.error(err);
alert('Failed to subscribe: ' + (err.message || 'Unknown error'));
} finally {
subscribing.value = null;
}
};
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatDuration = (seconds: number) => {
return `${Math.floor(seconds / 60)} mins`;
};
onMounted(() => {
fetchPlans();
});
</script>

View File

@@ -0,0 +1,181 @@
<template>
<div class="p-6 max-w-2xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Upload Video</h1>
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<div v-if="!file" class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-10 text-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" @drop.prevent="handleDrop" @dragover.prevent>
<input type="file" id="fileInput" class="hidden" accept="video/*" @change="handleFileSelect">
<label for="fileInput" class="cursor-pointer">
<span class="i-heroicons-cloud-arrow-up text-6xl text-gray-400 mb-4 inline-block"></span>
<p class="text-xl font-medium text-gray-700 dark:text-gray-200">Drag & drop your video here</p>
<p class="text-sm text-gray-500 mt-2">or click to browse</p>
</label>
</div>
<div v-else class="space-y-6">
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded">
<div class="flex items-center space-x-3 overflow-hidden">
<span class="i-heroicons-video-camera text-2xl text-primary-500 flex-shrink-0"></span>
<span class="truncate font-medium">{{ file.name }}</span>
<span class="text-xs text-gray-500 flex-shrink-0">({{ formatBytes(file.size) }})</span>
</div>
<button @click="resetFile" class="text-red-500 hover:text-red-700 flex-shrink-0" :disabled="uploading">
<span class="i-heroicons-x-mark text-xl"></span>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Title</label>
<input v-model="form.title" type="text" class="w-full rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 p-2" placeholder="My Awesome Video">
</div>
<div>
<label class="block text-sm font-medium mb-1">Description</label>
<textarea v-model="form.description" class="w-full rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 p-2" rows="3" placeholder="Describe your video..."></textarea>
</div>
</div>
<div v-if="uploadError" class="text-red-500 text-sm">
{{ uploadError }}
</div>
<div v-if="uploading" class="space-y-2">
<div class="flex justify-between text-sm">
<span>Uploading...</span>
<span>{{ uploadProgress }}%</span>
</div>
<div class="h-2 bg-gray-200 rounded-full overflow-hidden">
<div class="h-full bg-primary-600 transition-all duration-300" :style="{ width: `${uploadProgress}%` }"></div>
</div>
</div>
<button
@click="startUpload"
class="w-full py-3 bg-primary-600 hover:bg-primary-700 text-white rounded font-medium disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="uploading || !form.title"
>
{{ uploading ? 'Uploading...' : 'Upload Video' }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { client } from '@/api/client';
const router = useRouter();
const file = ref<File | null>(null);
const uploading = ref(false);
const uploadProgress = ref(0);
const uploadError = ref<string | null>(null);
const form = reactive({
title: '',
description: ''
});
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
file.value = target.files[0];
form.title = file.value.name.replace(/\.[^/.]+$/, ""); // Default title from filename
}
};
const handleDrop = (event: DragEvent) => {
if (event.dataTransfer?.files.length) {
file.value = event.dataTransfer.files[0];
form.title = file.value.name.replace(/\.[^/.]+$/, "");
}
};
const resetFile = () => {
file.value = null;
form.title = '';
form.description = '';
uploadError.value = null;
uploadProgress.value = 0;
};
const startUpload = async () => {
if (!file.value || !form.title) return;
uploading.value = true;
uploadError.value = null;
uploadProgress.value = 0;
try {
// 1. Get Presigned URL
const presignedResponse = await client.videos.uploadUrlCreate({
filename: file.value.name,
content_type: file.value.type,
size: file.value.size
});
// Check for data in response
const data = (presignedResponse.data as any).data || presignedResponse.data;
if (!data || !data.url) {
throw new Error('Failed to get upload URL');
}
const uploadUrl = data.url;
const finalVideoUrl = data.key || uploadUrl.split('?')[0]; // Assuming key is returned or can be derived
// 2. Upload file to S3 (using XMLHttpRequest for progress)
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('PUT', uploadUrl, true);
xhr.setRequestHeader('Content-Type', file.value!.type);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
uploadProgress.value = Math.round((e.loaded / e.total) * 100);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('Network error during upload'));
xhr.send(file.value);
});
// 3. Create Video Record
await client.videos.videosCreate({
title: form.title,
description: form.description,
size: file.value.size,
format: file.value.type,
url: finalVideoUrl,
// duration is optional, server might process it
});
// Redirect to videos page
router.push({ name: 'video' });
} catch (err: any) {
console.error(err);
uploadError.value = err.message || 'Upload failed';
} finally {
uploading.value = false;
}
};
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
</script>

103
src/routes/video/Videos.vue Normal file
View File

@@ -0,0 +1,103 @@
<template>
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">My Videos</h1>
<router-link to="/upload" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded transition-colors flex items-center">
<span class="i-heroicons-cloud-arrow-up mr-2"></span>
Upload Video
</router-link>
</div>
<div v-if="loading" class="flex justify-center">
<div class="i-svg-spinners-180-ring-with-bg text-4xl"></div>
</div>
<div v-else-if="error" class="text-red-500">
{{ error }}
</div>
<div v-else-if="videos.length === 0" class="text-center text-gray-500 py-10">
<p>No videos found. Upload your first video!</p>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div v-for="video in videos" :key="video.id" class="border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow bg-white dark:bg-gray-800">
<div class="aspect-video bg-gray-200 dark:bg-gray-700 relative group">
<img v-if="video.thumbnail" :src="video.thumbnail" :alt="video.title" class="w-full h-full object-cover">
<div v-else class="w-full h-full flex items-center justify-center text-gray-400">
<span class="i-heroicons-video-camera text-4xl"></span>
</div>
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<button class="text-white bg-primary-600 p-2 rounded-full hover:bg-primary-700" title="Play">
<span class="i-heroicons-play-20-solid text-xl"></span>
</button>
</div>
<span class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-1 rounded">{{ formatDuration(video.duration || 0) }}</span>
</div>
<div class="p-4">
<h3 class="font-semibold text-lg mb-1 truncate" :title="video.title">{{ video.title }}</h3>
<p class="text-sm text-gray-500 mb-2 truncate">{{ video.description || 'No description' }}</p>
<div class="flex justify-between items-center text-xs text-gray-400">
<span>{{ formatDate(video.created_at) }}</span>
<span :class="getStatusClass(video.status)">{{ video.status }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { client, type ModelVideo } from '@/api/client';
const videos = ref<ModelVideo[]>([]);
const loading = ref(true);
const error = ref<string | null>(null);
const fetchVideos = async () => {
loading.value = true;
error.value = null;
try {
const response = await client.videos.videosList({ page: 1, limit: 50 });
// Based on docs.json, schema might be incomplete, assuming standard response structure
// response.data is the body. The body should have 'data' containing the list?
const body = response.data as any; // Cast because generated type didn't have data field
if (body.data && Array.isArray(body.data)) {
videos.value = body.data;
} else if (Array.isArray(body)) {
videos.value = body;
} else {
console.warn('Unexpected video list format:', body);
videos.value = [];
}
} catch (err: any) {
console.error(err);
error.value = err.message || 'Failed to load videos';
} finally {
loading.value = false;
}
};
const formatDuration = (seconds: number) => {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
};
const formatDate = (dateString?: string) => {
if (!dateString) return '';
return new Date(dateString).toLocaleDateString();
};
const getStatusClass = (status?: string) => {
switch(status?.toLowerCase()) {
case 'ready': return 'text-green-500';
case 'processing': return 'text-yellow-500';
case 'failed': return 'text-red-500';
default: return 'text-gray-500';
}
};
onMounted(() => {
fetchVideos();
});
</script>

View File

@@ -1,115 +1,128 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
// import { client } from '@/api/rpcclient'; // client no longer used for auth actions import { ref } from 'vue';
import { ref, onMounted } from 'vue'; import { client, ResponseResponse, type ModelUser } from '@/api/client';
import { emailAuth, signUp, auth, googleAuth } from '@/lib/firebase';
import { onAuthStateChanged, signOut, User as FirebaseUser } from 'firebase/auth';
interface User {
id: string;
username: string;
email: string;
name: string;
}
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null); const user = ref<ModelUser | null>(null);
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
const error = ref<string | null>(null); const error = ref<string | null>(null);
const initialized = ref(false); const initialized = ref(false);
// Check auth status on init using Firebase observer // Initial check for session could go here if there was a /me endpoint or token check
async function init() { async function init() {
if (initialized.value) return; // if (initialized.value) return;
const response = await client.request<
return new Promise<void>((resolve) => { ResponseResponse & {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => { data?: ModelUser;
if (currentUser) { },
user.value = mapFirebaseUser(currentUser); ResponseResponse
} else { >({
user.value = null; path: '/me',
} method: 'GET'
initialized.value = true;
resolve();
// We could unsubscribe here if we only want initial load,
// but keeping it listens for changes (token refresh etc)
// However, 'init' usually implies just ONCE waiter.
// For reactivity, user.value is updated.
});
// Note: onAuthStateChanged returns an unsubscribe function.
// If we want to keep listening, we shouldn't unsubscribe immediately,
// but for 'await auth.init()' we just want to wait for the first known state.
}); });
} if (response.ok) {
// user.value = response.data?.data;
function mapFirebaseUser(fwUser: FirebaseUser): User { if (response.data?.data) {
return { user.value = response.data.data;
id: fwUser.uid, }
username: fwUser.email?.split('@')[0] || 'user', // fallback initialized.value = true;
email: fwUser.email || '', }
name: fwUser.displayName || fwUser.email?.split('@')[0] || 'User'
};
} }
async function login(username: string, password: string) { async function login(username: string, password: string) {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
// Assuming username is email for Firebase, or we need to look it up? try {
// Firebase works with Email. If input is username, this might fail. const response = await client.auth.loginCreate({
// For now assume email. email: username,
return emailAuth(username, password).then((userCredential) => { password: password
user.value = mapFirebaseUser(userCredential.user); });
router.push('/');
}).catch((e: any) => { // Expected response structure: { data: { code: 200, data: User, message: "..." } } based on typical wrapper + schema
// BUT client.ts generated code typically returns the body directly in .data property of HttpResponse
// And schema says ResponseResponse has 'data': {}
// So: response.data (HttpResponse body) -> .data (ResponseResponse payload)
const body = response.data as any; // Cast to access potential 'data' property if types are loose
if (body && body.data) {
user.value = body.data;
router.push('/');
} else {
throw new Error('Login failed: No user data received');
}
} catch (e: any) {
console.error(e); console.error(e);
error.value = 'Login failed: ' + (e.message || 'Unknown error'); error.value = 'Login failed: ' + (e.message || 'Unknown error');
throw e; throw e;
}).finally(() => { } finally {
loading.value = false; loading.value = false;
}); }
} }
async function loginWithGoogle() { async function loginWithGoogle() {
loading.value = true; // usually this initiates a redirect loop.
error.value = null; // Doing it via client.request might follow redirect or return html.
return googleAuth().then((result) => { // Best to just redirect the window.
user.value = mapFirebaseUser(result.user); window.location.href = `${client.baseUrl}/auth/google/login`;
router.push('/');
}).catch((e: any) => {
console.error(e);
error.value = 'Google Login failed';
throw e;
}).finally(() => {
loading.value = false;
});
} }
async function register(username: string, email: string, password: string) { async function register(username: string, email: string, password: string) {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
return signUp(email, password).then((fwUser) => { try {
// update profile with username? const response = await client.auth.registerCreate({
// updateProfile(fwUser, { displayName: username }); username,
user.value = mapFirebaseUser(fwUser); email,
router.push('/'); password
}).catch((e: any) => { });
// Check success
const body = response.data as any;
if (response.ok) {
// Auto login or redirect to login?
// Usually register returns success, user must login.
router.push('/login');
} else {
throw new Error(body.message || 'Registration failed');
}
} catch (e: any) {
console.error(e); console.error(e);
error.value = 'Registration failed: ' + (e.message || 'Unknown error'); error.value = 'Registration failed: ' + (e.message || 'Unknown error');
throw e; throw e;
}).finally(() => { } finally {
loading.value = false; loading.value = false;
}); }
} }
async function logout() { async function logout() {
return signOut(auth).then(() => { loading.value = true;
try {
await client.auth.logoutCreate();
user.value = null; user.value = null;
router.push('/'); router.push('/login');
}) } catch (e: any) {
console.error('Logout error', e);
// Force local logout anyway
user.value = null;
router.push('/login');
} finally {
loading.value = false;
}
} }
return { return {
user, loading, error, initialized, init, login, loginWithGoogle, register, logout, $reset: () => { user,
loading,
error,
initialized,
init,
login,
loginWithGoogle,
register,
logout,
$reset: () => {
user.value = null; user.value = null;
loading.value = false; loading.value = false;
error.value = null; error.value = null;

7
src/type.d.ts vendored
View File

@@ -2,10 +2,5 @@
/// <reference types="unplugin-vue-components/types/vue" /> /// <reference types="unplugin-vue-components/types/vue" />
declare module "@httpClientAdapter" { declare module "@httpClientAdapter" {
import { TinyRpcClientAdapter } from "@hiogawa/tiny-rpc"; export const customFetch: (url: string, options: RequestInit) => Promise<Response>;
export function httpClientAdapter(opts: {
url: string;
pathsForGET?: string[];
headers?: () => Promise<{ Authorization?: undefined; } | { Authorization: string; }>
}): TinyRpcClientAdapter;
} }