Tesses.CMS Done enough
ci/woodpecker/push/devel Pipeline is pending Details
ci/woodpecker/push/master Pipeline is pending Details

This commit is contained in:
Mike Nolan 2024-10-27 08:03:52 -05:00
parent 1b8a789600
commit ba91f939fc
30 changed files with 630 additions and 69 deletions

2
.gitignore vendored
View File

@ -484,4 +484,4 @@ $RECYCLE.BIN/
*.swp
help.txt
data/
out/
out/

View File

@ -93,6 +93,26 @@ public partial class App : Application
return title;
}
internal static (string loginButtonText,LoggedInUserAccount? login) LoginData()
{
LoggedInUserAccount? login=null;
string loginText="Offline";
Task.Run(async()=>{
try {
Client.Users.LoginToken = Platform.Configuration.LoginToken;
login = await Client.Users.GetLoggedInUserAccountAsync();
loginText = login.LoggedIn ? login.ProperName : "Login";
} catch(Exception ex)
{
_=ex;
}
}).Wait();
return (loginText,login);
}
internal static async Task<IImage> GetMovieThumbnailAsync(string username, string name)
{
//we need to cache the resource
@ -249,6 +269,15 @@ public partial class App : Application
}
}
internal static bool HasDownloaded(UserAccount account,Movie movie)
{
UnixPath dir = Special.Root / "Downloads" / account.Username / "Movies" / movie.Name;
UnixPath filename = dir / movie.ProperName + ".mp4";
var fs = Platform.DownloadFilesystem;
if(fs is null) return false;
return fs.FileExists(filename);
}
internal static void StartDownload(UserAccount account, Movie movie)
{
string username = account.Username;

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS.Avalonia", "Tesses.CMS.Avalonia.csproj", "{36061BA4-CB71-4B6C-8C0A-6482635EB893}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{36061BA4-CB71-4B6C-8C0A-6482635EB893}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36061BA4-CB71-4B6C-8C0A-6482635EB893}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36061BA4-CB71-4B6C-8C0A-6482635EB893}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36061BA4-CB71-4B6C-8C0A-6482635EB893}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B5F29959-050C-43EB-A56F-C7760E27834E}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,40 @@
namespace Tesses.CMS.Avalonia.ViewModels;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Tesses.CMS.Avalonia.Models;
using System.Threading.Tasks;
using ReactiveUI;
using Tesses.CMS.Avalonia.Views.HomePages;
using Tesses.CMS.Avalonia.ViewModels.HomePages;
public partial class AccountViewModel : ViewModelBase
{
MainViewModel m;
public AccountViewModel(MainViewModel m)
{
this.m = m;
this.LogoutCommand = ReactiveCommand.CreateFromTask(LogoutAsync);
}
public IReactiveCommand LogoutCommand {get;init;}
public async Task LogoutAsync()
{
App.Client.Users.LoginToken = App.Platform.Configuration.LoginToken;
await App.Client.Users.LogoutAsync();
(m.LoginText,_) = App.LoginData();
m.CurrentPage = new HomePageViewModel();
}
}

View File

@ -16,6 +16,7 @@ public partial class HomePageViewModel : ViewModelBase
public HomePageViewModel()
{
_curPage = new HomeUserListPageViewModel(this);
}
private ViewModelBase _curPage;
public ViewModelBase CurrentPage

View File

@ -0,0 +1,41 @@
namespace Tesses.CMS.Avalonia.ViewModels.HomePages;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using global::Avalonia.Controls;
using Tesses.CMS.Avalonia.Models;
using Tesses.CMS.Avalonia.Views.HomePages;
using Tesses.CMS.Client;
public partial class HomeAboutMePageViewModel : ViewModelBase, IBackable
{
HomePageViewModel homePage;
HomeUserPageViewModel userPage;
[ObservableProperty]
private string _aboutMe;
[ObservableProperty]
private string _properName;
public HomeAboutMePageViewModel(HomePageViewModel homePage,HomeUserPageViewModel userPage)
{
App.Log("In HomeAboutMePageViewModel::ctor block begin");
this.homePage = homePage;
this.userPage = userPage;
this.AboutMe = userPage.Account.AboutMe;
this.ProperName = $"About {userPage.Account.ProperName}";
App.Log("In HomeAboutMePageViewModel::ctor block end");
}
public ViewModelBase Back()
{
return userPage;
}
}

View File

@ -44,12 +44,17 @@ public partial class HomeMoviePageViewModel : ViewModelBase, IBackable
[ObservableProperty]
private bool _canWatch=false;
[ObservableProperty]
private bool _canDownload=false;
[ObservableProperty]
private string _addOrRemoveFromFavorites="";
[ObservableProperty]
private string _downloadButtonText="Download";
private bool _favoriteExists=false;
public bool FavoriteExists {
get=>_favoriteExists;
@ -66,6 +71,7 @@ public partial class HomeMoviePageViewModel : ViewModelBase, IBackable
public HomeMoviePageViewModel(HomePageViewModel home,HomeMovieListPageViewModel movieList,UserAccount account,MovieItem movie)
{
this.DownloadCommand = ReactiveCommand.CreateFromTask(DownloadAsync);
this.account = account;
username = account.Username;
this.home = home;
@ -89,19 +95,30 @@ public partial class HomeMoviePageViewModel : ViewModelBase, IBackable
Media = JObject.FromObject(movie.Movie)
};
FavoriteExists = App.FavoritesExists(fav);
if(App.HasDownloaded(account,movie.Movie))
{
CanDownload = true;
DownloadButtonText = "Watch Downloaded";
}
Task.Run(async()=>{
var res=await App.Client.Movies.GetMovieContentMetadataAsync(username,movie.Movie.Name);
if(res.HasBrowserStream)
watchUrl = res.BrowserStream;
CanWatch = res.HasBrowserStream;
CanDownload = res.HasDownloadStream;
if(!App.HasDownloaded(account,movie.Movie))
{
CanDownload = res.HasDownloadStream;
}
}).Wait(0);
}
public ViewModelBase Back()
{
@ -116,11 +133,18 @@ public partial class HomeMoviePageViewModel : ViewModelBase, IBackable
home.CurrentPage = new HomeMovieVideoPlayerViewModel(home,this,watchUrl);
}
}
[RelayCommand]
public void Download()
public IReactiveCommand DownloadCommand {get;init;}
public async Task DownloadAsync()
{
if(CanDownload)
App.StartDownload(account,movie);
if(App.HasDownloaded(account,movie))
{
await this.home.PlayDownloadedAsync(fav);
}
else
{
if(CanDownload)
App.StartDownload(account,movie);
}
}
[RelayCommand]

View File

@ -41,6 +41,7 @@ public partial class HomeUserPageViewModel : ViewModelBase, IBackable
this.account = account;
UserItems.Add(new UserPageItem("Movies",new HomeMovieListPageViewModel(homePage,this)));
UserItems.Add(new UserPageItem("Shows",new HomeShowListPageViewModel(homePage,this)));
UserItems.Add(new UserPageItem("About",new HomeAboutMePageViewModel(homePage,this)));
}
[ObservableProperty]
private UserPageItem? _selectedListItem;

View File

@ -0,0 +1,53 @@
namespace Tesses.CMS.Avalonia.ViewModels;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Tesses.CMS.Avalonia.Models;
using System.Threading.Tasks;
using ReactiveUI;
using Tesses.CMS.Avalonia.Views.HomePages;
using Tesses.CMS.Avalonia.ViewModels.HomePages;
public partial class LoginViewModel : ViewModelBase
{
[ObservableProperty]
private string _email="";
[ObservableProperty]
private string _password="";
[ObservableProperty]
private bool _failed=false;
MainViewModel m;
public LoginViewModel(MainViewModel m)
{
this.m = m;
this.LoginCommand = ReactiveCommand.CreateFromTask(LoginAsync);
}
public IReactiveCommand LoginCommand {get;init;}
public async Task LoginAsync()
{
Failed=false;
var tkn= await App.Client.Users.CreateTokenAsync(Email,Password);
if(tkn.Success)
{
App.Platform.Configuration.LoginToken = tkn.Cookie;
await App.Platform.WriteConfigurationAsync();
(m.LoginText,_) = App.LoginData();
m.CurrentPage = new HomePageViewModel();
}
else
{
Failed=true;
}
}
}

View File

@ -7,6 +7,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Tesses.CMS.Avalonia.Models;
using Tesses.CMS.Avalonia.Views;
using Tesses.CMS.Client;
public partial class MainViewModel : ViewModelBase
{
@ -21,6 +23,8 @@ public partial class MainViewModel : ViewModelBase
SelectedListItem = Pages.First();
this.Title = title;
(LoginText,_)= App.LoginData();
if(Environment.GetCommandLineArgs().Length > 1)
{
string path = Environment.GetCommandLineArgs()[1];
@ -55,7 +59,7 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty]
private Page? _selectedListItem;
[ObservableProperty]
private string _loginText="Login";
private string _loginText="";
[ObservableProperty]
private string _title;
@ -81,6 +85,20 @@ public partial class MainViewModel : ViewModelBase
private void LoginAccount()
{
App.Log("Login button");
LoggedInUserAccount? data=null;
(LoginText,data)= App.LoginData();
if(data is not null)
{
if(data.LoggedIn)
{
CurrentPage = new AccountViewModel(this);
}
else
{
CurrentPage = new LoginViewModel(this);
}
}
}
internal void SetHome()

View File

@ -0,0 +1,18 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Tesses.CMS.Avalonia.ViewModels"
xmlns:v="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.AccountView"
x:DataType="vm:AccountViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:AccountViewModel />
</Design.DataContext>
<StackPanel Margin="20">
<Button Command="{Binding LogoutCommand}">Logout</Button>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Tesses.CMS.Avalonia.Views;
public partial class AccountView : UserControl
{
public AccountView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,23 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Tesses.CMS.Avalonia.ViewModels.HomePages"
xmlns:v="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.HomePages.HomeAboutMePageView"
x:DataType="vm:HomeAboutMePageViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:HomeAboutMePageViewModel />
</Design.DataContext>
<ScrollViewer>
<Grid RowDefinitions="Auto,*" Margin="20">
<TextBlock Text="{Binding ProperName}" Grid.Row="0" />
<v:InlineText Margin="0 20 0 0" Grid.Row="1" Text="{Binding AboutMe}" />
</Grid>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Tesses.CMS.Avalonia.Views.HomePages;
public partial class HomeAboutMePageView : UserControl
{
public HomeAboutMePageView()
{
InitializeComponent();
}
}

View File

@ -25,7 +25,7 @@
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,Auto">
<Button Grid.Column="0" Command="{Binding WatchCommand}" Content="Watch" IsVisible="{Binding CanWatch}"/>
<Button Grid.Column="1" Command="{Binding DownloadCommand}" Content="Download" IsVisible="{Binding CanDownload}"/>
<Button Grid.Column="1" Command="{Binding DownloadCommand}" Content="{Binding DownloadButtonText}" IsVisible="{Binding CanDownload}"/>
<Button Grid.Column="2" Command="{Binding AddToFavoritesCommand}" Content="{Binding AddOrRemoveFromFavorites}"/>
</Grid>

View File

@ -49,7 +49,9 @@ public class InlineText : WrapPanel
{
if(b.Length > 0)
{
this.Children.Add(new TextBlock(){Text = b.ToString()});
var res = new TextBlock(){Text = $"\u200B{b.ToString()}\u200B",TextWrapping = TextWrapping.Wrap};
this.Children.Add(res);
b.Clear();
}
StringBuilder b2 = new StringBuilder();
@ -62,7 +64,7 @@ public class InlineText : WrapPanel
}
b2.Append(text[i]);
}
var tb = new TextBlock(){Text = b2.ToString()};
var tb = new TextBlock(){Text = $"\u200B{b2.ToString()}\u200B",TextWrapping = TextWrapping.Wrap};
tb.Foreground = new SolidColorBrush(Color.FromRgb(0,0,255));
tb.TextDecorations = TextDecorations.Underline;
@ -80,7 +82,7 @@ public class InlineText : WrapPanel
}
if(b.Length > 0)
{
this.Children.Add(new TextBlock(){Text = b.ToString()});
this.Children.Add(new TextBlock(){Text = $"\u200B{b.ToString()}\u200B", TextWrapping = TextWrapping.Wrap});
b.Clear();
}
}

View File

@ -0,0 +1,23 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Tesses.CMS.Avalonia.ViewModels"
xmlns:v="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.LoginView"
x:DataType="vm:LoginViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:LoginViewModel />
</Design.DataContext>
<StackPanel Margin="20">
<TextBlock Background="Red" IsVisible="{Binding Failed}">The Email or Password was incorrect, please try again.</TextBlock>
<TextBlock Margin="0 5" >Email:</TextBlock>
<TextBox Text="{Binding Email}" Watermark="Enter your email"/>
<TextBlock Margin="0 5" >Password:</TextBlock>
<TextBox Text="{Binding Password}" PasswordChar="*" Watermark="Enter your password"/>
<Button Command="{Binding LoginCommand}">Login</Button>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Tesses.CMS.Avalonia.Views;
public partial class LoginView : UserControl
{
public LoginView()
{
InitializeComponent();
}
}

View File

@ -256,7 +256,12 @@ namespace Tesses.CMS.Client
public async Task LogoutAsync()
{
(await client.client.GetAsync("/logout")).Dispose();
(await client.client.GetAsync($"{client.rooturl}/logout")).Dispose();
}
public async Task<LoggedInUserAccount> GetLoggedInUserAccountAsync()
{
return JsonConvert.DeserializeObject<LoggedInUserAccount>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/LoggedInUserAccount"));
}
@ -280,11 +285,10 @@ namespace Tesses.CMS.Client
return "";
}
set{
if(string.IsNullOrWhiteSpace(value))
if(client.client.DefaultRequestHeaders.Contains("Authorization"))
if(client.client.DefaultRequestHeaders.Contains("Authorization"))
client.client.DefaultRequestHeaders.Remove("Authorization");
else
client.client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",value);
if(!string.IsNullOrWhiteSpace(value))
client.client.DefaultRequestHeaders.Add("Authorization",$"Bearer {value}");
}
}
@ -297,6 +301,26 @@ namespace Tesses.CMS.Client
}
}
public class LoggedInUserAccount
{
[JsonProperty("logged_in")]
public bool LoggedIn {get;set;}=false;
[JsonProperty("username")]
public string UserName {get;set;}="";
[JsonProperty("proper_name")]
public string ProperName {get;set;}="";
[JsonProperty("is_admin")]
public bool IsAdmin {get;set;}=false;
[JsonProperty("is_invited")]
public bool IsInvited {get;set;}=false;
[JsonProperty("is_verified")]
public bool IsVerified {get;set;}=false;
}
public class LoginToken
{
[JsonProperty("success")]

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS.Client", "Tesses.CMS.Client.csproj", "{887B0982-1D90-413D-BFEE-72065D115DE0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{887B0982-1D90-413D-BFEE-72065D115DE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{887B0982-1D90-413D-BFEE-72065D115DE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{887B0982-1D90-413D-BFEE-72065D115DE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{887B0982-1D90-413D-BFEE-72065D115DE0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {17F92768-0843-4A4F-B369-22B8120B4996}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS.Providers.LiteDb", "Tesses.CMS.Providers.LiteDb.csproj", "{D0D6C7FF-2B46-4909-9741-77DBDFD52D4D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D0D6C7FF-2B46-4909-9741-77DBDFD52D4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0D6C7FF-2B46-4909-9741-77DBDFD52D4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0D6C7FF-2B46-4909-9741-77DBDFD52D4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0D6C7FF-2B46-4909-9741-77DBDFD52D4D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {45074933-DBC8-4448-B846-2287DFF81409}
EndGlobalSection
EndGlobal

View File

@ -1,5 +1,5 @@
<h1>Change movie metadata</h1>
<form action="./edit" method="post" enctype="application/x-www-form-urlencoded">
<h1>Change episode metadata</h1>
<form action="./edit?csrf={{csrf}}" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
@ -12,7 +12,7 @@
</form>
<h1>Upload episode files</h1>
<form action="./upload" method="post" enctype="multipart/form-data">
<form action="./upload?csrf={{csrf2}}" method="post" enctype="multipart/form-data">
<h1>You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)</h1>
<select class="form-select" name="type" aria-label="Select resource">
<option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the episode)</option>

View File

@ -1,5 +1,5 @@
<h1>Change season metadata</h1>
<form action="./edit" method="post" enctype="application/x-www-form-urlencoded">
<form action="./edit?csrf={{csrf}}" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
@ -13,7 +13,7 @@
</form>
<br>
<h1>Add episode</h1>
<form action="./addepisode" method="post" enctype="application/x-www-form-urlencoded">
<form action="./addepisode?csrf={{csrf2}}" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="name" class="form-label">Episode Name</label>
<input type="text" class="form-control" id="name" name="name" aria-describedby="message">
@ -38,7 +38,7 @@
</form>
<br>
<h1>Upload asset files</h1>
<form action="./upload" method="post" enctype="multipart/form-data">
<form action="./upload?csrf={{csrf3}}" method="post" enctype="multipart/form-data">
<h1>You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)</h1>
<select class="form-select" name="type" aria-label="Select resource">
<option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the season (also used for when episode does not have one))</option>

View File

@ -1,5 +1,5 @@
<h1>Change show metadata</h1>
<form action="./edit" method="post" enctype="application/x-www-form-urlencoded">
<form action="./edit?csrf={{csrf}}" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
@ -13,7 +13,7 @@
</form>
<br>
<h1>Add season</h1>
<form action="./addseason" method="post" enctype="application/x-www-form-urlencoded">
<form action="./addseason?csrf={{csrf2}}" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="number" class="form-label">Season Number</label>
<input type="number" min="1" class="form-control" id="number" name="number" value="{{newseasonnumber}}">
@ -32,7 +32,7 @@
</form>
<br>
<h1>Upload asset files</h1>
<form action="./upload" method="post" enctype="multipart/form-data">
<form action="./upload?csrf={{csrf3}}" method="post" enctype="multipart/form-data">
<h1>You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)</h1>
<select class="form-select" name="type" aria-label="Select resource">
<option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the show (also used for when season or episode does not have one))</option>

View File

@ -26,6 +26,7 @@
const end = document.getElementById('end');
const text = document.getElementById('text');
const _captions = document.getElementById('captions');
var csrf = "{{csrf}}";
function set_max()
{
begin.max = video_player.duration;
@ -50,7 +51,7 @@
_delete.classList.add('btn');
_delete.classList.add('btn-danger');
_delete.onclick = ()=>{
var index=captions.indexof(item);
var index=captions.indexOf(item);
if(index > -1)
{
captions.splice(index,1);
@ -116,20 +117,25 @@
addEventListener("DOMContentLoaded", (event) => {loaded();});
function save()
{
fetch("./subtitles?lang={{lang}}",{method: 'POST',
fetch(`./subtitles?lang={{lang}}&csrf=${csrf}`,{method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},body: JSON.stringify(captions)/*https://stackoverflow.com/a/29823632*/}).then(e=>{
if(e.ok)
{
alert("Saved");
return e.json();
}
else
{
alert("Failed");
return Promise.reject();
}
e.close();
}).catch(e=>{
alert("Failed to save.");
}).then(e=>{
csrf=e.csrf;
alert("Success.");
});
}
</script>

View File

@ -932,17 +932,33 @@ namespace Tesses.CMS
{
foreach (var c in cookies)
{
var co = c.Split(new char[] { '=' }, 2);
if (co.Length == 2 && co[0] == "Session")
foreach(var cE in c.Split(new string[]{"; "},StringSplitOptions.RemoveEmptyEntries))
{
if (provider.ContainsSession(co[1]))
var co = cE.Split(new char[] { '=' }, 2);
if (co.Length == 2 && co[0] == "Session")
{
provider.DeleteSession(co[1]);
return;
if (provider.ContainsSession(co[1]))
{
provider.DeleteSession(co[1]);
return;
}
}
}
}
}
else if (ctx.RequestHeaders.TryGetFirst("Authorization", out var auth))
{
var co = auth.Split(new char[] { ' ' }, 2);
if (co.Length == 2 && co[0] == "Bearer")
{
if (provider.ContainsSession(co[1]))
{
provider.DeleteSession(co[1]);
return;
}
}
}
}
HttpClient client = new HttpClient();
@ -1141,35 +1157,40 @@ namespace Tesses.CMS
cookie = "";
if (ctx.RequestHeaders.TryGetValue("Cookie", out var cookies))
{
foreach (var c in cookies)
{
var co = c.Split(new char[] { '=' }, 2);
if (co.Length == 2 && co[0] == "Session")
foreach(var cE in c.Split(new string[]{"; "},StringSplitOptions.RemoveEmptyEntries))
{
cookie = co[1];
long? account = provider.GetSession(cookie);
if (account.HasValue)
var co = cE.Split(new char[] { '=' }, 2);
if (co.Length == 2 && co[0] == "Session")
{
if (requiresCSRF)
cookie = co[1];
long? account = provider.GetSession(cookie);
if (account.HasValue)
{
if (ctx.QueryParams.TryGetFirst("csrf", out var csrf))
if (requiresCSRF)
{
if (IsValidCSRFAndDestroy(account.Value, cookie, csrf))
if (ctx.QueryParams.TryGetFirst("csrf", out var csrf))
{
return provider.GetUserById(account.Value);
if (IsValidCSRFAndDestroy(account.Value, cookie, csrf))
{
return provider.GetUserById(account.Value);
}
}
throw new InvalidCSRFException();
}
throw new InvalidCSRFException();
return provider.GetUserById(account.Value);
}
return provider.GetUserById(account.Value);
}
}
}
}
else if (ctx.RequestHeaders.TryGetFirst("Authentication", out var auth))
else if (ctx.RequestHeaders.TryGetFirst("Authorization", out var auth))
{
var co = auth.Split(new char[] { ' ' }, 2);
if (co.Length == 2 && co[0] == "Bearer")
@ -2094,7 +2115,7 @@ namespace Tesses.CMS
string user = usersPathValueServer.GetValue(ctx);
string show = showPathValueServer.GetValue(ctx);
var me = GetAccount(ctx);
var me = GetAccount(ctx,out var cookie);
string seasonS = seasonPathValueServer.GetValue(ctx);
string episodeS = episodePathValueServer.GetValue(ctx);
if (!int.TryParse(seasonS, out var season))
@ -2110,7 +2131,17 @@ namespace Tesses.CMS
if (me != null)
{
if (_episode != null)
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditEpisodeDetails.RenderAsync(new { Propername = System.Web.HttpUtility.HtmlAttributeEncode(_episode.ProperName), Description = System.Web.HttpUtility.HtmlEncode(_episode.Description) })));
{
string csrf="";
string csrf2="";
if(me != null)
{
csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie));
csrf2 = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie));
}
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditEpisodeDetails.RenderAsync(new { Propername = System.Web.HttpUtility.HtmlAttributeEncode(_episode.ProperName), Description = System.Web.HttpUtility.HtmlEncode(_episode.Description), csrf, csrf2 })));
}
}
else
{
@ -2166,7 +2197,7 @@ namespace Tesses.CMS
int season = 1;
if (!int.TryParse(seasonS, out season))
season = 1;
var me = GetAccount(ctx);
var me = GetAccount(ctx,out var cookie);
var _season = provider.GetSeason(user, show, season);
@ -2178,7 +2209,18 @@ namespace Tesses.CMS
if (me != null)
{
if (_season != null)
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditSeasonDetails.RenderAsync(new { Propername = System.Web.HttpUtility.HtmlAttributeEncode(_season.ProperName), newseasonnumber = provider.EpisodeCount(user, show, season) + 1, Description = System.Web.HttpUtility.HtmlEncode(_season.Description) })));
{
string csrf="";
string csrf2="";
string csrf3="";
if(me != null)
{
csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie));
csrf2 = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie));
csrf3 = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie));
}
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditSeasonDetails.RenderAsync(new { Propername = System.Web.HttpUtility.HtmlAttributeEncode(_season.ProperName), newseasonnumber = provider.EpisodeCount(user, show, season) + 1, Description = System.Web.HttpUtility.HtmlEncode(_season.Description), csrf,csrf2,csrf3 })));
}
}
else
{
@ -2315,7 +2357,7 @@ namespace Tesses.CMS
{
string user = usersPathValueServer.GetValue(ctx);
string show = showPathValueServer.GetValue(ctx);
var me = GetAccount(ctx);
var me = GetAccount(ctx,out var cookie);
var _show = provider.GetShow(user, show);
if (me != null && me.Username != user && !me.IsAdmin)
@ -2326,7 +2368,18 @@ namespace Tesses.CMS
if (me != null)
{
if (_show != null)
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditShowDetails.RenderAsync(new { Propername = System.Web.HttpUtility.HtmlAttributeEncode(_show.ProperName), newseasonnumber = provider.SeasonCount(user, show) + 1, Description = System.Web.HttpUtility.HtmlEncode(_show.Description) })));
{
string csrf="";
string csrf2="";
string csrf3="";
if(me != null)
{
csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie));
csrf2 = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie));
csrf3 = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie));
}
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditShowDetails.RenderAsync(new { Propername = System.Web.HttpUtility.HtmlAttributeEncode(_show.ProperName), newseasonnumber = provider.SeasonCount(user, show) + 1, Description = System.Web.HttpUtility.HtmlEncode(_show.Description) ,csrf,csrf2,csrf3 })));
}
}
else
{
@ -2616,7 +2669,7 @@ namespace Tesses.CMS
{
string user = usersPathValueServer.GetValue(ctx);
string movie = moviePathValueServer.GetValue(ctx);
var me = GetAccount(ctx,true);
var me = GetAccount(ctx,out var cookie,true);
var _movie = provider.GetMovie(user, movie);
if (me != null && me.Username != user && !me.IsAdmin)
{
@ -2642,12 +2695,13 @@ namespace Tesses.CMS
{
Subtitle.ToSrt(srtFile, json);
}
await ctx.SendTextAsync("Success");
var csrf=HttpUtility.UrlEncode(this.CreateCSRF(me.Id, cookie));
await ctx.SendJsonAsync(new{success=true,csrf});
return;
}
}
ctx.StatusCode = 400;
await ctx.SendTextAsync("Fail");
await ctx.SendJsonAsync(new{success=false});
}
private async Task SubtitlesEpisodeAsync(ServerContext ctx)
{
@ -2661,7 +2715,7 @@ namespace Tesses.CMS
if (!int.TryParse(episodeS, out var episode))
episode = 1;
var me = GetAccount(ctx);
var me = GetAccount(ctx,out var cookie);
var _show = provider.GetMovie(user, show);
var _episode = provider.GetEpisode(user, show, season, episode);
if (me != null && me.Username != user && !me.IsAdmin)
@ -2675,6 +2729,10 @@ namespace Tesses.CMS
string langDir = Path.Combine(path, user, "show", show, $"Season {season.ToString("D2")}", $"{_episode.EpisodeName} S{season.ToString("D2")}E{episode.ToString("D2")}-subtitles", lang);
string langFile = Path.Combine(langDir, $"{_episode.ProperName} S{season.ToString("D2")}E{episode.ToString("D2")}.json");
string browserfile = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/S{season.ToString("D2")}E{episode.ToString("D2")}.mp4";
string csrf="";
csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie));
string json = "";
bool hasjson = false;
if (File.Exists(langFile))
@ -2682,7 +2740,7 @@ namespace Tesses.CMS
hasjson = true;
json = File.ReadAllText(langFile);
}
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageSubtitleEditor.RenderAsync(new { hasjson, json, lang, browserfile })));
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageSubtitleEditor.RenderAsync(new { hasjson, json, lang, browserfile, csrf })));
}
else
{
@ -2749,7 +2807,7 @@ namespace Tesses.CMS
{
string user = usersPathValueServer.GetValue(ctx);
string movie = moviePathValueServer.GetValue(ctx);
var me = GetAccount(ctx);
var me = GetAccount(ctx,out var cookie);
var _movie = provider.GetMovie(user, movie);
if (me != null && me.Username != user && !me.IsAdmin)
{
@ -2769,7 +2827,8 @@ namespace Tesses.CMS
hasjson = true;
json = File.ReadAllText(langFile);
}
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageSubtitleEditor.RenderAsync(new { hasjson, json, lang, browserfile })));
var csrf=HttpUtility.UrlEncode(this.CreateCSRF(me.Id, cookie));
await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageSubtitleEditor.RenderAsync(new { hasjson, json, lang, browserfile,csrf })));
}
else
{
@ -3993,7 +4052,7 @@ namespace Tesses.CMS
{
var user = provider.GetUserById(show.UserId);
int count2 = provider.EpisodeCount(user.Username, show.Name, season);
for (int j = 1; j < count2; j++)
for (int j = 1; j <= count2; j++)
{
yield return provider.GetEpisode(user.Username, show.Name, season, j);
}
@ -4172,6 +4231,7 @@ namespace Tesses.CMS
});
},new SwagmeDocumentation("Branding for server"));
swagmeServer.Add("/Login", ApiLogin, new SwagmeDocumentation("Login to account", "<b>email</b>: the email of account</br><b>password</b>: the password of account</br><b>type</b>: json or cookie"), "POST", "Users");
swagmeServer.Add("/LoggedInUserAccount",LoggedInAccountAsync,new SwagmeDocumentation("Get logged in account via authorization"),"GET","Users");
swagmeServer.Add("/GetPublicUsers", ApiGetPublicUsers, new SwagmeDocumentation("Get all public users"), "GET", "Users");
swagmeServer.Add("/Updates", (ctx) =>
{
@ -4204,7 +4264,20 @@ namespace Tesses.CMS
swagmeServer.Add("/GetAlbums", ApiGetAlbumsAsync, new SwagmeDocumentation("Get a list of albums", "<b>user</b>: the user of the albums<br><b>type</b>: format of list (defaults to json): json or rss"), "GET", "Albums");
return swagmeServer;
}
private async Task LoggedInAccountAsync(ServerContext ctx)
{
var account = this.GetAccount(ctx);
if(account is null)
{
await ctx.SendJsonAsync(new LoggedInUserAccount{LoggedIn=false});
}
else
{
await ctx.SendJsonAsync(new LoggedInUserAccount{LoggedIn=true,UserName = account.Username, ProperName = account.ProperName, IsAdmin = account.IsAdmin, IsInvited = account.IsInvited, IsVerified=account.IsVerified});
}
}
private async Task ApiLogin(ServerContext ctx)
{
ctx.ParseBody();
@ -4381,7 +4454,7 @@ namespace Tesses.CMS
if (type == "json")
{
List<Season> seasons = new List<Season>();
for (int i = 1; i < provider.SeasonCount(s.UserId, s.Id); i++)
for (int i = 1; i <= provider.SeasonCount(s.UserId, s.Id); i++)
{
var season = provider.GetSeason(s.UserId, s.Id, i);
if (season != null)
@ -4471,7 +4544,7 @@ namespace Tesses.CMS
if (type == "json")
{
List<Episode> episodes = new List<Episode>();
for (int i = 1; i < provider.EpisodeCount(s.UserId, s.Id, season); i++)
for (int i = 1; i <= provider.EpisodeCount(s.UserId, s.Id, season); i++)
{
var episode = provider.GetEpisode(s.UserId, s.Id, season, i);
if (episode != null)

View File

@ -37,7 +37,7 @@ namespace Tesses.CMS
Season = SeasonNumber,
Episode = EpisodeNumber,
Description = System.Web.HttpUtility.HtmlEncode(Description),
Thumbnail = File.Exists(Path.Combine(dir,user,"show",show,$"Season {SeasonNumber.ToString("D2")}",$"{EpisodeName} S{SeasonNumber.ToString("D2")}E{SeasonNumber.ToString("D2")}-thumbnail.jpg")) ? $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{SeasonNumber.ToString("D2")}/{EpisodeName}%20S{SeasonNumber.ToString("D2")}E{SeasonNumber.ToString("D2")}-thumbnail.jpg" : File.Exists(Path.Combine(dir,user,"show",show,$"Season {SeasonNumber.ToString("D2")}","thumbnail.jpg")) ? $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{SeasonNumber.ToString("D2")}/thumbnail.jpg" : $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg"
Thumbnail = File.Exists(Path.Combine(dir,user,"show",show,$"Season {SeasonNumber.ToString("D2")}",$"{EpisodeName} S{SeasonNumber.ToString("D2")}E{EpisodeNumber.ToString("D2")}-thumbnail.jpg")) ? $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{SeasonNumber.ToString("D2")}/{EpisodeName}%20S{SeasonNumber.ToString("D2")}E{EpisodeNumber.ToString("D2")}-thumbnail.jpg" : File.Exists(Path.Combine(dir,user,"show",show,$"Season {SeasonNumber.ToString("D2")}","thumbnail.jpg")) ? $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{SeasonNumber.ToString("D2")}/thumbnail.jpg" : $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg"
};
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Tesses.CMS
{
public class LoggedInUserAccount
{
[JsonProperty("logged_in")]
public bool LoggedIn {get;set;}=false;
[JsonProperty("username")]
public string UserName {get;set;}="";
[JsonProperty("proper_name")]
public string ProperName {get;set;}="";
[JsonProperty("is_admin")]
public bool IsAdmin {get;set;}=false;
[JsonProperty("is_invited")]
public bool IsInvited {get;set;}=false;
[JsonProperty("is_verified")]
public bool IsVerified {get;set;}=false;
}
}

25
Tesses.CMS/Tesses.CMS.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS", "Tesses.CMS.csproj", "{2AB7647E-947B-43FD-8348-4F43B13DD9AA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2AB7647E-947B-43FD-8348-4F43B13DD9AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2AB7647E-947B-43FD-8348-4F43B13DD9AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2AB7647E-947B-43FD-8348-4F43B13DD9AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2AB7647E-947B-43FD-8348-4F43B13DD9AA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C4517F8F-92D8-4A81-8081-C8683339096F}
EndGlobalSection
EndGlobal

14
sample_config.json Normal file
View File

@ -0,0 +1,14 @@
{
"Title": "TessesStudios",
"Urls": [{"Url":"https://tesses.net", "Text":"Tesses"}, {"Url": "https://jellyfin.site.tesses.net/", "Text": "Jellyfin (use guest)"}],
"Root": "CHANGE_TO_DOMAIN_NAME",
"Email":{
"Host": "CHANGE_TO_EMAIL_SMTP_SERVER",
"User": "CHANGE_TO_EMAIL_USERNAME",
"Pass": "CHNAGE_TO_EMAIL_PASS",
"Port": 587,
"Encryption": "StartTls",
"Email": "CHANGE_TO_EMAIL"
},
"Publish": "RequireInvite"
}