feat(admin): added backend for admin pages
This commit is contained in:
parent
54cfc05b88
commit
56ea7fb7f0
26 changed files with 662 additions and 42 deletions
|
|
@ -15,7 +15,7 @@ public class AuthController : ControllerBase
|
||||||
_service = service;
|
_service = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost("login")]
|
||||||
public async Task<IActionResult> Authenticate([FromForm] string username, [FromForm] string password)
|
public async Task<IActionResult> Authenticate([FromForm] string username, [FromForm] string password)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -29,4 +29,123 @@ public class AuthController : ControllerBase
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetLogins()
|
||||||
|
{
|
||||||
|
if (!await IsAdminAuthenticated())
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
var logins = await _service.GetLogins();
|
||||||
|
|
||||||
|
return Ok(logins);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{username}")]
|
||||||
|
public async Task<IActionResult> GetLogin([FromRoute] string username)
|
||||||
|
{
|
||||||
|
if (!await IsAdminAuthenticated())
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var login = await _service.GetLogin(username);
|
||||||
|
|
||||||
|
return Ok(login);
|
||||||
|
}
|
||||||
|
catch (AuthNotFoundException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateLogin([FromForm] string username, [FromForm] string password)
|
||||||
|
{
|
||||||
|
if (!await IsAdminAuthenticated())
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
var newLogin = await _service.CreateLogin(username, password);
|
||||||
|
|
||||||
|
return Ok(newLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
public async Task<IActionResult> UpdateLogin([FromForm] string username, [FromForm] string password, [FromForm] string newPassword)
|
||||||
|
{
|
||||||
|
if (!await IsAdminAuthenticated())
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _service.UpdateLoginOwnPassword(username, newPassword, password);
|
||||||
|
}
|
||||||
|
catch (AuthNotFoundException)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
catch (LoginFailedException) //the old password is wrong
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
public async Task<IActionResult> DeleteLogin([FromForm] string username)
|
||||||
|
{
|
||||||
|
if (!await IsAdminAuthenticated())
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deletedLogin = await _service.DeleteLogin(username);
|
||||||
|
|
||||||
|
return Ok(deletedLogin);
|
||||||
|
}
|
||||||
|
catch (AuthNotFoundException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetSessionToken()
|
||||||
|
{
|
||||||
|
return Request.Cookies["session"];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsAuthenticated()
|
||||||
|
{
|
||||||
|
var sessionToken = GetSessionToken();
|
||||||
|
|
||||||
|
if (await _service.IsAuthenticated(sessionToken))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsAdminAuthenticated()
|
||||||
|
{
|
||||||
|
var sessionToken = GetSessionToken();
|
||||||
|
|
||||||
|
if (await _service.IsAuthenticated(sessionToken) && await _service.IsAdmin(sessionToken))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using JellyGlass.Exceptions;
|
||||||
using JellyGlass.Services;
|
using JellyGlass.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
|
@ -30,8 +31,15 @@ public class SearchController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
var decodedSearchTerm = HttpUtility.UrlDecode(searchTerm);
|
var decodedSearchTerm = HttpUtility.UrlDecode(searchTerm);
|
||||||
var results = await _service.Search(decodedSearchTerm, serverId);
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = await _service.Search(decodedSearchTerm, serverId);
|
||||||
return Ok(results);
|
return Ok(results);
|
||||||
}
|
}
|
||||||
|
catch (ClientNotFoundException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using JellyGlass.Exceptions;
|
||||||
using JellyGlass.Services;
|
using JellyGlass.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
|
@ -18,12 +19,10 @@ public class ServersController : ControllerBase
|
||||||
_authService = authService;
|
_authService = authService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet("all")]
|
||||||
public async Task<IActionResult> getServers()
|
public async Task<IActionResult> GetServers()
|
||||||
{
|
{
|
||||||
var sessionToken = Request.Cookies["session"];
|
if (!await IsAuthenticated())
|
||||||
|
|
||||||
if (!await _authService.IsAuthenticated(sessionToken))
|
|
||||||
{
|
{
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
@ -32,4 +31,98 @@ public class ServersController : ControllerBase
|
||||||
|
|
||||||
return Ok(servers);
|
return Ok(servers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetServer([FromQuery] int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var server = await _serverService.GetServerByID(id);
|
||||||
|
return Ok(server);
|
||||||
|
}
|
||||||
|
catch (ServerNotFoundException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
public async Task<IActionResult> UpdateServer([FromBody] int id, string owner, string url, string apiToken)
|
||||||
|
{
|
||||||
|
if (!await IsAdminAuthenticated())
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var updatedServer = await _serverService.UpdateServer(id, owner, url, apiToken);
|
||||||
|
return Ok(updatedServer);
|
||||||
|
}
|
||||||
|
catch (ServerNotFoundException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> AddServer([FromBody] string owner, string url, string apiToken)
|
||||||
|
{
|
||||||
|
if (!await IsAdminAuthenticated())
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
var newServer = await _serverService.CreateServer(owner, url, apiToken);
|
||||||
|
|
||||||
|
return Ok(newServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
public async Task<IActionResult> DeleteServer([FromBody] int id)
|
||||||
|
{
|
||||||
|
if (!await IsAdminAuthenticated())
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deletedServer = await _serverService.DeleteServer(id);
|
||||||
|
return Ok(deletedServer);
|
||||||
|
}
|
||||||
|
catch (ServerNotFoundException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetSessionToken()
|
||||||
|
{
|
||||||
|
return Request.Cookies["session"];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsAuthenticated()
|
||||||
|
{
|
||||||
|
var sessionToken = GetSessionToken();
|
||||||
|
|
||||||
|
if (await _authService.IsAuthenticated(sessionToken))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsAdminAuthenticated()
|
||||||
|
{
|
||||||
|
var sessionToken = GetSessionToken();
|
||||||
|
|
||||||
|
if (await _authService.IsAuthenticated(sessionToken) && await _authService.IsAdmin(sessionToken))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,10 +7,16 @@ public class AuthRepositoryException : System.Exception
|
||||||
public AuthRepositoryException(string message, System.Exception inner) : base(message, inner) { }
|
public AuthRepositoryException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
[System.Serializable]
|
|
||||||
public class AuthNotFoundException : AuthRepositoryException
|
public class AuthNotFoundException : AuthRepositoryException
|
||||||
{
|
{
|
||||||
public AuthNotFoundException() { }
|
public AuthNotFoundException() { }
|
||||||
public AuthNotFoundException(string message) : base(message) { }
|
public AuthNotFoundException(string message) : base(message) { }
|
||||||
public AuthNotFoundException(string message, System.Exception inner) : base(message, inner) { }
|
public AuthNotFoundException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class AuthAlreadyExistsException : AuthRepositoryException
|
||||||
|
{
|
||||||
|
public AuthAlreadyExistsException() { }
|
||||||
|
public AuthAlreadyExistsException(string message) : base(message) { }
|
||||||
|
public AuthAlreadyExistsException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
16
backend/src/Exceptions/ClientServiceExceptions.cs
Normal file
16
backend/src/Exceptions/ClientServiceExceptions.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace JellyGlass.Exceptions;
|
||||||
|
|
||||||
|
public class ClientServiceException : System.Exception
|
||||||
|
{
|
||||||
|
public ClientServiceException() { }
|
||||||
|
public ClientServiceException(string message) : base(message) { }
|
||||||
|
public ClientServiceException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
public class ClientNotFoundException : ClientServiceException
|
||||||
|
{
|
||||||
|
public ClientNotFoundException() { }
|
||||||
|
public ClientNotFoundException(string message) : base(message) { }
|
||||||
|
public ClientNotFoundException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
15
backend/src/Exceptions/ServerRepositoryExceptions.cs
Normal file
15
backend/src/Exceptions/ServerRepositoryExceptions.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace JellyGlass.Exceptions;
|
||||||
|
|
||||||
|
public class ServerRepositoryException : Exception
|
||||||
|
{
|
||||||
|
public ServerRepositoryException() { }
|
||||||
|
public ServerRepositoryException(string message) : base(message) { }
|
||||||
|
public ServerRepositoryException(string message, Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ServerNotFoundException : ServerRepositoryException
|
||||||
|
{
|
||||||
|
public ServerNotFoundException() { }
|
||||||
|
public ServerNotFoundException(string message) : base(message) { }
|
||||||
|
public ServerNotFoundException(string message, Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
92
backend/src/Migrations/20260305170928_admin.Designer.cs
generated
Normal file
92
backend/src/Migrations/20260305170928_admin.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using JellyGlass.Repositories;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace JellyGlassBackend.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DatabaseContext))]
|
||||||
|
[Migration("20260305170928_admin")]
|
||||||
|
partial class admin
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("JellyGlass.Models.Server", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ApiToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Owner")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Servers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("JellyGlass.Models.UserLogin", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAdmin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Username");
|
||||||
|
|
||||||
|
b.ToTable("Logins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("JellyGlass.Models.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SessionToken")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LoginUsername")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("SessionToken");
|
||||||
|
|
||||||
|
b.HasIndex("LoginUsername");
|
||||||
|
|
||||||
|
b.ToTable("Sessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("JellyGlass.Models.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("JellyGlass.Models.UserLogin", "Login")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("LoginUsername");
|
||||||
|
|
||||||
|
b.Navigation("Login");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
backend/src/Migrations/20260305170928_admin.cs
Normal file
47
backend/src/Migrations/20260305170928_admin.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace JellyGlassBackend.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class admin : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "Id",
|
||||||
|
table: "Servers",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT")
|
||||||
|
.Annotation("Sqlite:Autoincrement", true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsAdmin",
|
||||||
|
table: "Logins",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsAdmin",
|
||||||
|
table: "Logins");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Id",
|
||||||
|
table: "Servers",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "INTEGER")
|
||||||
|
.OldAnnotation("Sqlite:Autoincrement", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,9 @@ namespace JellyGlassBackend.Migrations
|
||||||
|
|
||||||
modelBuilder.Entity("JellyGlass.Models.Server", b =>
|
modelBuilder.Entity("JellyGlass.Models.Server", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<int>("Id")
|
||||||
.HasColumnType("TEXT");
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("ApiToken")
|
b.Property<string>("ApiToken")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
|
|
@ -48,6 +49,9 @@ namespace JellyGlassBackend.Migrations
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAdmin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Username");
|
b.HasKey("Username");
|
||||||
|
|
||||||
b.ToTable("Logins");
|
b.ToTable("Logins");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
namespace JellyGlass.Models;
|
|
||||||
|
|
||||||
public class LoginDTO
|
|
||||||
{
|
|
||||||
public required string Username { get; set; }
|
|
||||||
public required string Password { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace JellyGlass.Models;
|
namespace JellyGlass.Models;
|
||||||
|
|
||||||
public class Server
|
public class Server
|
||||||
{
|
{
|
||||||
public string Owner { get; set; } = string.Empty;
|
public string Owner { get; set; } = string.Empty;
|
||||||
public string Url { get; set; } = string.Empty;
|
public string Url { get; set; } = string.Empty;
|
||||||
public string Id { get; set; } = string.Empty;
|
[Key]
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public int Id { get; set; }
|
||||||
public string ApiToken { get; set; } = string.Empty;
|
public string ApiToken { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ public class ServerDTO
|
||||||
|
|
||||||
public string Owner { get; set; } = string.Empty;
|
public string Owner { get; set; } = string.Empty;
|
||||||
public string Url { get; set; } = string.Empty;
|
public string Url { get; set; } = string.Empty;
|
||||||
public string Id { get; set; } = string.Empty;
|
public int Id { get; set; }
|
||||||
|
public string JellyfinServerID { get; set; } = string.Empty;
|
||||||
public bool Errored { get; set; } = false;
|
public bool Errored { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
@ -7,4 +7,5 @@ public class UserLogin
|
||||||
[Key]
|
[Key]
|
||||||
public required string Username { get; set; }
|
public required string Username { get; set; }
|
||||||
public required string HashedPassword { get; set; }
|
public required string HashedPassword { get; set; }
|
||||||
|
public required bool IsAdmin { get; set; } = false;
|
||||||
}
|
}
|
||||||
13
backend/src/Models/UserLoginDTO.cs
Normal file
13
backend/src/Models/UserLoginDTO.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace JellyGlass.Models;
|
||||||
|
|
||||||
|
public class UserLoginDTO
|
||||||
|
{
|
||||||
|
public UserLoginDTO(UserLogin login)
|
||||||
|
{
|
||||||
|
Username = login.Username;
|
||||||
|
IsAdmin = login.IsAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Username { get; set; }
|
||||||
|
public bool IsAdmin { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -5,5 +5,8 @@ namespace JellyGlass.Repositories;
|
||||||
public interface IServerRepository
|
public interface IServerRepository
|
||||||
{
|
{
|
||||||
public Task<Server[]> GetServers();
|
public Task<Server[]> GetServers();
|
||||||
public Task<Server> GetServerById(string id);
|
public Task<Server> GetServerById(int id);
|
||||||
|
public Task<Server> CreateServer(string owner, string url, string apiToken);
|
||||||
|
public Task<Server> UpdateServer(int id, string owner, string url, string apiToken);
|
||||||
|
public Task<Server> DeleteServer(int id);
|
||||||
}
|
}
|
||||||
|
|
@ -5,4 +5,9 @@ namespace JellyGlass.Repositories;
|
||||||
public interface ILoginRepository
|
public interface ILoginRepository
|
||||||
{
|
{
|
||||||
public Task<UserLogin> GetUserLogin(string username);
|
public Task<UserLogin> GetUserLogin(string username);
|
||||||
|
public Task<UserLogin[]> GetUserLogins();
|
||||||
|
public Task<UserLogin> CreateLogin(string username, string hashedPassword, bool isAdmin);
|
||||||
|
public Task<UserLogin> ChangeUserPassword(string username, string hashedPassword);
|
||||||
|
public Task<UserLogin> ChangeUserAdmin(string username, bool isAdmin);
|
||||||
|
public Task<UserLogin> DeleteLogin(string username);
|
||||||
}
|
}
|
||||||
|
|
@ -24,4 +24,65 @@ public class LoginRepository : ILoginRepository
|
||||||
|
|
||||||
return login;
|
return login;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<UserLogin[]> GetUserLogins()
|
||||||
|
{
|
||||||
|
var logins = await _context.Logins.ToArrayAsync();
|
||||||
|
|
||||||
|
return logins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserLogin> CreateLogin(string username, string hashedPassword, bool isAdmin)
|
||||||
|
{
|
||||||
|
if (_context.Logins.Where(l => l.Username == username).Any())
|
||||||
|
{
|
||||||
|
throw new AuthAlreadyExistsException(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newLogin = new UserLogin()
|
||||||
|
{
|
||||||
|
Username = username,
|
||||||
|
HashedPassword = hashedPassword,
|
||||||
|
IsAdmin = isAdmin
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.Logins.AddAsync(newLogin);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return newLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserLogin> ChangeUserPassword(string username, string hashedPassword)
|
||||||
|
{
|
||||||
|
var login = await GetUserLogin(username);
|
||||||
|
|
||||||
|
login.HashedPassword = hashedPassword;
|
||||||
|
|
||||||
|
_context.Logins.Update(login);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return login;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserLogin> ChangeUserAdmin(string username, bool isAdmin)
|
||||||
|
{
|
||||||
|
var login = await GetUserLogin(username);
|
||||||
|
|
||||||
|
login.IsAdmin = isAdmin;
|
||||||
|
|
||||||
|
_context.Logins.Update(login);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return login;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserLogin> DeleteLogin(string username)
|
||||||
|
{
|
||||||
|
var login = await GetUserLogin(username);
|
||||||
|
|
||||||
|
_context.Logins.Remove(login);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return login;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using JellyGlass.Exceptions;
|
||||||
using JellyGlass.Models;
|
using JellyGlass.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
|
@ -19,10 +20,55 @@ public class ServerRepository : IServerRepository
|
||||||
return servers;
|
return servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Server> GetServerById(string id)
|
public async Task<Server> GetServerById(int id)
|
||||||
{
|
{
|
||||||
var server = await _context.Servers.FirstOrDefaultAsync(s => s.Id == id);
|
var server = await _context.Servers.FirstOrDefaultAsync(s => s.Id == id);
|
||||||
|
|
||||||
|
if (server == null)
|
||||||
|
{
|
||||||
|
throw new ServerNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Server> CreateServer(string owner, string url, string apiToken)
|
||||||
|
{
|
||||||
|
|
||||||
|
var newServer = new Server()
|
||||||
|
{
|
||||||
|
ApiToken = apiToken,
|
||||||
|
Url = url,
|
||||||
|
Owner = owner
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.Servers.AddAsync(newServer);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return newServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Server> UpdateServer(int id, string owner, string url, string apiToken)
|
||||||
|
{
|
||||||
|
var server = await GetServerById(id);
|
||||||
|
|
||||||
|
server.Owner = owner;
|
||||||
|
server.Url = url;
|
||||||
|
server.ApiToken = apiToken;
|
||||||
|
|
||||||
|
_context.Servers.Update(server);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Server> DeleteServer(int id)
|
||||||
|
{
|
||||||
|
var server = await GetServerById(id);
|
||||||
|
|
||||||
|
_context.Servers.Remove(server);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ public class SessionRepository : ISessionRepository
|
||||||
|
|
||||||
public async Task<UserSession?> GetUserSession(string sessionToken)
|
public async Task<UserSession?> GetUserSession(string sessionToken)
|
||||||
{
|
{
|
||||||
var session = await _context.Sessions.FirstOrDefaultAsync(s => s.SessionToken == sessionToken);
|
var session = await _context.Sessions.Include(s => s.Login).FirstOrDefaultAsync(s => s.SessionToken == sessionToken);
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,19 +49,87 @@ public class AuthService : IAuthService
|
||||||
|
|
||||||
var session = await _sessionRepo.GetUserSession(sessionToken);
|
var session = await _sessionRepo.GetUserSession(sessionToken);
|
||||||
|
|
||||||
if (session == null)
|
if (session == null) //session doesn't exist
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (session.ExpiresOn < DateTime.Now)
|
else if (session.ExpiresOn < DateTime.Now) //session has timed out
|
||||||
{
|
{
|
||||||
await _sessionRepo.DeleteSession(sessionToken);
|
await _sessionRepo.DeleteSession(sessionToken);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else
|
else //session exists and hasn't timed out
|
||||||
{
|
{
|
||||||
await _sessionRepo.RefreshSessionExpiry(sessionToken);
|
await _sessionRepo.RefreshSessionExpiry(sessionToken);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsAdmin(string sessionToken)
|
||||||
|
{
|
||||||
|
var session = await _sessionRepo.GetUserSession(sessionToken);
|
||||||
|
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
throw new SessionNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.Login.IsAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserLoginDTO[]> GetLogins()
|
||||||
|
{
|
||||||
|
var logins = await _loginRepo.GetUserLogins();
|
||||||
|
|
||||||
|
var loginDTOs = new List<UserLoginDTO>();
|
||||||
|
|
||||||
|
foreach (var login in logins)
|
||||||
|
{
|
||||||
|
loginDTOs.Add(new UserLoginDTO(login));
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginDTOs.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserLoginDTO> GetLogin(string username)
|
||||||
|
{
|
||||||
|
var login = await _loginRepo.GetUserLogin(username);
|
||||||
|
|
||||||
|
return new UserLoginDTO(login);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserLoginDTO> CreateLogin(string username, string password)
|
||||||
|
{
|
||||||
|
var hashedPassword = BCrypt.Net.BCrypt.HashPassword(password);
|
||||||
|
|
||||||
|
var newLogin = await _loginRepo.CreateLogin(username, hashedPassword, false);
|
||||||
|
|
||||||
|
return new UserLoginDTO(newLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLoginOwnPassword(string username, string newPassword, string oldPassword)
|
||||||
|
{
|
||||||
|
var login = await _loginRepo.GetUserLogin(username);
|
||||||
|
|
||||||
|
if (!BCrypt.Net.BCrypt.Verify(oldPassword, login.HashedPassword))
|
||||||
|
{
|
||||||
|
throw new LoginFailedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpdateLoginPassword(username, newPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLoginPassword(string username, string newPassword)
|
||||||
|
{
|
||||||
|
var hashedNewPassword = BCrypt.Net.BCrypt.HashPassword(newPassword);
|
||||||
|
|
||||||
|
await _loginRepo.ChangeUserPassword(username, hashedNewPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserLoginDTO> DeleteLogin(string username)
|
||||||
|
{
|
||||||
|
var deletedLogin = await _loginRepo.DeleteLogin(username);
|
||||||
|
|
||||||
|
return new UserLoginDTO(deletedLogin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,4 +6,11 @@ public interface IAuthService
|
||||||
{
|
{
|
||||||
public Task<UserSessionDTO> AuthenticateUser(string username, string password);
|
public Task<UserSessionDTO> AuthenticateUser(string username, string password);
|
||||||
public Task<bool> IsAuthenticated(string? sessionToken);
|
public Task<bool> IsAuthenticated(string? sessionToken);
|
||||||
|
public Task<bool> IsAdmin(string sessionToken);
|
||||||
|
public Task<UserLoginDTO[]> GetLogins();
|
||||||
|
public Task<UserLoginDTO> GetLogin(string username);
|
||||||
|
public Task<UserLoginDTO> CreateLogin(string username, string password);
|
||||||
|
public Task UpdateLoginOwnPassword(string username, string newPassword, string oldPassword);
|
||||||
|
public Task UpdateLoginPassword(string username, string newPassword);
|
||||||
|
public Task<UserLoginDTO> DeleteLogin(string username);
|
||||||
}
|
}
|
||||||
|
|
@ -7,4 +7,8 @@ namespace JellyGlass.Services;
|
||||||
public interface IServerService
|
public interface IServerService
|
||||||
{
|
{
|
||||||
public Task<ServerDTO[]> GetServers();
|
public Task<ServerDTO[]> GetServers();
|
||||||
|
public Task<ServerDTO> GetServerByID(int id);
|
||||||
|
public Task<ServerDTO> CreateServer(string owner, string url, string apiToken);
|
||||||
|
public Task<ServerDTO> UpdateServer(int id, string owner, string url, string apiToken);
|
||||||
|
public Task<ServerDTO> DeleteServer(int id);
|
||||||
}
|
}
|
||||||
|
|
@ -28,11 +28,9 @@ public class ServerService : IServerService
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"ID for server {client.InstanceUrl} is {client.ID}");
|
_logger.LogInformation($"ID for server {client.InstanceUrl} is {client.ID}");
|
||||||
|
|
||||||
var dto = new ServerDTO();
|
|
||||||
var server = servers.First(s => s.Url == client.InstanceUrl);
|
var server = servers.First(s => s.Url == client.InstanceUrl);
|
||||||
dto.Id = client.ID;
|
var dto = new ServerDTO(server);
|
||||||
dto.Url = client.InstanceUrl;
|
dto.JellyfinServerID = client.ID;
|
||||||
dto.Owner = server.Owner;
|
|
||||||
|
|
||||||
dtos.Add(dto);
|
dtos.Add(dto);
|
||||||
}
|
}
|
||||||
|
|
@ -51,17 +49,31 @@ public class ServerService : IServerService
|
||||||
return dtos.ToArray();
|
return dtos.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ServerDTO[]> GetServers2()
|
public async Task<ServerDTO> GetServerByID(int id)
|
||||||
{
|
{
|
||||||
var servers = await _repository.GetServers();
|
var server = await _repository.GetServerById(id);
|
||||||
|
|
||||||
var dtos = new List<ServerDTO>();
|
return new ServerDTO(server);
|
||||||
|
|
||||||
foreach (var s in servers)
|
|
||||||
{
|
|
||||||
dtos.Add(new ServerDTO(s));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dtos.ToArray();
|
public async Task<ServerDTO> CreateServer(string owner, string url, string apiToken)
|
||||||
|
{
|
||||||
|
var newServer = await _repository.CreateServer(owner, url, apiToken);
|
||||||
|
|
||||||
|
return new ServerDTO(newServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServerDTO> UpdateServer(int id, string owner, string url, string apiToken)
|
||||||
|
{
|
||||||
|
var updatedServer = await _repository.UpdateServer(id, owner, url, apiToken);
|
||||||
|
|
||||||
|
return new ServerDTO(updatedServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServerDTO> DeleteServer(int id)
|
||||||
|
{
|
||||||
|
var deletedServer = await _repository.DeleteServer(id);
|
||||||
|
|
||||||
|
return new ServerDTO(deletedServer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ const ServerSearch = ({ searchTerm, server }: ServerSearchProps) => {
|
||||||
const [searchResults, setSearchResults] = useState<Array<SearchResult | undefined>>();
|
const [searchResults, setSearchResults] = useState<Array<SearchResult | undefined>>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
search(searchTerm, server.id).then(results => {
|
search(searchTerm, server.jellyfinServerID).then(results => {
|
||||||
setSearchResults(results);
|
setSearchResults(results);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export interface Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logIn = async (username: string, password: string) => {
|
export const logIn = async (username: string, password: string) => {
|
||||||
const response = await axios.post<Session>(`${apiUrl}/auth`, `username=${encodeURI(username)}&password=${encodeURI(password)}`);
|
const response = await axios.post<Session>(`${apiUrl}/auth/login`, `username=${encodeURI(username)}&password=${encodeURI(password)}`);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,8 @@ import { apiUrl } from "./api";
|
||||||
|
|
||||||
export interface Server {
|
export interface Server {
|
||||||
name?: string;
|
name?: string;
|
||||||
id: string;
|
id: number;
|
||||||
|
jellyfinServerID: string;
|
||||||
errored: boolean;
|
errored: boolean;
|
||||||
owner: string;
|
owner: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|
@ -11,7 +12,7 @@ export interface Server {
|
||||||
|
|
||||||
export const getServerList = async (): Promise<Array<Server>> => {
|
export const getServerList = async (): Promise<Array<Server>> => {
|
||||||
console.log("fetching server list");
|
console.log("fetching server list");
|
||||||
const response = await axios.get<Array<Server>>(`${apiUrl}/servers`, { withCredentials: true });
|
const response = await axios.get<Array<Server>>(`${apiUrl}/servers/all`, { withCredentials: true });
|
||||||
|
|
||||||
console.log(response);
|
console.log(response);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue