diff --git a/Dockerfile b/Dockerfile index 232fb59..21eb1ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM onedev.site.tesses.net/crosslang/crosslang:latest RUN mkdir /app COPY Tesses.YouTubeDownloader.Server/bin/ /app WORKDIR /data +ENV TYTDDIR=/data EXPOSE 3255 diff --git a/Tesses.YouTubeDownloader.Server/src/components/shellsimple.tcross b/Tesses.YouTubeDownloader.Server/src/components/shellsimple.tcross new file mode 100644 index 0000000..f874029 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/shellsimple.tcross @@ -0,0 +1,27 @@ +func Components.ShellSimple(title, html, $htmx) +{ + return + + + + + TYTD - {title} + + + + + + + + + + + + + +
+ +
+ +; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/main.tcross b/Tesses.YouTubeDownloader.Server/src/main.tcross index 85dbfa5..cb1309d 100644 --- a/Tesses.YouTubeDownloader.Server/src/main.tcross +++ b/Tesses.YouTubeDownloader.Server/src/main.tcross @@ -17,23 +17,305 @@ var TYTDResources = [ var times=1; class TYTDApp { + private OOBE_STATE = { + tag = "UnknownPC", + pollHours = 3, + pollMinutes = 0, + pollSeconds = 0, + enablePlugins = true + }; + private TYTD; public TYTDApp() { - var tytdfs = new SubdirFilesystem(FS.Local, "TYTD"); - this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull("TYTD")); + var tytdfs = new SubdirFilesystem(FS.Local, GetTYTDDir()); + this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull(GetTYTDDir())); this.TYTD.Start(); } + public Handle(ctx) { + if(ctx.Path == "/api/v1/login") + { + if(ctx.Method == "POST") + { + const req=ctx.ReadJson(); + const result = this.TYTD.Login(req.username, req.password); + if(result) + { + ctx.SendJson({ + success = true, + token = result + }); + } + else { + ctx.SendJson({ + success=false + }); + } + return true; + } + else { + ctx.StatusCode=307; + ctx.SendRedirect("/login"); + return true; + } + } + if(ctx.Path == "/login") + { + var incorrect=false; + const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/"; + const username = ctx.QueryParams.TryGetFirst("username") ?? ""; + const password = ctx.QueryParams.TryGetFirst("password") ?? ""; + + if(ctx.Method == "POST") + { + const result = this.TYTD.Login(username, password); + if(result) + { + ctx.StatusCode = 303; + ctx.WithHeader("Set-Cookie",$"Session={result}; SameSite=Strict").SendRedirect("/"); + return true; + } + else incorrect=true; + } + + ctx.WithMimeType("text/html").SendText(Pages.Login(redirect, incorrect)); + return true; + } + const loggedIn = this.TYTD.IsLoggedIn(ctx); + if(!loggedIn) + { + each(var file : TYTDResources) + { + if(ctx.Path == file.path) + { + ctx.WithMimeType(Net.Http.MimeType(file.path)).SendBytes(file.value); + return true; + } + } + ctx.StatusCode = 307; + ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(ctx.Path)}"); + return true; + } + + + + + + if(this.TYTD.Config.OobeState == "oobe") + { + each(var file : TYTDResources) + { + if(ctx.Path == file.path) + { + ctx.WithMimeType(Net.Http.MimeType(file.path)).SendBytes(file.value); + return true; + } + } + + if(ctx.Path == "/oobe") + { + if(ctx.Method == "POST") + { + const from = ctx.QueryParams.TryGetFirstInt("from"); + switch(from) + { + case 1: + { + const enablePlugins = ctx.QueryParams.GetFirstBoolean("enablePlugins"); + const tag = ctx.QueryParams.TryGetFirst("tag") ?? "UnknownPC"; + const pollHours = ctx.QueryParams.TryGetFirstInt("pollHours") ?? 3; + const pollMinutes = ctx.QueryParams.TryGetFirstInt("pollMinutes") ?? 0; + const pollSeconds = ctx.QueryParams.TryGetFirstInt("pollSeconds") ?? 0; + + + this.OOBE_STATE.tag = tag; + this.OOBE_STATE.pollHours = pollHours; + this.OOBE_STATE.pollMinutes = pollMinutes; + + this.OOBE_STATE.pollSeconds = pollSeconds; + + this.OOBE_STATE.enablePlugins = enablePlugins; + + const page = this.OOBE_STATE.enablePlugins ? "/oobe?page=2" : "/oobe?page=3"; + ctx.StatusCode = 303; + ctx.SendRedirect(page); + + return true; + } + break; + case 3: + { + + const createUser = ctx.QueryParams.GetFirstBoolean("user"); + + var seconds = this.OOBE_STATE.pollSeconds; + seconds += this.OOBE_STATE.pollMinutes * 60; + seconds += this.OOBE_STATE.pollHours * 3600; + this.TYTD.Config.TYTDTag = this.OOBE_STATE.tag; + this.TYTD.Config.BellTimer = seconds; + this.TYTD.Config.EnablePlugins = this.OOBE_STATE.enablePlugins; + this.TYTD.Config.OobeState = "welcome"; + this.TYTD.SaveConfig(); + + + if(createUser) + { + + ctx.StatusCode = 303; + ctx.SendRedirect("/newuser"); + + return true; + } else { + + ctx.StatusCode = 303; + ctx.SendRedirect("/"); + + return true; + } + } + break; + } + ctx.WithMimeType("text/html").SendText(Components.ShellSimple("First Setup",

Malformed data

)); + return true; + } + const page = ctx.QueryParams.TryGetFirstInt("page"); + switch(page) + { + case 1: + ctx.WithMimeType("text/html").SendText(Pages.OobePage1(this.OOBE_STATE)); + break; + case 2: + ctx.WithMimeType("text/html").SendText(Pages.OobePage2(this.TYTD,ctx)); + break; + case 3: + ctx.WithMimeType("text/html").SendText(Pages.OobePage3()); + break; + default: + ctx.WithMimeType("text/html").SendText(Components.ShellSimple("First Setup",

Setup page not found

)); + break; + } + return true; + } + if(ctx.Path != "/package-manage" && ctx.Path != "/api/v1/plugin-thumbnail.png") + { + ctx.StatusCode = 307; + ctx.SendRedirect($"/oobe?page=1"); + return true; + } + } + + if(ctx.Path == "/welcome") + { + const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/settings"; + if(ctx.QueryParams.GetFirstBoolean("closePopup")) + { + if(this.TYTD.Config.OobeState == "welcome") + { + this.TYTD.Config.OobeState = "finished"; + this.TYTD.SaveConfig(); + } + ctx.StatusCode = 307; + ctx.SendRedirect(redirect); + + return true; + } + ctx.WithMimeType("text/html").SendText(Pages.Welcome(redirect)); + return true; + } + if(this.TYTD.Config.OobeState == "welcome") + { + each(var file : TYTDResources) + { + if(ctx.Path == file.path) + { + ctx.WithMimeType(Net.Http.MimeType(file.path)).SendBytes(file.value); + return true; + } + } + if(ctx.Path != "/newuser") + { + + ctx.StatusCode = 307; + ctx.SendRedirect($"/welcome?redirect={Net.Http.UrlEncode(ctx.Path)}"); + return true; + } + } if(ctx.Path == "/") { ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD)); return true; } + else if(ctx.Path == "/passwd") + { + var incorrect=false; + + const password = ctx.QueryParams.TryGetFirst("password") ?? ""; + const newpassword = ctx.QueryParams.TryGetFirst("newpassword") ?? ""; + + const confirm = ctx.QueryParams.TryGetFirst("newpassword") ?? ""; + if(ctx.Method == "POST") + { + this.TYTD.Passwd(); + } + + ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect)); + } + else if(ctx.Path == "/newuser") + { + var error = null; + if(ctx.Method == "POST") + { + const username = ctx.QueryParams.TryGetFirst("username"); + const password = ctx.QueryParams.TryGetFirst("password"); + const confirm = ctx.QueryParams.TryGetFirst("confirm"); + const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin"); + const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins"); + const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins"); + const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB"); + const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/"; + var flags = 0; + if(isAdmin) flags |= UserFlags.AdminFlag; + if(canUsePlugins) flags |= UserFlags.PluginFlag; + if(canManagePlugins) flags |= UserFlags.ManagePluginFlag; + if(canDownloadDB) flags |= UserFlags.DatabaseFlag; + + + if(password != confirm) + { + error = "Passwords do not match"; + } + else if(!TypeIsString(password)) { + error = "Password is not defined"; + + } + else if(!TypeIsString(username)) { + error = "Username not defined"; + } + else { + error = this.TYTD.CreateAccount(ctx, username, password, flags); + } + + if(!TypeIsString(error)) + { + ctx.StatusCode=303; + ctx.SendRedirect(redirect); + } + } + ctx.WithMimeType("text/html").SendText(Pages.NewUser(error)); + return true; + } + else if(ctx.Path == "/logout") + { + this.TYTD.Logout(ctx); + ctx.StatusCode = 307; + ctx.SendRedirect("/"); + return true; + } else if(ctx.Path == "/api") { ctx.WithMimeType("text/html").SendText(Pages.Api()); @@ -150,7 +432,11 @@ class TYTDApp { } else if(ctx.Path == "/api/v1/database.db") { - this.TYTD.SendDatabase(ctx); + + if(!this.TYTD.SendDatabase(ctx)) + { + ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized","

You are not authorized to download the database

",3,/"api/v1/database.db")); + } return true; } else if(ctx.Path == "/api/v1/personal") @@ -270,6 +556,12 @@ class TYTDApp { ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id)); return true; } + else if(ctx.Path == "/whoami") + { + const row = this.TYTD.WhoAmI(ctx); + ctx.WithMimeType("text/html").SendText(Pages.WhoAmI(row)); + return true; + } else if(ctx.Path == "/edit-personal-description") { var name = ctx.QueryParams.TryGetFirst("name"); @@ -299,8 +591,98 @@ class TYTDApp { } else if(ctx.Path == "/settings") { + + ctx.WithMimeType("text/html").SendText(Pages.Settings(this.TYTD,ctx)); + return true; + } + else if(ctx.Path == "/edituser") + { + if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx))) + { + ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",

You can{"'"}t modify admin settings as you are not an admin

,3)); + return true; + } + const user = ctx.QueryParams.TryGetFirst("user"); + + if(TypeIsString(user)) + { + var userObj = null; + const whoami = this.TYTD.WhoAmI(ctx); + if(ctx.Method == "POST") + { + if(whoami.username != user) + { + const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin"); + const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins"); + const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins"); + const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB"); + var flags = 0; + if(isAdmin) flags |= UserFlags.AdminFlag; + if(canUsePlugins) flags |= UserFlags.PluginFlag; + if(canManagePlugins) flags |= UserFlags.ManagePluginFlag; + if(canDownloadDB) flags |= UserFlags.DatabaseFlag; + + + this.TYTD.Mutex.Lock(); + const db = this.TYTD.OpenDB(); + Sqlite.Exec(db, $"UPDATE users SET flags = {flags} WHERE username = {Sqlite.Escape(user)}"); + Sqlite.Close(db); + this.TYTD.Mutex.Unlock(); + } + } + if(ctx.Method == "DELETE") + { + if(whoami.username != user) + { + this.TYTD.Mutex.Lock(); + const db = this.TYTD.OpenDB(); + Sqlite.Exec(db,$"DELETE FROM users WHERE username = {Sqlite.Escape(user)}"); + Sqlite.Close(db); + this.TYTD.Mutex.Unlock(); + ctx.StatusCode = 200; + ctx.WithHeader("HX-Location", "/edituser"); + ctx.SendText("Redirect"); + } + else + { + ctx.StatusCode = 401; + ctx.WriteHeaders(); + } + return true; + } + + + + if(whoami.username != user) + { + this.TYTD.Mutex.Lock(); + const db = this.TYTD.OpenDB(); + userObj = Sqlite.Exec(db,$"SELECT * FROM users WHERE username = {Sqlite.Escape(user)}"); + Sqlite.Close(db); + this.TYTD.Mutex.Unlock(); + if(TypeIsList(userObj) && userObj.Length == 1) userObj = userObj[0]; + else userObj=null; + } + + + + ctx.WithMimeType("text/html").SendText(Pages.EditUser(userObj)); + } + else { + ctx.WithMimeType("text/html").SendText(Pages.EditUserList(this.TYTD, ctx)); + } + return true; + } + else if(ctx.Path == "/admin") + { + if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx))) + { + ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",

You can{"'"}t modify admin settings as you are not an admin

,3)); + return true; + } if(ctx.Method == "POST") { + /* hours minutes @@ -324,7 +706,7 @@ class TYTDApp { this.TYTD.SaveConfig(); } } - ctx.WithMimeType("text/html").SendText(Pages.Settings(this.TYTD,ctx)); + ctx.WithMimeType("text/html").SendText(Pages.Admin(this.TYTD,ctx)); return true; } else if(ctx.Path == "/watch" || ctx.Path == "/video") @@ -427,11 +809,13 @@ class TYTDApp { } else if(ctx.Path == "/plugins-download") { + if(!UserFlags.CanManagePlugins(this.TYTD.IsLoggedIn(ctx))) return false; ctx.WithMimeType("text/html").SendText(Pages.DownloadPlugins(this.TYTD,ctx)); return true; } else if(ctx.Path == "/package-manage" && ctx.Method == "POST") { + if(!UserFlags.CanManagePlugins(this.TYTD.IsLoggedIn(ctx))) return false; var data = { name = ctx.QueryParams.TryGetFirst("name"), version = ctx.QueryParams.TryGetFirst("version"), @@ -502,7 +886,12 @@ class TYTDApp { } else if(ctx.Path.StartsWith("/plugin/")) { + if(UserFlags.CanUsePlugins(this.TYTD.IsLoggedIn(ctx))) return this.TYTD.Servers.Handle(ctx); + else + { + return false; + } } else { each(var file : TYTDResources) @@ -523,8 +912,53 @@ class TYTDApp { } } +func GetTYTDDir() +{ + const dir = Env["TYTDDIR"]; + if(TypeIsString(dir) && dir.Length > 0) + return dir; + return (Env.Videos / "TYTD2025").ToString(); +} + func WebAppMain(args) { + if(args.Length == 2 && args[1] == "change-password") + { + Console.Write("Username: "); + const username = Console.ReadLine(); + Console.Write("Password: "); + const echo = Console.Echo; + Console.Echo = false; + const password = Console.ReadLine(); + Console.WriteLine(); + Console.Write("Confirm: "); + const confirm = Console.ReadLine(); + Console.WriteLine(); + Console.Echo = true; + + if(password != confirm) + { + Console.WriteLine("Passwords do not match!"); + return null; + } + + const salt = Crypto.RandomBytes(32, "TYTD2025"); + const hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384); + FS.Local.CreateDirectory(GetTYTDDir()); + const db = Sqlite.Open(GetTYTDDir()/"tytd.db"); + + + const res = Sqlite.Exec(db, $"UPDATE users SET password_hash = {Sqlite.Escape(Crypto.Base64Encode(hash))}, password_salt = {Sqlite.Escape(Crypto.Base64Encode(salt))} WHERE username = {Sqlite.Escape(username)};"); + if(TypeIsList(res)) + { + Console.WriteLine("Changed password successfully"); + } + else { + Console.WriteLine($"Failed to change password: {res}"); + } + + return null; + } return new TYTDApp(); } func main(args) diff --git a/Tesses.YouTubeDownloader.Server/src/pages/admin.tcross b/Tesses.YouTubeDownloader.Server/src/pages/admin.tcross new file mode 100644 index 0000000..ab69271 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/admin.tcross @@ -0,0 +1,76 @@ +func Pages.Admin(tytd,ctx) +{ + var totalSecs = tytd.Config.BellTimer ?? 10800; + var enablePlugins = tytd.Config.EnablePlugins ?? true; + var hours = totalSecs / 3600; + totalSecs -= hours * 3600; + var minutes = totalSecs / 60; + var seconds = totalSecs % 60; + + + var html =
+
+ +
+
+ + +
+ +
+ Subscriber Poll Rate +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+ Actions + + New user + + Edit user + Back to Settings + +
+ + + + +
; + + return Components.Shell("Admin Settings",html ,3); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/changepassword.tcross b/Tesses.YouTubeDownloader.Server/src/pages/changepassword.tcross new file mode 100644 index 0000000..5daf7d9 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/changepassword.tcross @@ -0,0 +1,49 @@ +func Pages.ChangePassword(redirect,incorrect) +{ + + + + +const html = + + +
+
The password could not be changed
+
+
+ +
+ +
+
+
Change your password
+
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + +
+
+
+
+
+
; + + return Components.Shell("Change your password",html,3); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/edituser.tcross b/Tesses.YouTubeDownloader.Server/src/pages/edituser.tcross new file mode 100644 index 0000000..d0df2ad --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/edituser.tcross @@ -0,0 +1,145 @@ +func Pages.EditUserList(tytd, ctx) +{ + tytd.Mutex.Lock(); + const db = tytd.OpenDB(); + const users = Sqlite.Exec(db,"SELECT * FROM users;"); + Sqlite.Close(db); + tytd.Mutex.Unlock(); + const myuser = tytd.WhoAmI(ctx); + const html = +
+ Find by username +
+
+ + +
+ +
+
+ +
+ Users + +
+
; + + return Components.Shell("Edit users",html, 3); +} + +func Pages.EditUser(user) +{ + user.flags = ParseLong(user.flags); + const html = + +

User: {user.username}

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + + +
+
+ + You can{"'"}t edit yourself + + ; + + + return Components.Shell("Edit user",html, 3); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/login.tcross b/Tesses.YouTubeDownloader.Server/src/pages/login.tcross new file mode 100644 index 0000000..d7aa2c9 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/login.tcross @@ -0,0 +1,35 @@ +func Pages.Login(redirect,incorrect) +{ + const html = + + +
+
The login was incorrect, or does not exist
+
+
+ +
+ +
+
+
Login to TYTD2025
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
; + + return Components.ShellSimple("Login", html); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/newaccount.tcross b/Tesses.YouTubeDownloader.Server/src/pages/newaccount.tcross new file mode 100644 index 0000000..37aba13 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/newaccount.tcross @@ -0,0 +1,85 @@ +func Pages.NewUser(error) +{ + const html = + + +
+
{error}
+
+
+ +
+
+
Create new user
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+
; + + return Components.Shell("New User",html ,3); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/oobe/page1.tcross b/Tesses.YouTubeDownloader.Server/src/pages/oobe/page1.tcross new file mode 100644 index 0000000..2683cde --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/oobe/page1.tcross @@ -0,0 +1,73 @@ + +func Pages.OobePage1(state) +{ + + const html= +
+ +
+ +
+
+ + +
+
+ Subscriber Poll Rate +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
; +return Components.ShellSimple("Settings", html); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/oobe/page2.tcross b/Tesses.YouTubeDownloader.Server/src/pages/oobe/page2.tcross new file mode 100644 index 0000000..7d18c19 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/oobe/page2.tcross @@ -0,0 +1,88 @@ +func Pages.OobePage2(tytd,ctx) +{ + var q = ctx.QueryParams.TryGetFirst("q"); + if(TypeOf(q) != "String") + { + q = ""; + } + var server = ctx.QueryParams.TryGetFirst("server"); + if(TypeOf(server) != "String") server = "https://cpkg.tesseslanguage.com/"; + var items=[]; + var items2 = []; + + each(var item : tytd.PackageManager.Search(q,{server,type="lib",pluginHost="tytd2025"})) + { + items2.Add({ + name = item.packageName, + version = item.version, + url = $"{server}/package?name={Net.Http.UrlEncode(item.packageName)}", + thumb = $"{server}/api/v1/package_icon.png?name={Net.Http.UrlEncode(item.packageName)}&version={Net.Http.UrlEncode(item.version)}" + }); + } + + + each(var item : tytd.PackageManager.GetPackageServers()) + { + items.Add({ + active = items.Count == 0, + url = item + }); + } + + + + + var html= + +
+ +
+ + arrow_drop_down +
+
+
+ +
+ + +
+ +
+ + + + + Next +
; + + return Components.ShellSimple("Download plugins",html ,true); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/oobe/page3.tcross b/Tesses.YouTubeDownloader.Server/src/pages/oobe/page3.tcross new file mode 100644 index 0000000..de82c4c --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/oobe/page3.tcross @@ -0,0 +1,44 @@ +func Pages.OobePage3() +{ + + + var html= + +
+ +
+ Do you want to create an account + +
+ +
+
; + +return Components.ShellSimple("Security",html); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/plugins.tcross b/Tesses.YouTubeDownloader.Server/src/pages/plugins.tcross index aaf63d8..7ff683c 100644 --- a/Tesses.YouTubeDownloader.Server/src/pages/plugins.tcross +++ b/Tesses.YouTubeDownloader.Server/src/pages/plugins.tcross @@ -1,6 +1,8 @@ func Pages.Plugins(tytd,ctx) { - var html=