From 3919384b57b3e0b5f355d03376401878052193b8 Mon Sep 17 00:00:00 2001
From: Mike Nolan
Date: Thu, 29 Jan 2026 15:08:12 -0600
Subject: [PATCH] Add login support and oobe
---
Dockerfile | 1 +
.../src/components/shellsimple.tcross | 27 ++
.../src/main.tcross | 442 +++++++++++++++++-
.../src/pages/admin.tcross | 76 +++
.../src/pages/changepassword.tcross | 49 ++
.../src/pages/edituser.tcross | 145 ++++++
.../src/pages/login.tcross | 35 ++
.../src/pages/newaccount.tcross | 85 ++++
.../src/pages/oobe/page1.tcross | 73 +++
.../src/pages/oobe/page2.tcross | 88 ++++
.../src/pages/oobe/page3.tcross | 44 ++
.../src/pages/plugins.tcross | 9 +-
.../src/pages/settings.tcross | 75 +--
.../src/pages/welcome.tcross | 29 ++
.../src/pages/whoami.tcross | 15 +
.../src/YouTubeDownloader.tcross | 263 ++++++++++-
16 files changed, 1386 insertions(+), 70 deletions(-)
create mode 100644 Tesses.YouTubeDownloader.Server/src/components/shellsimple.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/admin.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/changepassword.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/edituser.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/login.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/newaccount.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/oobe/page1.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/oobe/page2.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/oobe/page3.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/welcome.tcross
create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/whoami.tcross
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 = ;
+
+ 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
+
+
+
+
+;
+
+ 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 =
+
+
+
+ ;
+
+ 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
+
+
+
+
+;
+
+ 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}
+
+
+
+
+;
+
+ 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=
+
+;
+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=
+
+
+
+
+
+
+ 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=
+
+
+;
+
+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=
+ {"I'm Done"}
+ ;
+
+ return Components.ShellSimple("Welcome", html);
+}
\ No newline at end of file
diff --git a/Tesses.YouTubeDownloader.Server/src/pages/whoami.tcross b/Tesses.YouTubeDownloader.Server/src/pages/whoami.tcross
new file mode 100644
index 0000000..b0102e2
--- /dev/null
+++ b/Tesses.YouTubeDownloader.Server/src/pages/whoami.tcross
@@ -0,0 +1,15 @@
+func Pages.WhoAmI(row)
+{
+ const html =
+ Your username is: {row.username}
+
+ Permissions:
+
+ - Is admin: {UserFlags.IsAdmin(row.flags)}
+ - Can download database: {UserFlags.CanDownloadDB(row.flags)}
+ - Can use plugins: {UserFlags.CanUsePlugins(row.flags)}
+ - Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}
+
+ ;
+ return Components.Shell("Settings",html ,3);
+}
\ No newline at end of file
diff --git a/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross b/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross
index 24c3c69..55e5f0d 100644
--- a/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross
+++ b/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross
@@ -1,4 +1,44 @@
+//DO NOT ADD A FLAG WITH ONES BIT SET AS THIS IS
+//TO DENOTE USER IS LOGGED IN
+class UserFlags {
+ static getAdminFlag() 0b00000100;
+ static getPluginFlag() 0b00000010;
+ static getDatabaseFlag() 0b00001000;
+ static getManagePluginFlag() 0b00100000;
+ static IsAdmin(flags)
+ {
+ if(flags & UserFlags.AdminFlag) return true;
+ return false;
+ }
+ static CanDownloadDB(flags)
+ {
+ if(flags & UserFlags.AdminFlag) return true;
+ if(flags & UserFlags.DatabaseFlag) return true;
+ return false;
+ }
+ static CanCreateUsers(flags)
+ {
+ if(flags & UserFlags.AdminFlag) return true;
+ return false;
+ }
+ static CanUsePlugins(flags)
+ {
+ if(flags & UserFlags.AdminFlag) return true;
+ if(flags & UserFlags.ManagePluginFlag) return true;
+ if(flags & UserFlags.PluginFlag) return true;
+ return false;
+ }
+ static CanManagePlugins(flags)
+ {
+ if(flags & UserFlags.AdminFlag) return true;
+ if(flags & UserFlags.ManagePluginFlag) return true;
+ return false;
+ }
+ static getITTR() 35000;
+}
+
class TYTD.Downloader {
+
/^
The storage vfs that TYTD accesses
^/
@@ -715,7 +755,8 @@ class TYTD.Downloader {
public Config = {
TYTDTag = "UnknownPC",
BellTimer = 10800,
- EnablePlugins=true
+ EnablePlugins=true,
+ OobeState = "oobe"
};
public SaveConfig()
{
@@ -1255,6 +1296,8 @@ class TYTD.Downloader {
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS personal_list_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, listName TEXT, videoId TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS plugin_settings (id INTEGER PRIMARY KEY AUTOINCREMENT, extension TEXT, key TEXT, value TEXT, UNIQUE(extension,key) ON CONFLICT REPLACE);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
+ Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password_hash TEXT, password_salt TEXT, flags INTEGER);");
+ Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, key TEXT UNIQUE);");
var config=Sqlite.Exec(db,"SELECT * FROM plugin_settings WHERE extension = '' AND key = 'settings';");
if(TypeOf(config) == "List" && config.Length>0)
{
@@ -1264,6 +1307,10 @@ class TYTD.Downloader {
}
}
+
+ this.Config.OobeState ??= "oobe";
+
+
Sqlite.Close(db);
this.Mutex.Unlock();
@@ -1718,11 +1765,219 @@ class TYTD.Downloader {
this.lastRequest = curRequest;
this.rlm.Unlock();
}
+ public GetSessionToken(ctx)
+ {
+ var cookie = ctx.RequestHeaders.TryGetFirst("Cookie");
+ if(TypeOf(cookie) == "String")
+ {
+ each(var part : cookie.Split("; "))
+ {
+ if(part.Length > 0)
+ {
+ var cookieKV = part.Split("=",true,2);
+ if(cookieKV.Length == 2 && cookieKV[0] == "Session")
+ {
+ return cookieKV[1];
+
+ }
+ }
+ }
+ }
+ var auth = ctx.RequestHeaders.TryGetFirst("Authorization");
+ if(TypeOf(auth) == "String")
+ {
+ auth=auth.Split(" ",true,2);
+ if(auth.Length < 2) return null;
+ if(auth[0] != "Bearer") return null;
+ return auth[1];
+ }
+ return null;
+ }
+ public Logout(ctx)
+ {
+ const token = GetSessionToken(ctx);
+ if(TypeIsString(token))
+ {
+
+ this.Mutex.Lock();
+ const db = this.OpenDB();
+ Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(token)};");
+ Sqlite.Close(db);
+ this.Mutex.Unlock();
+ }
+ }
+
+ public CreateAccount(ctx, username, password, flags)
+ {
+ var loggedIn = this.IsLoggedIn(ctx);
+ if(loggedIn == 0xFFFFFFFE) flags = 0xFFFFFFFE;
+
+ if(UserFlags.CanCreateUsers(loggedIn))
+ {
+ this.Mutex.Lock();
+ const db = this.OpenDB();
+
+ const salt = Crypto.RandomBytes(32, "TYTD2025");
+ const hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
+ const resp = Sqlite.Exec(db, $"INSERT INTO users (username, password_salt, password_hash, flags) VALUES ({Sqlite.Escape(username)}, {Sqlite.Escape(Crypto.Base64Encode(salt))}, {Sqlite.Escape(Crypto.Base64Encode(hash))}, {flags});");
+
+ Sqlite.Close(db);
+ this.Mutex.Unlock();
+
+ if(TypeIsString(resp)) return resp;
+ }
+ else {
+ return "You are not authorized to create user accounts";
+ }
+ return null;
+ }
+
+ public IsLoggedIn(ctx)
+ {
+ this.Mutex.Lock();
+ const db = this.OpenDB();
+ const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
+ var noAccounts=true;
+ if(TypeOf(res) == "List" && res.Length != 0)
+ {
+ if(res[0].["COUNT(*)"] != "0")
+ noAccounts=false;
+ }
+ if(noAccounts) {
+ Sqlite.Close(db);
+ this.Mutex.Unlock();
+ return 0xFFFFFFFE;
+ }
+ const sessionToken = this.GetSessionToken(ctx);
+ if(TypeIsString(sessionToken))
+ {
+ const res = Sqlite.Exec(db, $"SELECT * FROM sessions s INNER JOIN users u ON s.accountId = u.id WHERE key = {Sqlite.Escape(sessionToken)};");
+
+ if(TypeIsList(res))
+ each(var item : res)
+ {
+ Sqlite.Close(db);
+ this.Mutex.Unlock();
+ return ParseLong(item.flags) | 1;
+ }
+ }
+
+ Sqlite.Close(db);
+ this.Mutex.Unlock();
+ return 0;
+ }
+
+ public WhoAmI(ctx)
+ {
+ his.Mutex.Lock();
+ const db = this.OpenDB();
+ const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
+ var noAccounts=true;
+ if(TypeOf(res) == "List" && res.Length != 0)
+ {
+ if(res[0].["COUNT(*)"] != "0")
+ noAccounts=false;
+ }
+ if(noAccounts) {
+ Sqlite.Close(db);
+ this.Mutex.Unlock();
+ return { flags = 0xFFFFFFFE, username = "N/A" };
+ }
+ const sessionToken = this.GetSessionToken(ctx);
+ if(TypeIsString(sessionToken))
+ {
+ const res = Sqlite.Exec(db, $"SELECT * FROM sessions s INNER JOIN users u ON s.accountId = u.id WHERE key = {Sqlite.Escape(sessionToken)};");
+
+ if(TypeIsList(res))
+ each(var item : res)
+ {
+ Sqlite.Close(db);
+ this.Mutex.Unlock();
+ item.flags = ParseLong(item.flags);
+ return item;
+ }
+ }
+
+ Sqlite.Close(db);
+ this.Mutex.Unlock();
+ return { flags = 0, username = "N/A" };
+ }
+ public Passwd(ctx, oldPassword, newPassword, logout)
+ {
+ const whoami = this.WhoAmI(ctx);
+ if(TypeIsDictionary(user) && TypeIsString(item.password_salt))
+ {
+ var salt = Crypto.Base64Decode(item.password_salt);
+ var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
+ var hashStr = Crypto.Base64Encode(hash);
+
+ if(item.password_hash == hashStr)
+ {
+ this.Mutex.Lock();
+ const db = this.OpenDB();
+ 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(item.username)};");
+ if(TypeIsList(res))
+ {
+ if(logout)
+ {
+ Sqlite.Exec(db, $"DELETE FROM sessions WHERE accountId = {Sqlite.Escape(res.accountId)};");
+ }
+ Sqlite.Close(db);
+
+ this.Mutex.Unlock();
+ return {success=true};
+ }
+ else
+ {
+
+ Sqlite.Close(db);
+ this.Mutex.Unlock();
+ return {success=false, reason = res};
+ }
+ }
+
+ }
+ }
+ public Login(username, password)
+ {
+ this.Mutex.Lock();
+ const db = this.OpenDB();
+ const user = Sqlite.Exec(db, $"SELECT * FROM users WHERE username = {Sqlite.Escape(username)};");
+ if(TypeIsList(user))
+ {
+ each(var item : user)
+ {
+ this.Mutex.Unlock();
+
+ var salt = Crypto.Base64Decode(item.password_salt);
+ var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
+ var hashStr = Crypto.Base64Encode(hash);
+
+ if(item.password_hash == hashStr)
+ {
+ var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "TYTD2025")));
+ this.Mutex.Lock();
+ const dbCon = this.OpenDB();
+ Sqlite.Exec(dbCon, $"INSERT INTO sessions (accountId,key) VALUES ({item.id},{Sqlite.Escape(rand)});");
+ Sqlite.Close(dbCon);
+ this.Mutex.Unlock();
+ return rand;
+ }
+
+ return null;
+ }
+ }
+
+ this.Mutex.Unlock();
+ return null;
+ }
/^
Send the database as http response
^/
public SendDatabase(ctx)
{
+ if(UserFlags.CanDownloadDB(this.IsLoggedIn(ctx)))
+ {
this.Mutex.Lock();
try {
var strm = FS.Local.OpenFile(this.DatabaseDirectory/"tytd.db","rb");
@@ -1732,5 +1987,11 @@ class TYTD.Downloader {
Console.WriteLine($"ERROR: {ex}");
}
this.Mutex.Unlock();
+ return true;
+ }
+ return false;
}
+
+
}
+