src/Api/Program.cs
+2
-0
diff --git a/src/Api/Program.cs b/src/Api/Program.cs
index 46bb403..44876f7 100644
@@ -60,4 +60,6 @@ app.MapApi();
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapFallbackToFile("index.html");
app.Run();
src/Api/YouTubeAuth/YouTubeAuthExtensions.cs
+22
-0
diff --git a/src/Api/YouTubeAuth/YouTubeAuthExtensions.cs b/src/Api/YouTubeAuth/YouTubeAuthExtensions.cs
index 7605fad..726272e 100644
@@ -1,8 +1,10 @@
using System.Threading.Tasks;
using Google.Apis.Auth.AspNetCore3;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Slopper.Infrastructure.YouTube;
@@ -22,6 +24,26 @@ public static class YouTubeAuthExtensions
options.CallbackPath = "/admin/redirect/youtube";
options.ResponseMode = OpenIdConnectResponseMode.Query;
options.TokenValidationParameters.NameClaimType = "name";
options.Events = new()
{
OnRedirectToIdentityProvider = context =>
{
if (context.Request.Headers.GetCommaSeparatedValues("Sec-Fetch-Mode") is ["navigate"])
{
return Task.CompletedTask;
}
context.Response.OnStarting(
static response =>
{
((HttpResponse)response).StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
},
context.Response
);
return Task.CompletedTask;
},
};
}
);
src/Frontend/package-lock.json
+495
-5
diff --git a/src/Frontend/package-lock.json b/src/Frontend/package-lock.json
index 936bd4d..974a076 100644
@@ -8,7 +8,8 @@
"name": "slopper-frontend",
"version": "0.0.0",
"dependencies": {
"vue": "^3.5.32"
"vue": "^3.5.32",
"vue-router": "^5.0.7"
},
"devDependencies": {
"@types/node": "^24.12.2",
@@ -19,6 +20,69 @@
"vue-tsc": "^3.2.7"
}
},
"node_modules/@babel/generator": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.5.tgz",
"integrity": "sha512-nFZPWz3FHIS7y6rMIVoa/WBwjdutfIaRJIBQjzn+t3RnecZoRNlGmGcyR2wb0T/IgSd50Kz/6dG8/LvMCRunjg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^8.0.0-rc.5",
"@babel/types": "^8.0.0-rc.5",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"@types/jsesc": "^2.5.0",
"jsesc": "^3.0.2"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/helper-string-parser": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.5.tgz",
"integrity": "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A==",
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.5.tgz",
"integrity": "sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg==",
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/parser": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.5.tgz",
"integrity": "sha512-/Mfg83rK3+jsRbl4Vbd0jqxc6M1A1/WNFtgrowRM1unEsD3XcNnrBdMM0JWakd0/RN9lseQKwPduW1TiEwKOlQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^8.0.0-rc.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/types": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.5.tgz",
"integrity": "sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^8.0.0-rc.5",
"@babel/helper-validator-identifier": "^8.0.0-rc.5"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -99,12 +163,51 @@
"tslib": "^2.4.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
@@ -409,6 +512,12 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/jsesc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
"integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.12.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.3.tgz",
@@ -465,6 +574,33 @@
"vscode-uri": "^3.0.8"
}
},
"node_modules/@vue-macros/common": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz",
"integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==",
"license": "MIT",
"dependencies": {
"@vue/compiler-sfc": "^3.5.22",
"ast-kit": "^2.1.2",
"local-pkg": "^1.1.2",
"magic-string-ast": "^1.0.2",
"unplugin-utils": "^0.3.0"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/vue-macros"
},
"peerDependencies": {
"vue": "^2.7.0 || ^3.2.25"
},
"peerDependenciesMeta": {
"vue": {
"optional": true
}
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.34",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz",
@@ -515,6 +651,33 @@
"@vue/shared": "3.5.34"
}
},
"node_modules/@vue/devtools-api": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.2.tgz",
"integrity": "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^8.1.2"
}
},
"node_modules/@vue/devtools-kit": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz",
"integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^8.1.2",
"birpc": "^2.6.1",
"hookable": "^5.5.3",
"perfect-debounce": "^2.0.0"
}
},
"node_modules/@vue/devtools-shared": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz",
"integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==",
"license": "MIT"
},
"node_modules/@vue/language-core": {
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.8.tgz",
@@ -600,6 +763,18 @@
}
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/alien-signals": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
@@ -607,6 +782,68 @@
"dev": true,
"license": "MIT"
},
"node_modules/ast-kit": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
"integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"pathe": "^2.0.3"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/ast-walker-scope": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz",
"integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.4",
"ast-kit": "^2.1.3"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/birpc": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"license": "MIT",
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/confbox": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -641,11 +878,16 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/exsolve": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -674,6 +916,36 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -935,6 +1207,23 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/local-pkg": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
"license": "MIT",
"dependencies": {
"mlly": "^1.7.4",
"pkg-types": "^2.3.0",
"quansync": "^0.2.11"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -944,11 +1233,54 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/magic-string-ast": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz",
"integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==",
"license": "MIT",
"dependencies": {
"magic-string": "^0.30.19"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/mlly": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
"integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
"license": "MIT",
"dependencies": {
"acorn": "^8.16.0",
"pathe": "^2.0.3",
"pkg-types": "^1.3.1",
"ufo": "^1.6.3"
}
},
"node_modules/mlly/node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
"license": "MIT"
},
"node_modules/mlly/node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"license": "MIT",
"dependencies": {
"confbox": "^0.1.8",
"mlly": "^1.7.4",
"pathe": "^2.0.1"
}
},
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -976,6 +1308,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"license": "MIT"
},
"node_modules/perfect-debounce": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -986,7 +1330,6 @@
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -995,6 +1338,17 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkg-types": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
"integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
"license": "MIT",
"dependencies": {
"confbox": "^0.2.4",
"exsolve": "^1.0.8",
"pathe": "^2.0.3"
}
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
@@ -1023,6 +1377,35 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/antfu"
},
{
"type": "individual",
"url": "https://github.com/sponsors/sxzz"
}
],
"license": "MIT"
},
"node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.18",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz",
@@ -1064,6 +1447,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/scule": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1077,7 +1466,6 @@
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -1112,6 +1500,12 @@
"node": ">=14.17"
}
},
"node_modules/ufo": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -1119,6 +1513,36 @@
"dev": true,
"license": "MIT"
},
"node_modules/unplugin": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
"integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"picomatch": "^4.0.3",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/unplugin-utils": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
"license": "MIT",
"dependencies": {
"pathe": "^2.0.3",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/vite": {
"version": "8.0.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz",
@@ -1225,6 +1649,51 @@
}
}
},
"node_modules/vue-router": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.7.tgz",
"integrity": "sha512-dqfk8kvRbCutmCOCj/XLDqDEYxc1wBdAOGLuVy5M93ifYMsBd5fIjfaPN4tQAbxr5IprdBDIox1gr4wYyOx/SA==",
"license": "MIT",
"dependencies": {
"@babel/generator": "^8.0.0-rc.4",
"@vue-macros/common": "^3.1.1",
"@vue/devtools-api": "^8.1.1",
"ast-walker-scope": "^0.8.3",
"chokidar": "^5.0.0",
"json5": "^2.2.3",
"local-pkg": "^1.1.2",
"magic-string": "^0.30.21",
"mlly": "^1.8.0",
"muggle-string": "^0.4.1",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"scule": "^1.3.0",
"tinyglobby": "^0.2.15",
"unplugin": "^3.0.0",
"unplugin-utils": "^0.3.1",
"yaml": "^2.8.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@pinia/colada": ">=0.21.2",
"@vue/compiler-sfc": "^3.5.34",
"pinia": "^3.0.4",
"vue": "^3.5.34"
},
"peerDependenciesMeta": {
"@pinia/colada": {
"optional": true
},
"@vue/compiler-sfc": {
"optional": true
},
"pinia": {
"optional": true
}
}
},
"node_modules/vue-tsc": {
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.8.tgz",
@@ -1241,6 +1710,27 @@
"peerDependencies": {
"typescript": ">=5.0.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT"
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
}
}
}
src/Frontend/package.json
+2
-1
diff --git a/src/Frontend/package.json b/src/Frontend/package.json
index 4d6dfa3..3e40ffb 100644
@@ -9,7 +9,8 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.32"
"vue": "^3.5.32",
"vue-router": "^5.0.7"
},
"devDependencies": {
"@types/node": "^24.12.2",
src/Frontend/src/App.vue
+1
-5
diff --git a/src/Frontend/src/App.vue b/src/Frontend/src/App.vue
index dafb4a7..7c2aa3f 100644
@@ -1,7 +1,3 @@
<script setup lang="ts">
import ClipFeed from "./components/ClipFeed.vue";
</script>
<template>
<ClipFeed />
<RouterView />
</template>
src/Frontend/src/components/StatusCard.vue
+48
-0
diff --git a/src/Frontend/src/components/StatusCard.vue b/src/Frontend/src/components/StatusCard.vue
new file mode 100644
index 0000000..fca6994
@@ -0,0 +1,48 @@
<script setup lang="ts">
defineProps<{ isRunning: boolean; runningText: string; idleText: string }>();
</script>
<template>
<div class="status-card">
<div class="status-row">
<span class="dot" :class="{ active: isRunning }"></span>
<span>{{ isRunning ? runningText : idleText }}</span>
</div>
<progress v-if="isRunning"></progress>
</div>
</template>
<style scoped>
.status-card {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 1rem 1.25rem;
}
.status-row {
display: flex;
align-items: center;
gap: 0.625rem;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.65);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.18);
flex-shrink: 0;
transition: background 0.3s, box-shadow 0.3s;
}
.dot.active {
background: #22c55e;
box-shadow: 0 0 8px rgba(34, 197, 94, 0.5);
}
progress {
margin-top: 1rem;
}
</style>
src/Frontend/src/layouts/AdminLayout.vue
+88
-0
diff --git a/src/Frontend/src/layouts/AdminLayout.vue b/src/Frontend/src/layouts/AdminLayout.vue
new file mode 100644
index 0000000..a5ecec9
@@ -0,0 +1,88 @@
<template>
<div class="admin">
<main class="admin-content">
<RouterView />
</main>
<nav class="admin-nav">
<RouterLink to="/admin/youtube" class="nav-item">YouTube</RouterLink>
</nav>
</div>
</template>
<style scoped>
.admin {
display: flex;
flex-direction: column;
height: 100dvh;
color: #fff;
background: #000;
}
.admin-content {
flex: 1;
overflow-y: auto;
padding: 1.5rem 1rem;
min-height: 0;
}
.admin-nav {
display: flex;
background: #0a0a0a;
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-bottom: env(safe-area-inset-bottom, 0);
flex-shrink: 0;
}
.nav-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0.875rem 0;
color: rgba(255, 255, 255, 0.4);
text-decoration: none;
font-size: 0.8rem;
letter-spacing: 0.04em;
text-transform: uppercase;
transition: color 0.15s;
}
.nav-item.router-link-active {
color: #fff;
}
@media (min-width: 640px) {
.admin {
flex-direction: row;
}
.admin-nav {
order: -1;
flex-direction: column;
justify-content: flex-start;
width: 13rem;
border-top: none;
border-right: 1px solid rgba(255, 255, 255, 0.08);
padding: 1.5rem 0.625rem;
gap: 0.125rem;
}
.nav-item {
flex: none;
justify-content: flex-start;
padding: 0.625rem 0.75rem;
border-radius: 6px;
font-size: 0.875rem;
text-transform: none;
letter-spacing: 0;
}
.nav-item.router-link-active {
background: rgba(255, 255, 255, 0.07);
}
.admin-content {
padding: 2rem 2.5rem;
}
}
</style>
src/Frontend/src/main.ts
+2
-1
diff --git a/src/Frontend/src/main.ts b/src/Frontend/src/main.ts
index 3c9bfeb..cada3c0 100644
@@ -1,5 +1,6 @@
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import router from "./router";
createApp(App).mount("#app");
createApp(App).use(router).mount("#app");
src/Frontend/src/pages/AdminYouTubePage.vue
+94
-0
diff --git a/src/Frontend/src/pages/AdminYouTubePage.vue b/src/Frontend/src/pages/AdminYouTubePage.vue
new file mode 100644
index 0000000..1322472
@@ -0,0 +1,94 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { getYouTubeUploadStatus, startYouTubeUpload, type JobStatus } from "../services/api";
import StatusCard from "../components/StatusCard.vue";
const status = ref<JobStatus | null>(null);
const unauthenticated = ref(false);
const starting = ref(false);
let pollInterval: ReturnType<typeof setInterval> | null = null;
function startPolling() {
stopPolling();
pollInterval = setInterval(async () => {
const result = await getYouTubeUploadStatus();
if (pollInterval === null) return;
if (result === null) {
unauthenticated.value = true;
stopPolling();
return;
}
status.value = result;
if (!result.isRunning) stopPolling();
}, 1000);
}
function stopPolling() {
if (pollInterval !== null) {
clearInterval(pollInterval);
pollInterval = null;
}
}
onMounted(async () => {
const result = await getYouTubeUploadStatus();
if (result === null) {
unauthenticated.value = true;
} else {
status.value = result;
if (result.isRunning) startPolling();
}
});
onUnmounted(stopPolling);
async function startUpload() {
starting.value = true;
try {
await startYouTubeUpload();
status.value = { isRunning: true, nextScheduledRun: null };
startPolling();
} finally {
starting.value = false;
}
}
</script>
<template>
<div v-if="unauthenticated" class="login-prompt">
<a href="/api/youtube/login" class="button">Login to YouTube</a>
</div>
<template v-else-if="status">
<h1>YouTube</h1>
<StatusCard :is-running="status.isRunning" running-text="Upload in progress" idle-text="Idle" />
<button :disabled="status.isRunning || starting" @click="startUpload">Start upload</button>
</template>
</template>
<style scoped>
h1 {
margin-bottom: 1.25rem;
}
button {
display: block;
width: 100%;
margin-top: 1rem;
}
.login-prompt {
display: flex;
min-height: 50dvh;
align-items: center;
justify-content: center;
}
@media (min-width: 640px) {
button {
width: auto;
min-width: 10rem;
}
}
</style>
src/Frontend/src/pages/ClipFeed.vue
+1
-1
diff --git a/src/Frontend/src/components/ClipFeed.vue b/src/Frontend/src/pages/ClipFeed.vue
similarity index 97%
rename from src/Frontend/src/components/ClipFeed.vue
rename to src/Frontend/src/pages/ClipFeed.vue
index 5754ccf..b0e83db 100644
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import ClipItem from "./ClipItem.vue";
import ClipItem from "../components/ClipItem.vue";
import type { Clip } from "../Clip";
import { fetchClips as fetchClipsFromApi } from "../services/api";
src/Frontend/src/router/index.ts
+16
-0
diff --git a/src/Frontend/src/router/index.ts b/src/Frontend/src/router/index.ts
new file mode 100644
index 0000000..28804c4
@@ -0,0 +1,16 @@
import { createRouter, createWebHistory } from "vue-router";
import ClipFeed from "../pages/ClipFeed.vue";
import AdminLayout from "../layouts/AdminLayout.vue";
import AdminYouTubePage from "../pages/AdminYouTubePage.vue";
export default createRouter({
history: createWebHistory(),
routes: [
{ path: "/", component: ClipFeed },
{
path: "/admin",
component: AdminLayout,
children: [{ path: "youtube", component: AdminYouTubePage }],
},
],
});
src/Frontend/src/services/api.ts
+17
-0
diff --git a/src/Frontend/src/services/api.ts b/src/Frontend/src/services/api.ts
index 1278746..572e8db 100644
@@ -1,5 +1,22 @@
import type { Clip } from "../Clip";
export interface JobStatus {
isRunning: boolean;
nextScheduledRun: string | null;
}
export async function getYouTubeUploadStatus(): Promise<JobStatus | null> {
const res = await fetch("/api/youtube/upload");
if (res.status === 403) return null;
if (!res.ok) throw new Error(`Unexpected response: ${res.status}`);
return res.json() as Promise<JobStatus>;
}
export async function startYouTubeUpload(): Promise<void> {
const res = await fetch("/api/youtube/upload", { method: "PUT" });
if (!res.ok) throw new Error(`Unexpected response: ${res.status}`);
}
export async function fetchClips(after?: string): Promise<Array<Clip>> {
const params = new URLSearchParams({ limit: "10" });
if (after) params.set("after", after);
src/Frontend/src/style.css
+75
-0
diff --git a/src/Frontend/src/style.css b/src/Frontend/src/style.css
index 7772771..785ea8d 100644
@@ -14,3 +14,78 @@ body,
margin: 0;
padding: 0;
}
h1 {
font-size: 1.25rem;
font-weight: 600;
letter-spacing: -0.02em;
color: rgba(255, 255, 255, 0.9);
}
button,
a.button {
padding: 0.9rem 1.25rem;
background: #fff;
color: #000;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
text-decoration: none;
transition: opacity 0.15s;
}
button:not(:disabled):active,
a.button:active {
opacity: 0.85;
}
button:disabled {
opacity: 0.25;
cursor: not-allowed;
}
progress {
-webkit-appearance: none;
appearance: none;
display: block;
width: 100%;
height: 2px;
border: none;
border-radius: 1px;
background: rgba(255, 255, 255, 0.08);
overflow: hidden;
}
progress::-webkit-progress-bar {
background: rgba(255, 255, 255, 0.08);
}
progress::-webkit-progress-value {
background: #fff;
}
progress::-moz-progress-bar {
background: #fff;
}
@keyframes scan {
from {
background-position: -120% 0;
}
to {
background-position: 220% 0;
}
}
progress:indeterminate {
background: linear-gradient(90deg, transparent 20%, rgba(255, 255, 255, 0.7) 50%, transparent 80%);
background-size: 60% 100%;
background-repeat: no-repeat;
animation: scan 1.6s ease-in-out infinite;
}
progress:indeterminate::-webkit-progress-bar {
background: rgba(255, 255, 255, 0.08);
}
src/Frontend/vite.config.ts
+2
-1
diff --git a/src/Frontend/vite.config.ts b/src/Frontend/vite.config.ts
index 780ebf2..6148a60 100644
@@ -5,8 +5,9 @@ import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
server: {
port: 5055,
proxy: {
"/api": "https://slopper.xn--sberg-lra.net",
"/api": "http://localhost:5056",
},
},
});