mirror of
https://onedev.site.tesses.net/tytd2025
synced 2026-02-08 09:45:44 +00:00
Add login support and oobe
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
func Components.ShellSimple(title, html, $htmx)
|
||||
{
|
||||
return <!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TYTD - {title}</title>
|
||||
<link rel="stylesheet" href="beer.min.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
<script type="module" src="beer.min.js" defer></script>
|
||||
<if(htmx)>
|
||||
<true>
|
||||
<script src="htmx.min.js" defer></script>
|
||||
</true>
|
||||
</if>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
<main class="responsive">
|
||||
<raw(html)>
|
||||
</main>
|
||||
</body>
|
||||
</html>;
|
||||
}
|
||||
@@ -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", <h1>Malformed data</h1>));
|
||||
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", <h1>Setup page not found</h1>));
|
||||
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","<h1>You are not authorized to download the database</h1>",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",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,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",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,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)
|
||||
|
||||
76
Tesses.YouTubeDownloader.Server/src/pages/admin.tcross
Normal file
76
Tesses.YouTubeDownloader.Server/src/pages/admin.tcross
Normal file
@@ -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 = <form hx-post="./admin" hx-target="body" >
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Enable plugins</h6>
|
||||
<div>Plugins allow you to extend tytd, but they have full access</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<if(enablePlugins)>
|
||||
<true>
|
||||
<input name="enablePlugins" type="checkbox" checked>
|
||||
</true>
|
||||
<false>
|
||||
<input name="enablePlugins" type="checkbox">
|
||||
</false>
|
||||
</if>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="field label border">
|
||||
<input type="text" name="tag" value={tytd.TYTDTag}>
|
||||
<label>TYTD Tag</label>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Subscriber Poll Rate</legend>
|
||||
<div class="row">
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="number" name="hours" value={hours}>
|
||||
<label>Hours</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="number" name="minutes" value={minutes}>
|
||||
<label>Minutes</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="number" name="seconds" value={seconds}>
|
||||
<label>Seconds</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Actions</legend>
|
||||
<button>Save</button>
|
||||
<a class="button" hx-push-url="true" hx-get="./newuser" hx-target="body" href="./newuser">New user</a>
|
||||
|
||||
<a class="button" hx-push-url="true" hx-get="./edituser" hx-target="body" href="./edituser">Edit user</a>
|
||||
<a class="button" hx-push-url="true" hx-get="./settings" hx-target="body" href="./settings">Back to Settings</a>
|
||||
|
||||
</fieldset>
|
||||
|
||||
|
||||
|
||||
|
||||
</form>;
|
||||
|
||||
return Components.Shell("Admin Settings",html ,3);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
func Pages.ChangePassword(redirect,incorrect)
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
const html = <null>
|
||||
<if(incorrect)>
|
||||
<true>
|
||||
<blockquote>
|
||||
<h5>The password could not be changed</h5>
|
||||
</blockquote>
|
||||
</true>
|
||||
</if>
|
||||
<form method="POST" action="./passwd">
|
||||
<input type="hidden" name="redirect" value={redirect}>
|
||||
<article class="border medium no-padding center-align middle-align">
|
||||
<div class="padding">
|
||||
<h5>Change your password</h5>
|
||||
<div class="medium-padding">
|
||||
<div class="field label border">
|
||||
<input type="password" name="password">
|
||||
<label>Current password</label>
|
||||
</div>
|
||||
<div class="field label border">
|
||||
<input type="password" name="newpassword">
|
||||
<label>New password</label>
|
||||
</div>
|
||||
<div class="field label border">
|
||||
<input type="password" name="confirm">
|
||||
<label>Confirm new password</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-padding">
|
||||
|
||||
<button>Change password</button>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="logout" checked>
|
||||
<span>Log me out on all devices</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</form>
|
||||
</null>;
|
||||
|
||||
return Components.Shell("Change your password",html,3);
|
||||
}
|
||||
145
Tesses.YouTubeDownloader.Server/src/pages/edituser.tcross
Normal file
145
Tesses.YouTubeDownloader.Server/src/pages/edituser.tcross
Normal file
@@ -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 = <null>
|
||||
<fieldset>
|
||||
<legend>Find by username</legend>
|
||||
<form method="GET" hx-get="./edituser" action="./edituser" hx-target="body" hx-push-url="true">
|
||||
<div class="field label border">
|
||||
<input type="text" name="user">
|
||||
<label>Username</label>
|
||||
</div>
|
||||
<button>Edit User</button>
|
||||
</form>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Users</legend>
|
||||
<ul class="list border">
|
||||
<if(TypeIsList(users))>
|
||||
<true>
|
||||
<each(var item : users)>
|
||||
<if(item.username != myuser.username)>
|
||||
<true>
|
||||
<li>
|
||||
<a hx-get={$"./edituser?user={Net.Http.UrlEncode(item.username)}"} href={$"./edituser?user={Net.Http.UrlEncode(item.username)}"} hx-target="body" hx-push-url="true">{item.username}</a>
|
||||
</li>
|
||||
</true>
|
||||
</if>
|
||||
</each>
|
||||
</true>
|
||||
</if>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</null>;
|
||||
|
||||
return Components.Shell("Edit users",html, 3);
|
||||
}
|
||||
|
||||
func Pages.EditUser(user)
|
||||
{
|
||||
user.flags = ParseLong(user.flags);
|
||||
const html = <if(TypeIsDictionary(user))>
|
||||
<true>
|
||||
<h1>User: {user.username}</h1>
|
||||
|
||||
<form hx-post={$"./edituser?user={Net.Http.UrlEncode(user.username)}"} hx-target="body" hx-push-url="true">
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Is Admin</h6>
|
||||
<div>Is an administrator (will enable all toggles)</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<if(user.flags & UserFlags.AdminFlag)>
|
||||
<true>
|
||||
<input type="checkbox" name="isAdmin" checked>
|
||||
</true>
|
||||
<false>
|
||||
|
||||
<input type="checkbox" name="isAdmin">
|
||||
</false>
|
||||
</if>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Plugins</h6>
|
||||
<div>Can use plugins</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
|
||||
<if(user.flags & UserFlags.PluginFlag)>
|
||||
<true>
|
||||
<input type="checkbox" name="canUsePlugins" checked>
|
||||
</true>
|
||||
<false>
|
||||
|
||||
<input type="checkbox" name="canUsePlugins">
|
||||
</false>
|
||||
</if>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Manage plugins</h6>
|
||||
<div>Can manage plugins (will also enable Plugins)</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<if(user.flags & UserFlags.ManagePluginFlag)>
|
||||
<true>
|
||||
<input type="checkbox" name="canManagePlugins" checked>
|
||||
</true>
|
||||
<false>
|
||||
|
||||
<input type="checkbox" name="canManagePlugins">
|
||||
</false>
|
||||
</if>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Database Download</h6>
|
||||
<div>Can download database</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<if(user.flags & UserFlags.DatabaseFlag)>
|
||||
<true>
|
||||
<input type="checkbox" name="canDownloadDB" checked>
|
||||
</true>
|
||||
<false>
|
||||
|
||||
<input type="checkbox" name="canDownloadDB">
|
||||
</false>
|
||||
</if>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<button><i>save</i> Save</button>
|
||||
<button class="error-container" hx-delete={$"./edituser?user={Net.Http.UrlEncode(user.username)}"} hx-confirm="Are you sure?"><i>delete_forever</i> Delete</button>
|
||||
</form>
|
||||
</true>
|
||||
<false>
|
||||
You can{"'"}t edit yourself
|
||||
</false>
|
||||
</if>;
|
||||
|
||||
|
||||
return Components.Shell("Edit user",html, 3);
|
||||
}
|
||||
35
Tesses.YouTubeDownloader.Server/src/pages/login.tcross
Normal file
35
Tesses.YouTubeDownloader.Server/src/pages/login.tcross
Normal file
@@ -0,0 +1,35 @@
|
||||
func Pages.Login(redirect,incorrect)
|
||||
{
|
||||
const html = <null>
|
||||
<if(incorrect)>
|
||||
<true>
|
||||
<blockquote>
|
||||
<h5>The login was incorrect, or does not exist</h5>
|
||||
</blockquote>
|
||||
</true>
|
||||
</if>
|
||||
<form method="POST" action="./login">
|
||||
<input type="hidden" name="redirect" value={redirect}>
|
||||
<article class="border medium no-padding center-align middle-align">
|
||||
<div class="padding">
|
||||
<h5>Login to TYTD2025</h5>
|
||||
<div class="medium-padding">
|
||||
<div class="field label border">
|
||||
<input type="text" name="username">
|
||||
<label>Username</label>
|
||||
</div>
|
||||
<div class="field label border">
|
||||
<input type="password" name="password">
|
||||
<label>Password</label>
|
||||
</div>
|
||||
<div class="medium-padding">
|
||||
<button>Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</form>
|
||||
</null>;
|
||||
|
||||
return Components.ShellSimple("Login", html);
|
||||
}
|
||||
85
Tesses.YouTubeDownloader.Server/src/pages/newaccount.tcross
Normal file
85
Tesses.YouTubeDownloader.Server/src/pages/newaccount.tcross
Normal file
@@ -0,0 +1,85 @@
|
||||
func Pages.NewUser(error)
|
||||
{
|
||||
const html = <null>
|
||||
<if(TypeIsString(error))>
|
||||
<true>
|
||||
<blockquote>
|
||||
<h5>{error}</h5>
|
||||
</blockquote>
|
||||
</true>
|
||||
</if>
|
||||
<form hx-post="./newuser" hx-target="body" hx-push-url="true">
|
||||
<div class="padding">
|
||||
<h5>Create new user</h5>
|
||||
<div class="medium-padding">
|
||||
<div class="field label border">
|
||||
<input type="text" name="username">
|
||||
<label>Username</label>
|
||||
</div>
|
||||
<div class="field label border">
|
||||
<input type="password" name="password">
|
||||
<label>Password</label>
|
||||
</div>
|
||||
<div class="field label border">
|
||||
<input type="password" name="confirm">
|
||||
<label>Confirm Password</label>
|
||||
</div>
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Is Admin</h6>
|
||||
<div>Is an administrator (will enable all toggles)</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="isAdmin">
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Plugins</h6>
|
||||
<div>Can use plugins</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="canUsePlugins">
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Manage plugins</h6>
|
||||
<div>Can manage plugins (will also enable Plugins)</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="canManagePlugins">
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Database Download</h6>
|
||||
<div>Can download database</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="canDownloadDB">
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="medium-padding">
|
||||
<button>Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</null>;
|
||||
|
||||
return Components.Shell("New User",html ,3);
|
||||
}
|
||||
73
Tesses.YouTubeDownloader.Server/src/pages/oobe/page1.tcross
Normal file
73
Tesses.YouTubeDownloader.Server/src/pages/oobe/page1.tcross
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
func Pages.OobePage1(state)
|
||||
{
|
||||
|
||||
const html= <null><nav>
|
||||
<div class="center-align">
|
||||
<button class="circle small">1</button>
|
||||
<div class="small-margin">Settings</div>
|
||||
</div>
|
||||
<hr class="max">
|
||||
<div class="center-align">
|
||||
<button class="circle small" disabled>2</button>
|
||||
<div class="small-margin">Plugins</div>
|
||||
</div>
|
||||
<hr class="max">
|
||||
<div class="center-align">
|
||||
<button class="circle small" disabled>3</button>
|
||||
<div class="small-margin">Security</div>
|
||||
</div>
|
||||
</nav>
|
||||
<form action="./oobe" method="POST">
|
||||
<input type="hidden" value="1" name="from">
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Enable plugins</h6>
|
||||
<div>Plugins allow you to extend tytd, but they have full access</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<if(state.enablePlugins)>
|
||||
<true>
|
||||
<input name="enablePlugins" type="checkbox" checked>
|
||||
</true>
|
||||
<false>
|
||||
<input name="enablePlugins" type="checkbox">
|
||||
</false>
|
||||
</if>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="field label border">
|
||||
<input type="text" name="tag" value={state.tag}>
|
||||
<label>TYTD Tag (Name your instance)</label>
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>Subscriber Poll Rate</legend>
|
||||
<div class="row">
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="number" name="hours" value={state.pollHours}>
|
||||
<label>Hours</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="number" name="minutes" value={state.pollMinutes}>
|
||||
<label>Minutes</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="number" name="seconds" value={state.pollSeconds}>
|
||||
<label>Seconds</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<button>Next</button>
|
||||
</form>
|
||||
</null>;
|
||||
return Components.ShellSimple("Settings", html);
|
||||
}
|
||||
88
Tesses.YouTubeDownloader.Server/src/pages/oobe/page2.tcross
Normal file
88
Tesses.YouTubeDownloader.Server/src/pages/oobe/page2.tcross
Normal file
@@ -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= <null>
|
||||
<nav>
|
||||
<div class="center-align">
|
||||
<a href="./oobe?page=1" class="button circle small">
|
||||
<i>done</i>
|
||||
</a>
|
||||
<div class="small-margin">Settings</div>
|
||||
</div>
|
||||
<hr class="max">
|
||||
<div class="center-align">
|
||||
<button class="circle small">2</button>
|
||||
<div class="small-margin">Plugins</div>
|
||||
</div>
|
||||
<hr class="max">
|
||||
<div class="center-align">
|
||||
<button class="circle small" disabled>3</button>
|
||||
<div class="small-margin">Security</div>
|
||||
</div>
|
||||
</nav>
|
||||
<form hx-get="./oobe" hx-target="body" hx-push-url="true">
|
||||
<input type="hidden" name="page" value="2">
|
||||
<div class="field suffix border round">
|
||||
<select name="server">
|
||||
<each(var item : items)>
|
||||
<if(item.active)>
|
||||
<true>
|
||||
<option value={item.url} selected>{item.url}</option>
|
||||
</true>
|
||||
<false>
|
||||
<option value={item.url}>{item.url}</option>
|
||||
</false>
|
||||
</if>
|
||||
</each>
|
||||
</select>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
<div class="row no-space">
|
||||
<div class="field border left-round max">
|
||||
<input type="text" name="q" value={q}>
|
||||
</div>
|
||||
<button type="submit" class="large right-round min">Search</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<each(var item : items2)>
|
||||
<raw(Components.PackageItem(tytd,item))>
|
||||
|
||||
</each>
|
||||
<a class="button" href="./oobe?page=3">Next</a>
|
||||
</null>;
|
||||
|
||||
return Components.ShellSimple("Download plugins",html ,true);
|
||||
}
|
||||
44
Tesses.YouTubeDownloader.Server/src/pages/oobe/page3.tcross
Normal file
44
Tesses.YouTubeDownloader.Server/src/pages/oobe/page3.tcross
Normal file
@@ -0,0 +1,44 @@
|
||||
func Pages.OobePage3()
|
||||
{
|
||||
|
||||
|
||||
var html= <null>
|
||||
<nav>
|
||||
<div class="center-align">
|
||||
<a href="./oobe?page=1" class="button circle small">
|
||||
<i>done</i>
|
||||
</a>
|
||||
<div class="small-margin">Settings</div>
|
||||
</div>
|
||||
<hr class="max">
|
||||
<div class="center-align">
|
||||
<a href="./oobe?page=2" class="button circle small">2</a>
|
||||
<div class="small-margin">Plugins</div>
|
||||
</div>
|
||||
<hr class="max">
|
||||
<div class="center-align">
|
||||
<button class="circle small">3</button>
|
||||
<div class="small-margin">Security</div>
|
||||
</div>
|
||||
</nav>
|
||||
<form method="POST" action="/oobe">
|
||||
<input type="hidden" name="from" value="3">
|
||||
<fieldset>
|
||||
<legend>Do you want to create an account</legend>
|
||||
<nav class="vertical">
|
||||
<label class="radio">
|
||||
<input type="radio" name="user" value="yes" checked>
|
||||
<span>Yes create an account</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="user" value="no">
|
||||
<span>No don{"'"}t create an account (anyone who has access to this interface can use this downloader)</span>
|
||||
</label>
|
||||
</nav>
|
||||
</fieldset>
|
||||
<button>Next</button>
|
||||
</form>
|
||||
</null>;
|
||||
|
||||
return Components.ShellSimple("Security",html);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
func Pages.Plugins(tytd,ctx)
|
||||
{
|
||||
var html= <null><nav class="tabbed">
|
||||
const userFlags = tytd.IsLoggedIn(ctx);
|
||||
|
||||
var html= <null><if(UserFlags.CanManagePlugins(userFlags))><true><nav class="tabbed">
|
||||
<a class="active" hx-get="./plugins" hx-target="body" hx-push-url="true">
|
||||
<i>download_done</i>
|
||||
<span>Installed</span>
|
||||
@@ -10,12 +12,13 @@ func Pages.Plugins(tytd,ctx)
|
||||
<span>Download</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
</true>
|
||||
</if>
|
||||
<each(var item : tytd.Plugins)>
|
||||
<raw(Components.InstalledPlugin(item))>
|
||||
|
||||
</each>
|
||||
</null>;
|
||||
|
||||
return Components.Shell("Installed plugins",html ,2);
|
||||
return Components.Shell("Installed plugins", UserFlags.CanUsePlugins(userFlags) ? html : <h1>You can{"'"}t use plugins</h1> ,2);
|
||||
}
|
||||
@@ -1,68 +1,19 @@
|
||||
func Pages.Settings(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;
|
||||
|
||||
const html= <fieldset>
|
||||
<legend>Actions</legend>
|
||||
<a class="button" hx-push-url="true" hx-get="./admin" hx-target="body" href="./admin">Admin</a>
|
||||
|
||||
<a class="button" hx-push-url="true" href="./api/v1/database.db">Download Database</a>
|
||||
|
||||
|
||||
var html = <form hx-post="./settings" hx-target="body" >
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Enable plugins</h6>
|
||||
<div>Plugins allow you to extend tytd, but they have full access</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<if(enablePlugins)>
|
||||
<true>
|
||||
<input name="enablePlugins" type="checkbox" checked>
|
||||
</true>
|
||||
<false>
|
||||
<input name="enablePlugins" type="checkbox">
|
||||
</false>
|
||||
</if>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="field label border">
|
||||
<input type="text" name="tag" value={tytd.TYTDTag}>
|
||||
<label>TYTD Tag</label>
|
||||
</div>
|
||||
<a class="button" hx-push-url="true" hx-get="./whoami" hx-target="body" href="./whoami">whoami</a>
|
||||
<a class="button" hx-push-url="true" hx-get="./passwd" hx-target="body" href="./passwd">Change password</a>
|
||||
|
||||
<fieldset>
|
||||
<legend>Subscriber Poll Rate</legend>
|
||||
<div class="row">
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="number" name="hours" value={hours}>
|
||||
<label>Hours</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="number" name="minutes" value={minutes}>
|
||||
<label>Minutes</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="number" name="seconds" value={seconds}>
|
||||
<label>Seconds</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<a class="button" href="./logout">Logout</a>
|
||||
|
||||
<footer>
|
||||
<button>Save</button>
|
||||
</footer>
|
||||
|
||||
<a class="button responsive" href="./api/v1/database.db"><i>download</i> Download Database</a>
|
||||
</form>;
|
||||
</fieldset>;
|
||||
|
||||
return Components.Shell("Settings",html ,3);
|
||||
}
|
||||
29
Tesses.YouTubeDownloader.Server/src/pages/welcome.tcross
Normal file
29
Tesses.YouTubeDownloader.Server/src/pages/welcome.tcross
Normal file
@@ -0,0 +1,29 @@
|
||||
func Pages.Welcome(redirect)
|
||||
{
|
||||
const html = <null>
|
||||
<h1>You are all set</h1>
|
||||
<p>
|
||||
This is Tesses YouTube Downloader 2025, a YouTube Downloader created in <a href="https://crosslang.tesseslanguage.com/">CrossLang</a>
|
||||
|
||||
<h4><i>home</i> Home</h4>
|
||||
Once you click the <b>{"I'm Done"}</b> button, you will be able to add YouTube videos to the downloader by using <i>add</i>, you can download the video from the server using <i>download</i> or view metadata/watch the video using <i>info</i> (this also will allow you to view playlist/channel contents)
|
||||
|
||||
<h4><i>download</i> Downloads</h4>
|
||||
You can search or browse <i>movie</i> Videos, <i>list</i> Playlists, <i>person</i> Channels or <i>edit_note</i> Personal Lists (Playlists but created in TYTD2025)
|
||||
|
||||
<h4><i>extension</i> Plugins</h4>
|
||||
You can browse (and access if you click on extension name) for installed extensions in <i>download_done</i> Installed
|
||||
<br>
|
||||
You can search (and install, uninstall or upgrade) plugins on <a href="https://cpkg.tesseslanguage.com/">CPKG</a> or any other CPKG server (requires editing CrossLang config files) in <i>download</i> Download
|
||||
|
||||
<h4><i>settings</i> Settings</h4>
|
||||
You can create users, download database, enable/disable plugin support, your downloader{"'"}s tag, and the interval between subscriptions
|
||||
|
||||
<h4><i>api</i> Api</h4>
|
||||
For developers who want to make apps around the downloader (documentation not complete yet)
|
||||
</p>
|
||||
<a class="button" href={$"./welcome?redirect={Net.Http.UrlEncode(redirect)}&closePopup=true"}>{"I'm Done"}</a>
|
||||
</null>;
|
||||
|
||||
return Components.ShellSimple("Welcome", html);
|
||||
}
|
||||
15
Tesses.YouTubeDownloader.Server/src/pages/whoami.tcross
Normal file
15
Tesses.YouTubeDownloader.Server/src/pages/whoami.tcross
Normal file
@@ -0,0 +1,15 @@
|
||||
func Pages.WhoAmI(row)
|
||||
{
|
||||
const html = <null>
|
||||
<h5>Your username is: {row.username}</h5>
|
||||
|
||||
<h6>Permissions: </h6>
|
||||
<ul>
|
||||
<li>Is admin: {UserFlags.IsAdmin(row.flags)}</li>
|
||||
<li>Can download database: {UserFlags.CanDownloadDB(row.flags)}</li>
|
||||
<li>Can use plugins: {UserFlags.CanUsePlugins(row.flags)}</li>
|
||||
<li>Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}</li>
|
||||
</ul>
|
||||
</null>;
|
||||
return Components.Shell("Settings",html ,3);
|
||||
}
|
||||
@@ -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,10 +1765,218 @@ 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 {
|
||||
@@ -1732,5 +1987,11 @@ class TYTD.Downloader {
|
||||
Console.WriteLine($"ERROR: {ex}");
|
||||
}
|
||||
this.Mutex.Unlock();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user