diff --git a/.gitignore b/.gitignore index fe43eec..35b1d98 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,8 @@ tmp *.vsix vscode-extension/out vscode-extension/package-lock.json -vscode-extension/node-modules \ No newline at end of file +vscode-extension/node-modules +Packages +Tesses.CrossLang.PackageServer/conf.json +Tesses.CrossLang.PackageServer/data.db +Temp \ No newline at end of file diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..00d0ffe --- /dev/null +++ b/Changelog.md @@ -0,0 +1,8 @@ +## Changelog + + +## v0.0.1 +Make CPKG more complete + +## v0.0.0 +Migrate to Gitea \ No newline at end of file diff --git a/README.md b/README.md index 81a9916..eaae3ed 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > :warning: **NOT READY FOR PRODUCTION, BECAUSE IT MAY (WILL) HAVE BREAKING CHANGES** -[CrossLang](https://gitea.site.tesses.net/tesses50/crosslang) is required to build this +[CrossLang](https://git.tesses.org/tesses50/crosslang) is required to build this # To Build ```bash diff --git a/Tesses.CrossLang.PackageServer/src/backend/accounts.tcross b/Tesses.CrossLang.PackageServer/src/backend/accounts.tcross index 1b64c65..ebcf6f9 100644 --- a/Tesses.CrossLang.PackageServer/src/backend/accounts.tcross +++ b/Tesses.CrossLang.PackageServer/src/backend/accounts.tcross @@ -66,7 +66,8 @@ func DB.CreateUserFromAdmin(email, name, password, flags) var res = {Success=true}; DB.Lock(); var dbCon = DB.Open(); - exec = Sqlite.Exec(dbCon,$"SELECT * FROM accounts WHERE accountName = {Sqlite.Escape(name)};"); + var exec = Sqlite.Exec(dbCon,$"SELECT * FROM accounts WHERE accountName = {Sqlite.Escape(name)};"); + if(TypeOf(exec) == "List" && exec.Length > 0) { res = {Success=false, Reason = "Name already exists"}; @@ -75,8 +76,8 @@ func DB.CreateUserFromAdmin(email, name, password, flags) { var salt = Crypto.RandomBytes(32, "CPKG"); var hash = Crypto.PBKDF2(password, salt, DB.ITTR,64,384); - - var r = Sqlite.Exec(dbCon,$"INSERT INTO accounts (email, accountName, password_hash, password_salt, verifyExpire, flags) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{0},{flags});"); + const now = DateTime.NowEpoch ?? 0; + var r = Sqlite.Exec(dbCon,$"INSERT INTO accounts (email, accountName, password_hash, password_salt, verifyExpire, flags, created, verified) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{0},{flags},{now},{now});"); if(TypeOf(r) == "String") res = {Success = false, Reason = r}; res = {Success=true}; } @@ -87,8 +88,12 @@ func DB.CreateUserFromAdmin(email, name, password, flags) var verify_hash = Crypto.RandomBytes(32, "CPKG"); var verify_hash_str = Crypto.Base64Encode(verify_hash); - - var r = Sqlite.Exec(dbCon,$"INSERT INTO accounts (email, accountName, password_hash, password_salt, verifyKey, verifyExpire, flags) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{Sqlite.Escape(verify_hash_str)},{DateTime.NowEpoch+600},{flags});"); + const now = DateTime.NowEpoch ?? 0; + + const resp = $"INSERT INTO accounts (email, accountName, password_hash, password_salt, verifyKey, verifyExpire, flags, created, verified) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{Sqlite.Escape(verify_hash_str)},{now+600},{flags},{now},0);"; + Console.WriteLine(resp); + var r = Sqlite.Exec(dbCon, resp); + Console.WriteLine(r); if(TypeOf(r) == "String") res = {Success = false, Reason = r}; if(DB.Config.MailConfig) @@ -111,8 +116,8 @@ func DB.CreateUser(email, name, password) var salt = Crypto.RandomBytes(32, "CPKG"); var hash = Crypto.PBKDF2(password, salt, DB.ITTR,64,384); - - var r = Sqlite.Exec(dbCon,$"INSERT INTO accounts (email, accountName, password_hash, password_salt, flags) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{DB.FLAG_ADMIN|DB.FLAG_VERIFIED});"); + const now = DateTime.NowEpoch ?? 0; + var r = Sqlite.Exec(dbCon,$"INSERT INTO accounts (email, accountName, password_hash, password_salt, flags, created, verified) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{DB.FLAG_ADMIN|DB.FLAG_VERIFIED},{now},{now});"); if(TypeOf(r) == "String") res = {Success = false, Reason = r}; } @@ -146,8 +151,8 @@ func DB.CreateUser(email, name, password) var verify_hash = Crypto.RandomBytes(32, "CPKG"); var verify_hash_str = Crypto.Base64Encode(verify_hash); - - var r = Sqlite.Exec(dbCon,$"INSERT INTO accounts (email, accountName, password_hash, password_salt, verifyKey, verifyExpire, flags) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{Sqlite.Escape(verify_hash_str)},{DateTime.NowEpoch+600},{DB.FLAG_VERIFY});"); + const now = DateTime.NowEpoch ?? 0; + var r = Sqlite.Exec(dbCon,$"INSERT INTO accounts (email, accountName, password_hash, password_salt, verifyKey, verifyExpire, flags, created, verified) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{Sqlite.Escape(verify_hash_str)},{now+600},{DB.FLAG_VERIFY},{now},0);"); if(TypeOf(r) == "String") {res = {Success = false, Reason = r};} else { if(DB.Config.MailConfig) diff --git a/Tesses.CrossLang.PackageServer/src/backend/db.tcross b/Tesses.CrossLang.PackageServer/src/backend/db.tcross index e2a3051..3aac3db 100644 --- a/Tesses.CrossLang.PackageServer/src/backend/db.tcross +++ b/Tesses.CrossLang.PackageServer/src/backend/db.tcross @@ -105,8 +105,8 @@ func DB.Init(working) var dbCon = DB.Open(); Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS packages (id INTEGER PRIMARY KEY AUTOINCREMENT, packageName TEXT UNIQUE, accountId INTEGER);"); Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS versions (id INTEGER PRIMARY KEY AUTOINCREMENT, packageId INTEGER, version INTEGER, description TEXT, type TEXT, maintainer TEXT, homepage TEXT, repo TEXT, license TEXT, uploadTime INTEGER, pluginHost TEXT);"); - Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE, accountName TEXT UNIQUE, password_hash TEXT, password_salt TEXT, motto TEXT, verifyKey TEXT UNIQUE, verifyExpire INTEGER, flags INTEGER);"); - Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, key STRING UNIQUE);"); + Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE, accountName TEXT UNIQUE, password_hash TEXT, password_salt TEXT, motto TEXT, verifyKey TEXT UNIQUE, verifyExpire INTEGER, flags INTEGER, created INTEGER, verified INTEGER);"); + Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, key STRING UNIQUE, expires INTEGER, created INTEGER, name TEXT);"); Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS reserved_prefixes (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, prefix STRING UNIQUE);"); Sqlite.Close(dbCon); } @@ -119,5 +119,6 @@ DB.FLAG_VERIFY = 0b00000100; DB.ITTR = 35000; +DB.Expires = 86400*7; diff --git a/Tesses.CrossLang.PackageServer/src/backend/email.tcross b/Tesses.CrossLang.PackageServer/src/backend/email.tcross index 7fed1b8..cce25ac 100644 --- a/Tesses.CrossLang.PackageServer/src/backend/email.tcross +++ b/Tesses.CrossLang.PackageServer/src/backend/email.tcross @@ -151,7 +151,8 @@ func DB.VerifyEmail(verifyKey) { flags &= ~DB.FLAG_VERIFY; flags |= DB.FLAG_VERIFIED; - Sqlite.Exec(dbCon,$"UPDATE accounts SET flags = {flags} WHERE id = {exec[0].id};"); + const now = DateTime.NowEpoch; + Sqlite.Exec(dbCon,$"UPDATE accounts SET flags = {flags} WHERE id = {exec[0].id}, verified = {now};"); Sqlite.Close(dbCon); DB.Unlock(); return { Success=true }; diff --git a/Tesses.CrossLang.PackageServer/src/backend/package.tcross b/Tesses.CrossLang.PackageServer/src/backend/package.tcross index 5808e98..f4b0bdb 100644 --- a/Tesses.CrossLang.PackageServer/src/backend/package.tcross +++ b/Tesses.CrossLang.PackageServer/src/backend/package.tcross @@ -457,4 +457,44 @@ func DB.QueryReservedPrefixes(name) Success=true, Items = items }; -} \ No newline at end of file +} + +func DB.DownloadPackage(log,userId,server, name, version) +{ + Console.WriteLine("INDLPKG"); + if(!DB.CanUploadPackagePrefix(userId,name)) return; + + var res = DB.PackageExists(userId, {Name = name, Version = Version.Parse(version)}); + if(res == 2 || res == 4) return; + + const resp=Net.Http.MakeRequest($"{server.TrimEnd('/')}/api/v1/download?name={Net.Http.UrlEncode(name)}&version={Net.Http.UrlEncode(version)}",{FollowRedirects=true}); + if(resp.StatusCode >= 200 && resp.StatusCode <= 299) + { + var filePath = DB.working / "Temp" / $"{DB.GetUniqueNumber()}.crvm"; + + var strm = FS.Local.OpenFile(filePath,"wb"); + resp.CopyToStream(strm); + strm.Close(); + resp.Close(); + + //we need to load the exec, because the main thing erases it + strm = FS.Local.OpenFile(filePath,"rb"); + const exec = VM.LoadExecutable(strm); + strm.Close(); + const status = DB.UploadPackage(userId, filePath); + if(status.Success) + { + each(var item in exec.Dependencies) + { + DB.DownloadPackage(log, userId, server, item.Name, item.Version.ToString()); + } + each(var item in exec.Tools) + { + DB.DownloadPackage(log, userId, server, item.Name, item.Version.ToString()); + } + } + else { + log($"Failed to add package: {name} with {version}"); + } + } +} diff --git a/Tesses.CrossLang.PackageServer/src/backend/session.tcross b/Tesses.CrossLang.PackageServer/src/backend/session.tcross index 969a63e..c5589ce 100644 --- a/Tesses.CrossLang.PackageServer/src/backend/session.tcross +++ b/Tesses.CrossLang.PackageServer/src/backend/session.tcross @@ -4,12 +4,37 @@ func DB.GetUserIdFromSession(session) var dbCon = DB.Open(); var exec = Sqlite.Exec(dbCon,$"SELECT * FROM sessions WHERE key = {Sqlite.Escape(session)};"); - Sqlite.Close(dbCon); - DB.Unlock(); + - if(TypeOf(exec) == "List" && exec.Length == 1) return ParseLong(exec[0].accountId); + if(TypeOf(exec) == "List" && exec.Length == 1) + { + var expiry = 0; + const expires = ParseLong(exec[0].expires); + if(TypeIsLong(expires) && expires != 0) + { + const whenItExpires = ParseLong(expires); + const currentTime = DateTime.NowEpoch ?? 0; + if(whenItExpires != 0 && currentTime < whenItExpires && (whenItExpires - currentTime) < (DB.Expires-3600)) + { + expiry = currentTime + DB.Expires; + Sqlite.Exec(dbCon, $"UPDATE sessions SET expires = {expiry} WHERE key = {Sqlite.Escape(session)};"); + } + else if(whenItExpires != 0 && currentTime >= whenItExpires) + { + Sqlite.Exec(dbCon, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(session)};"); + Sqlite.Close(dbCon); + DB.Unlock(); + return null; + } - return -1; + } + Sqlite.Close(dbCon); + DB.Unlock(); + return { accountId = ParseLong(exec[0].accountId), expiry }; + } + Sqlite.Close(dbCon); + DB.Unlock(); + return null; } func DB.GetSessionFromBearer(ctx) { @@ -20,7 +45,7 @@ func DB.GetSessionFromBearer(ctx) if(auth.Length < 2) return null; if(auth[0] != "Bearer") return null; var uid = DB.GetUserIdFromSession(auth[1]); - if(uid != -1) return auth[1]; + if(TypeIsDictionary(uid)) return auth[1]; } return null; } @@ -37,23 +62,34 @@ func DB.GetSession(ctx) if(cookieKV.Length == 2 && cookieKV[0] == "Session") { var session = cookieKV[1]; - var sessionId = DB.GetUserIdFromSession(session); + var sessionObj = DB.GetUserIdFromSession(session); - if(sessionId != -1) + if(TypeIsDictionary(sessionObj)) + { + if(sessionObj.expiry > 0) + { + ctx.WithHeader("Set-Cookie",$"Session={session}; SameSite=Lax; Expires={new DateTime(sessionObj.expiry).ToHttpDate()}"); + } + return session; + } } } } } return null; } -func DB.CreateSession(userId) +func DB.CreateSession(userId, doesExpire, name) { + const now = DateTime.NowEpoch; + const expiryTime = doesExpire ? (DB.Expires + now) : 0; + + DB.Lock(); var dbCon = DB.Open(); var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "CPKG"))); - Sqlite.Exec(dbCon, $"INSERT INTO sessions (accountId,key) VALUES ({userId},{Sqlite.Escape(rand)});"); + Sqlite.Exec(dbCon,$"INSERT INTO sessions (accountId,key, expires, created, name) VALUES ({userId},{Sqlite.Escape(rand)},{Sqlite.Escape(expiryTime)},{Sqlite.Escape(now)}, {Sqlite.Escape(name)});"); Sqlite.Close(dbCon); DB.Unlock(); return rand; diff --git a/Tesses.CrossLang.PackageServer/src/pages/account.tcross b/Tesses.CrossLang.PackageServer/src/pages/account.tcross index a086ef6..0cde958 100644 --- a/Tesses.CrossLang.PackageServer/src/pages/account.tcross +++ b/Tesses.CrossLang.PackageServer/src/pages/account.tcross @@ -81,8 +81,18 @@ func Pages.Account(ctx)

{user.accountName}

- Packages|Reserved Prefixes - +
Created: {new DateTime(ParseLong(user.created)).ToString("%Y/%m/%d %H:%M:%S UTC")}
+ + +
Verified: {new DateTime(ParseLong(user.verified)).ToString("%Y/%m/%d %H:%M:%S UTC")}
+
+ +
Not verified
+
+ +
+ Packages | Reserved Prefixes +
@@ -100,6 +110,7 @@ func Pages.Account(ctx) Admin + Sessions Delete Packages Logout diff --git a/Tesses.CrossLang.PackageServer/src/pages/admin.tcross b/Tesses.CrossLang.PackageServer/src/pages/admin.tcross index 51e8754..a437a3e 100644 --- a/Tesses.CrossLang.PackageServer/src/pages/admin.tcross +++ b/Tesses.CrossLang.PackageServer/src/pages/admin.tcross @@ -33,6 +33,95 @@ func Pages.Admin(ctx) //we have authorization switch(action2) { + case "cache_package": + { + const userInfo = DB.GetUserIdFromSession(active.session); + const url = ctx.QueryParams.TryGetFirst("url"); + + if(!TypeIsString(url) || url.Length == 0 || !url.Contains("/package?name=") || !TypeIsDictionary(userInfo)) + { + ctx.StatusCode = 400; + return Shell("Invalid input", pages,

Invalid Input

); + } + + const components=url.Split("/package?name="); + if(components.Length < 2) + { + ctx.StatusCode = 400; + return Shell("Invalid input", pages,

Invalid Input

); + } + + + const strm = ctx.WithMimeType("text/html").OpenResponseStream(); + const textWriter = new StreamWriter(strm); + textWriter.Write(""); + + var finished = false; + var errors = ""; + + const thread = new Thread(()=>{ + + func log(err) + { + errors += "{err}\n"; + } + const server = components[0]; + const name = Net.Http.UrlDecode(components[1]); + + try { + const resp=Net.Http.MakeRequest($"{server.TrimEnd('/')}/api/v1/versions?name={Net.Http.UrlEncode(name)}",{FollowRedirects=true}); + + if(resp.StatusCode >= 200 && resp.StatusCode <= 299) + { + const respJson = resp.ReadAsJson(); + resp.Close(); + if(respJson.success) + { + if(TypeIsList(respJson.versions)) + { + each(var item in respJson.versions) + { + if(TypeIsString(item.version)) + { + DB.DownloadPackage(log,userInfo.accountId,server, name, item.version); + } + } + } + else { + log("versions is not list"); + } + } + else + { + log("versions json does not indicate success"); + } + } + else { + log($"Status code does not indicate success {resp.StatusCode} {Net.Http.StatusCodeString(resp.StatusCode)}"); + } + } catch(ex) { + log(ex.ToString()); + } + + + finished=true; + }); + + + while(!finished) + { + textWriter.Write("."); + DateTime.Sleep(1000); + } + errors = Net.Http.HtmlEncode(errors).Replace("\n","
"); + textWriter.Write($"{}
Done Go Back"); + + thread.Join(); + + return null; + } + + break; case "server_config": { var prefix = ctx.QueryParams.TryGetFirst("prefix"); @@ -129,7 +218,7 @@ func Pages.Admin(ctx)
Accounts -
+
diff --git a/Tesses.CrossLang.PackageServer/src/pages/admin_account.tcross b/Tesses.CrossLang.PackageServer/src/pages/admin_account.tcross new file mode 100644 index 0000000..27afdc2 --- /dev/null +++ b/Tesses.CrossLang.PackageServer/src/pages/admin_account.tcross @@ -0,0 +1,305 @@ +func Pages.AdminAccount(ctx) +{ + var active = DB.LoginButton(ctx,false,""); + var csrf=""; + + + var pages = [ + { + active = false, + route = "/packages", + text = "Packages" + }, + { + active = false, + route = "/upload", + text = "Upload" + }, + active + ]; + if(!active.admin) ctx.StatusCode = 401; + + + if(ctx.Method == "POST") + { + var csrf2 = ctx.QueryParams.TryGetFirst("csrf"); + if(!active.admin) {ctx.StatusCode = 401; return Shell("Not an admin", pages,

Not an admin

);} + if(TypeOf(csrf2) != "String") {ctx.StatusCode = 401; return Shell("Invalid CSRF", pages,

Invalid CSRF

);} + if(DB.VerifyCSRF(active.session, csrf2)) + { + + const oldname = ctx.QueryParams.TryGetFirst("oldname"); + const newname = ctx.QueryParams.TryGetFirst("newname"); + const motto = ctx.QueryParams.TryGetFirst("motto") ?? ""; + const admin = ctx.QueryParams.GetFirstBoolean("admin"); + const verified = ctx.QueryParams.GetFirstBoolean("verified"); + if(TypeIsString(oldname) && TypeIsString(newname)) + { + + const userInfo = DB.GetAccountInfo(oldname); + + if(TypeIsDictionary(userInfo)) + { + var flags = ParseLong(userInfo.flags); + //CREATE TABLE IF NOT EXISTS accounts + //(id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE, accountName TEXT UNIQUE, + //password_hash TEXT, password_salt TEXT, motto TEXT, verifyKey TEXT UNIQUE, + //verifyExpire INTEGER, flags INTEGER, created INTEGER, verified INTEGER); + + const wasVerified = (flags & DB.FLAG_VERIFIED) != 0; + + if(userInfo.accountName != active.text) + { + if(!admin) + { + flags &= ~DB.FLAG_ADMIN; + } + if(!verified) + { + flags |= DB.FLAG_VERIFY; + flags &= ~DB.FLAG_VERIFIED; + } + + } + + if(admin) + { + flags |= DB.FLAG_ADMIN; + } + + if(verified) + { + + flags |= DB.FLAG_VERIFIED; + flags &= ~DB.FLAG_VERIFY; + } + + DB.Lock(); + const dbCon = DB.Open(); + if(!wasVerified && verified) + { + Sqlite.Exec(dbCon, $"UPDATE accounts SET accountName = {Sqlite.Escape(newname)}, motto = {Sqlite.Escape(motto)}, flags = {Sqlite.Escape(flags)}, verified = {Sqlite.Escape(DateTime.NowEpoch ?? 0)} WHERE id = {Sqlite.Escape(userInfo.id)};"); + + } + else { + Sqlite.Exec(dbCon, $"UPDATE accounts SET accountName = {Sqlite.Escape(newname)}, motto = {Sqlite.Escape(motto)}, flags = {Sqlite.Escape(flags)} WHERE id = {Sqlite.Escape(userInfo.id)};"); + + } + + Sqlite.Close(dbCon); + DB.Unlock(); + + + ctx.StatusCode=303; + ctx.ResponseHeaders.SetValue("Location", "/admin_accounts"); + return Shell("Redirect",pages, + +

Redirecting

+ Click here if it does not redirect +
+ ); + } + } + + ctx.StatusCode=400; + return Shell("Must need a user",pages, + +

Must need a user

+ Click here to go back to admin list +
+ ); + + + + } + else + { + ctx.StatusCode = 401; return Shell("Invalid CSRF", pages,

Invalid CSRF

); + } + } + const name = ctx.QueryParams.TryGetFirst("account"); + const page = ctx.QueryParams.TryGetFirstInt("page") ?? 1; + var cur = (page - 1) % 3; + var firstPage = (page-1) - cur; + var userInfo = null; + var motto_ta = ""; + const list = []; + + if(active.admin && TypeIsString(name)) + { + userInfo = DB.GetAccountInfo(name); + if(!TypeIsDictionary(userInfo)) + { + if(TypeIsString(userInfo) && userInfo == "No such user exists") + ctx.StatusCode = 404; + else + ctx.StatusCode = 500; + } + else { + csrf = DB.CreateCSRF(ctx); + motto_ta = TypeOf(userInfo.motto) == "String" ? userInfo.motto : ""; + userInfo.flags = ParseLong(userInfo.flags); + } + } + + if(active.admin && !TypeIsString(name)) + { + const limit = 20; + DB.Lock(); + const db = DB.Open(); + const res = Sqlite.Exec(db, $"SELECT * FROM accounts LIMIT {Sqlite.Escape(limit)} OFFSET {Sqlite.Escape((page-1)*limit)};"); + Sqlite.Close(db); + DB.Unlock(); + + if(TypeIsList(res)) + { + each(var item in res) + { + const flags = ParseLong(item.flags); + + + list.Add({ + name = item.accountName, + created = new DateTime(ParseLong(item.created)).ToString("%Y/%m/%d %H:%M:%S UTC"), + verified = (flags & DB.FLAG_VERIFIED) ? (new DateTime(ParseLong(item.verified)).ToString("%Y/%m/%d %H:%M:%S UTC")) : "N/A", + admin = (flags & DB.FLAG_ADMIN) ? "Yes" : "No" + }); + } + } + else { + ctx.StatusCode = 500; + return Shell("Error", pages, +

Error {res}

); + + } + } + + var html =
+ + + + + + + + + + +
+ + +
+
+ + + + + +
+
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + +
+ + Back + +
+ +

Error {userInfo}

+
+ +
+ + + Back To Admin + + + + + + + + + + + + + + + + + + + +
NameCreatedVerifiedAdmin
{item.name}{item.created}{item.verified}{item.admin}
+ + +
+ +
+ +

You are not authorized in the admin panel

+
+ +
; + + return Shell("Admin Register", pages,html); +} \ No newline at end of file diff --git a/Tesses.CrossLang.PackageServer/src/pages/admin_register.tcross b/Tesses.CrossLang.PackageServer/src/pages/admin_register.tcross index 00415cb..0d5e917 100644 --- a/Tesses.CrossLang.PackageServer/src/pages/admin_register.tcross +++ b/Tesses.CrossLang.PackageServer/src/pages/admin_register.tcross @@ -31,7 +31,7 @@ func Pages.AdminRegister(ctx) var displayName = ctx.QueryParams.TryGetFirst("displayName"); var password = ctx.QueryParams.TryGetFirst("password"); var confirm = ctx.QueryParams.TryGetFirst("confirm"); - var flags = ctx.QueryParams.GetFirstBoolean("verified") ? DB.FLAG_VERIFIED : DB.FLAG_FLAG_VERIFY; + var flags = ctx.QueryParams.GetFirstBoolean("verified") ? DB.FLAG_VERIFIED : DB.FLAG_VERIFY; flags |= (ctx.QueryParams.GetFirstBoolean("admin") ? DB.FLAG_ADMIN : 0); if(TypeOf(email) != "String" || TypeOf(displayName) != "String" || TypeOf(password) != "String" || TypeOf(confirm) != "String") diff --git a/Tesses.CrossLang.PackageServer/src/pages/check_email.tcross b/Tesses.CrossLang.PackageServer/src/pages/check_email.tcross index 2e9921e..9b697b8 100644 --- a/Tesses.CrossLang.PackageServer/src/pages/check_email.tcross +++ b/Tesses.CrossLang.PackageServer/src/pages/check_email.tcross @@ -14,10 +14,20 @@ func Pages.CheckEmail(ctx) DB.LoginButton(ctx,false) ]; var html =
-

Please check your email.

-

- The email may or may not be in your spam. -

+ + +

Please check your email.

+

+ The email may or may not be in your spam. +

+
+ +

The admin will need to verify your account

+

+ Mail is disabled +

+
+
; - return Shell("Check your email",pages,html); + return Shell(DB.Config.MailConfig ? "Check your email" : "Admin will need to verify",pages,html); } \ No newline at end of file diff --git a/Tesses.CrossLang.PackageServer/src/pages/create_session.tcross b/Tesses.CrossLang.PackageServer/src/pages/create_session.tcross new file mode 100644 index 0000000..83c0039 --- /dev/null +++ b/Tesses.CrossLang.PackageServer/src/pages/create_session.tcross @@ -0,0 +1,66 @@ +func Pages.CreateSession(ctx) +{ + var active = DB.LoginButton(ctx,false,""); + + var pages = [ + { + active = false, + route = "/packages", + text = "Packages" + }, + { + active = false, + route = "/upload", + text = "Upload" + }, + active + ]; + + const account = DB.GetUserIdFromSession(active.session); + + if(TypeIsDictionary(account)) + { + if(ctx.Method == "GET") + { + + + return Shell("Create API Key", pages, +
+

Create session

+
+ +
+ + +
+ +
+
+ ); + } + else if(ctx.Method == "POST") + { + var name = ctx.QueryParams.TryGetFirst("name") ?? "CrossLang Shell"; + var csrf = ctx.QueryParams.TryGetFirst("csrf"); + + if(!DB.VerifyCSRF(active.session,csrf)) + return Shell("Invalid CSRF", pages, "

Invalid CSRF

"); + + + return Shell("Your API key", pages, +
+

Your API key

+

{"It won't be shown again"}

+
+ +
+ +
+
+
+ ); + + } + } + return Shell("Error", pages, "

Not logged in

"); +} \ No newline at end of file diff --git a/Tesses.CrossLang.PackageServer/src/pages/session.tcross b/Tesses.CrossLang.PackageServer/src/pages/session.tcross new file mode 100644 index 0000000..02318ec --- /dev/null +++ b/Tesses.CrossLang.PackageServer/src/pages/session.tcross @@ -0,0 +1,74 @@ +func Pages.Session(ctx) +{ + var active = DB.LoginButton(ctx,false,""); + + var pages = [ + { + active = false, + route = "/packages", + text = "Packages" + }, + { + active = false, + route = "/upload", + text = "Upload" + }, + active + ]; + + const account = DB.GetUserIdFromSession(active.session); + const id = ctx.QueryParams.TryGetFirstInt("id"); + if(TypeIsDictionary(account) && TypeIsLong(id)) + { + if(ctx.Method == "GET") + { + DB.Lock(); + const dbCon = DB.Open(); + const results = Sqlite.Exec(dbCon, $"SELECT * FROM sessions WHERE accountId = {Sqlite.Escape(account.accountId)} AND id = {Sqlite.Escape(id)};"); + Sqlite.Close(dbCon); + DB.Unlock(); + + return Shell("Delete session?", pages, + +
+

Do you want to delete session: {results[0].name}?

+

Created: {new DateTime(ParseLong(results[0].created) ?? 0).ToString("%Y/%m/%d %H:%M:%S UTC")}

+

Expires: {results[0].expires == "0" ? "Won't" : new DateTime(ParseLong(results[0].expires) ?? 0).ToString("%Y/%m/%d %H:%M:%S UTC")}

+

Type: {results[0].key == active.session ? "This Session" : results[0].expires == "0" ? "API Key" : "Browser"}

+
+ + + + +
+
+
+ ); + } + else if(ctx.Method == "POST") + { + var confirm = ctx.QueryParams.TryGetFirst("confirm") ?? "No"; + + if(confirm == "Yes") + { + var csrf = ctx.QueryParams.TryGetFirst("csrf"); + + if(!DB.VerifyCSRF(active.session,csrf)) + return Shell("Invalid CSRF", pages, "

Invalid CSRF

"); + + DB.Lock(); + const dbCon = DB.Open(); + Sqlite.Exec(dbCon, $"DELETE FROM sessions WHERE accountId = {Sqlite.Escape(account.accountId)} AND id = {Sqlite.Escape(id)};"); + Sqlite.Close(dbCon); + DB.Unlock(); + } + ctx.StatusCode = 303; + ctx.ResponseHeaders.SetValue("Location", "./sessions"); + return Shell("Redirecting", pages, +

Redirecting

+ Click here if it does not redirect +
); + } + } + return Shell("Error", pages, "

Not logged in

"); +} \ No newline at end of file diff --git a/Tesses.CrossLang.PackageServer/src/pages/sessions.tcross b/Tesses.CrossLang.PackageServer/src/pages/sessions.tcross new file mode 100644 index 0000000..add8db8 --- /dev/null +++ b/Tesses.CrossLang.PackageServer/src/pages/sessions.tcross @@ -0,0 +1,62 @@ +func Pages.Sessions(ctx) +{ + var active = DB.LoginButton(ctx,false,""); + + var pages = [ + { + active = false, + route = "/packages", + text = "Packages" + }, + { + active = false, + route = "/upload", + text = "Upload" + }, + active + ]; + + const account = DB.GetUserIdFromSession(active.session); + + if(TypeIsDictionary(account)) + { + DB.Lock(); + const dbCon = DB.Open(); + const results = Sqlite.Exec(dbCon, $"SELECT * FROM sessions WHERE accountId = {Sqlite.Escape(account.accountId)};"); + Sqlite.Close(dbCon); + DB.Unlock(); + + return Shell("Sessions", + pages, +
+ Create API Key + + + + + + + + + + + + + + + + + + + + + +
NameCreatedExpiresType
{item.name}{new DateTime(ParseLong(item.created) ?? 0).ToString("%Y/%m/%d %H:%M:%S UTC")}{item.expires == "0" ? "Won't" : new DateTime(ParseLong(item.expires) ?? 0).ToString("%Y/%m/%d %H:%M:%S UTC")}{item.key == active.session ? "This Session" : item.expires == "0" ? "API Key" : "Browser"}
+
+ +
+ ); + } + + return Shell("You are not logged in",pages,

You are not logged in

); +} \ No newline at end of file diff --git a/Tesses.CrossLang.PackageServer/src/program.tcross b/Tesses.CrossLang.PackageServer/src/program.tcross index 95ed6d0..45b63a0 100644 --- a/Tesses.CrossLang.PackageServer/src/program.tcross +++ b/Tesses.CrossLang.PackageServer/src/program.tcross @@ -1,5 +1,7 @@ func main(args) { + + var dir = "."; if(args.Length > 1) { @@ -7,6 +9,14 @@ func main(args) } DB.Init(dir); + const timer = new Timer(()=>{ + const time = DateTime.NowEpoch ?? 0; + DB.Lock(); + const db = DB.Open(); + Sqlite.Exec(db, "DELETE FROM sessions WHERE expires < {time} AND expires > 0;"); + Sqlite.Close(db); + DB.Unlock(); + },900000); @@ -41,6 +51,11 @@ func main(args) ctx.WithMimeType("text/html").SendText(Pages.AdminRegister(ctx)); return true; } + if(ctx.Path == "/admin_accounts") + { + ctx.WithMimeType("text/html").SendText(Pages.AdminAccount(ctx)); + return true; + } if(ctx.Path == "/package") { var name = ctx.QueryParams.TryGetFirst("name"); @@ -137,9 +152,9 @@ func main(args) if(DB.VerifyCSRF(session,csrf)) { - var userId = DB.GetUserIdFromSession(session); + var userId = DB.GetUserIdFromSession(session).accountId; var url = DB.ChangeMotto(userId,motto); - ctx.StatusCode = 302; + ctx.StatusCode = 303; ctx.ResponseHeaders.SetValue("Location", url); ctx.WithMimeType("text/html").SendText(

Redirecting

@@ -194,7 +209,7 @@ func main(args) if(!DB.VerifyCSRF(session,csrf)) { - var userId = DB.GetUserIdFromSession(session); + var userId = DB.GetUserIdFromSession(session).accountId; result = DB.UploadPackage(userId, filePath); } @@ -203,7 +218,7 @@ func main(args) if(result.Success) { - ctx.StatusCode = 302; + ctx.StatusCode = 303; ctx.ResponseHeaders.SetValue("Location", "/"); ctx.WithMimeType("text/html").SendText(

Redirecting

@@ -279,7 +294,7 @@ func main(args) }); return true; } - var userId = DB.GetUserIdFromSession(session); + var userId = DB.GetUserIdFromSession(session).accountId; var filePath = DB.working / "Temp" / $"{DB.GetUniqueNumber()}.crvm"; var strm = FS.Local.OpenFile(filePath,"wb"); @@ -335,7 +350,7 @@ func main(args) ctx.SendJson({ - token = DB.CreateSession(accountId) + token = DB.CreateSession(accountId,false, TypeIsString(json.name) ? json.name : "CrossLang Shell") }); return true; @@ -432,13 +447,16 @@ func main(args) if(accountId == -1) { ctx.StatusCode = 400; - ctx.SendText("

Invalid credentials

"); + ctx.SendText(Shell("Invalid credentials",[],"

Invalid credentials

")); return true; } - ctx.StatusCode = 302; + ctx.StatusCode = 303; ctx.ResponseHeaders.SetValue("Location", "/"); - ctx.ResponseHeaders.SetValue("Set-Cookie", $"Session={DB.CreateSession(accountId)}; SameSite=Strict"); + const browser="Browser"; + const now = DateTime.NowEpoch??0; + + ctx.ResponseHeaders.SetValue("Set-Cookie", $"Session={DB.CreateSession(accountId,true, browser)}; SameSite=Strict; Expires={new DateTime(now+DB.Expires).ToHttpDate()}"); ctx.WithMimeType("text/html").SendText(

Redirecting

Click here if it does not redirect @@ -492,7 +510,7 @@ func main(args) var res = DB.UnforgetPassword(code,password,confirm); if(res.Success) { - ctx.StatusCode = 302; + ctx.StatusCode = 303; ctx.ResponseHeaders.SetValue("Location", "/"); ctx.WithMimeType("text/html").SendText(

Redirecting

@@ -538,7 +556,7 @@ func main(args) } - ctx.StatusCode = 302; + ctx.StatusCode = 303; ctx.ResponseHeaders.SetValue("Location", res.Redirect); ctx.WithMimeType("text/html").SendText(

Redirecting

@@ -596,12 +614,12 @@ func main(args) if(!res.Success) { ctx.StatusCode = 400; - ctx.SendText($"

Error: {Net.Http.HtmlEncode(res.Reason)}

"); + ctx.SendText(Shell("Error",[],$"

Error: {Net.Http.HtmlEncode(res.Reason)}

")); return true; } - ctx.StatusCode = 302; + ctx.StatusCode = 303; ctx.ResponseHeaders.SetValue("Location", res.Redirect); ctx.WithMimeType("text/html").SendText(

Redirecting

@@ -615,6 +633,22 @@ func main(args) ctx.WithMimeType("text/html").SendText(Pages.Account(ctx)); return true; } + if(ctx.Path == "/sessions") + { + + ctx.WithMimeType("text/html").SendText(Pages.Sessions(ctx)); + return true; + } + if(ctx.Path == "/create_session") + { + ctx.WithMimeType("text/html").SendText(Pages.CreateSession(ctx)); + return true; + } + if(ctx.Path == "/session") + { + ctx.WithMimeType("text/html").SendText(Pages.Session(ctx)); + return true; + } if(ctx.Path == "/css/bootstrap.min.css") { ctx.WithMimeType("text/css").SendBytes(embed("css/bootstrap.min.css")); @@ -650,4 +684,6 @@ func main(args) return false; },DB.Port); + + timer.Callback = null; } \ No newline at end of file diff --git a/Tesses.CrossLang.Shell/src/login.tcross b/Tesses.CrossLang.Shell/src/login.tcross index c07053e..2c3f854 100644 --- a/Tesses.CrossLang.Shell/src/login.tcross +++ b/Tesses.CrossLang.Shell/src/login.tcross @@ -33,6 +33,9 @@ func Tesses.CrossLang.Shell.Login(dd) var name = dd.Arguments[1]; var host = dd.Arguments[2]; + Console.Write("Name (empty for CrossLang Shell): "); + var name = Console.ReadLine() ?? ""; + if(name == "") name = "CrossLang Shell"; Console.Write("Email: "); var email = Console.ReadLine(); @@ -45,6 +48,7 @@ func Tesses.CrossLang.Shell.Login(dd) Console.WriteLine(); var accountRequest = { + name, email, password }; diff --git a/Tesses.CrossLang.Shell/src/logout.tcross b/Tesses.CrossLang.Shell/src/logout.tcross new file mode 100644 index 0000000..1dd6d48 --- /dev/null +++ b/Tesses.CrossLang.Shell/src/logout.tcross @@ -0,0 +1,168 @@ +func Tesses.CrossLang.Shell.Logout(dd) +{ + var accounts = []; + + func help() + { + Console.WriteLine("USAGE: crosslang logout [name]"); + Console.WriteLine(); + Console.WriteLine("ARGUMENTS:"); + Console.WriteLine("name: the session name"); + } + + + if(FS.Local.FileExists(Env.CrossLangConfig / "auth.json")) + { + accounts = Json.Decode(FS.ReadAllText(FS.Local,Env.CrossLangConfig / "auth.json")); + if(TypeOf(accounts) != "List") accounts = []; + } + + if(dd.Flags.Contains("help")) + { + help(); + } + else { + if(FS.Local.FileExists(Env.CrossLangConfig / "auth.json")) + { + const json = Json.Decode(FS.ReadAllText(FS.Local,Env.CrossLangConfig / "auth.json")); + if(json.Length == 0) + { + Console.WriteLine("You were not logged in"); + return 0; + } + else if(json.Length == 1) + { + const host = json[0].host; + const token = json[0].token; + + if(!TypeIsString(host)) + { + Console.WriteLine("Host is not a string"); + FS.WriteAllText(FS.Local, Env.CrossLangConfig / "auth.json", "[]"); + return 1; + } + + if(!TypeIsString(token)) + { + Console.WriteLine("Token is not a string"); + FS.WriteAllText(FS.Local, Env.CrossLangConfig / "auth.json", "[]"); + return 1; + } + + const resp = Net.Http.MakeRequest($"{host.TrimEnd('/')}/api/v1/logout",{ + RequestHeaders = [ + {Key= "Authorization",Value=$"Bearer {token}"} + ], + Method = "GET" + }); + + if(resp.StatusCode == 200) + { + const json = resp.ReadAsJson(); + if(json.Success) + { + + FS.WriteAllText(FS.Local, Env.CrossLangConfig / "auth.json", "[]"); + resp.Close(); + return 0; + } + else { + FS.WriteAllText(FS.Local, Env.CrossLangConfig / "auth.json", "[]"); + resp.Close(); + Console.WriteLine($"Failed to logout, go to {host.TrimEnd('/')}/sessions to logout"); + return 1; + } + } + + resp.Close(); + return 1; + } + else { + if(dd.Arguments.Length < 2) + { + Console.WriteLine("Multiple entries in auth.json file, session is ambiguous."); + Console.WriteLine("Sessions:"); + each(var item : json) + { + Console.WriteLine($"{item.name}: {item.host}"); + } + } + else { + var host = ""; + var token = ""; + var found = false; + var cur = null; + each(var item : json) + { + + if(item.name == dd.Arguments[1]) + { + cur = item; + found=true; + host = item.host; + token = item.token; + break; + } + } + + if(!found) { + Console.WriteLine($"Could not find session with name: {dd.Arguments[1]}"); + return 1; + } + + + if(!TypeIsString(host)) + { + Console.WriteLine("Host is not a string"); + json.Remove(cur); + FS.WriteAllText(FS.Local, Env.CrossLangConfig / "auth.json", json.ToString()); + + return 1; + } + + if(!TypeIsString(token)) + { + Console.WriteLine("Token is not a string"); + json.Remove(cur); + FS.WriteAllText(FS.Local, Env.CrossLangConfig / "auth.json", json.ToString()); + + return 1; + } + + const resp = Net.Http.MakeRequest($"{host.TrimEnd('/')}/api/v1/logout",{ + RequestHeaders = [ + {Key= "Authorization",Value=$"Bearer {token}"} + ], + Method = "GET" + }); + + if(resp.StatusCode == 200) + { + const json2 = resp.ReadAsJson(); + if(json2.Success) + { + json.Remove(cur); + FS.WriteAllText(FS.Local, Env.CrossLangConfig / "auth.json", json.ToString()); + resp.Close(); + return 0; + } + else { + + json.Remove(cur); + FS.WriteAllText(FS.Local, Env.CrossLangConfig / "auth.json", json.ToString()); + resp.Close(); + Console.WriteLine($"Failed to logout, go to {host.TrimEnd('/')}/sessions to logout"); + return 1; + } + } + + resp.Close(); + return 1; + } + } + } else { + Console.WriteLine("You were not logged in"); + return 0; + } + } +} \ No newline at end of file diff --git a/Tesses.CrossLang.Shell/src/token.tcross b/Tesses.CrossLang.Shell/src/token.tcross index 0702a36..a099196 100644 --- a/Tesses.CrossLang.Shell/src/token.tcross +++ b/Tesses.CrossLang.Shell/src/token.tcross @@ -1,5 +1,8 @@ func Tesses.CrossLang.Shell.Token(dd) { + Console.Write("Name (empty for CrossLang Shell): "); + var name = Console.ReadLine() ?? ""; + if(name == "") name = "CrossLang Shell"; Console.Write("Host: "); var host = Console.ReadLine(); Console.Write("Email: "); @@ -15,6 +18,7 @@ func Tesses.CrossLang.Shell.Token(dd) var accountRequest = { + name, email, password }; diff --git a/vscode-extension/src/api.ts b/vscode-extension/src/api.ts index ebd36f8..9e2b97c 100644 --- a/vscode-extension/src/api.ts +++ b/vscode-extension/src/api.ts @@ -202,6 +202,7 @@ export async function createTemplate() if(project_name) { const dir = join(getDocumentsFolder(),"CrossLangProjects",project_name); + mkdirSync(getDocumentsFolder()); mkdirSync(join(getDocumentsFolder(),"CrossLangProjects")); mkdirSync(dir);