feat(auth): Added authentication
This commit is contained in:
parent
d85d4334f8
commit
5e100c75ed
39 changed files with 704 additions and 86 deletions
32
backend/src/Controllers/AuthController.cs
Normal file
32
backend/src/Controllers/AuthController.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
using JellyGlass.Exceptions;
|
||||||
|
using JellyGlass.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace JellyGlass.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
private IAuthService _service;
|
||||||
|
|
||||||
|
public AuthController(IAuthService service)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Authenticate([FromForm] string username, [FromForm] string password)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session = await _service.AuthenticateUser(username, password);
|
||||||
|
|
||||||
|
return Ok(session);
|
||||||
|
}
|
||||||
|
catch (LoginFailedException)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
using JellyGlass.Models;
|
|
||||||
using JellyGlass.Services;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace JellyGlass.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("/api/[controller]")]
|
|
||||||
public class LibraryController : ControllerBase
|
|
||||||
{
|
|
||||||
private ILogger<LibraryController> _logger;
|
|
||||||
private ILibraryService _service;
|
|
||||||
|
|
||||||
public LibraryController(ILogger<LibraryController> logger, ILibraryService service)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_service = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet()]
|
|
||||||
public async Task<IActionResult> GetLibrares()
|
|
||||||
{
|
|
||||||
var libraries = await _service.GetLibraries();
|
|
||||||
|
|
||||||
return Ok(libraries);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{libraryName}")]
|
|
||||||
public async Task<IActionResult> GetLibraryItems([FromRoute] string libraryName)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// [HttpGet("{libraryName}/Item/{itemName}")]
|
|
||||||
// public async Task<IActionResult> GetLibraryItem([FromRoute] string libraryName, string itemName)
|
|
||||||
// {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [HttpGet("TvShows/{seriesName}")]
|
|
||||||
// public async Task<IActionResult> GetSeasonsForTvSeries([FromRoute] string seriesName)
|
|
||||||
// {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [HttpGet("TvShows/{seriesName}/Season/{seasonName}")]
|
|
||||||
// public async Task<IActionResult> GetEpisodesForTvSeriesSeason([FromRoute] string seriesName, string seasonName)
|
|
||||||
// {
|
|
||||||
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
@ -10,16 +10,25 @@ public class SearchController : ControllerBase
|
||||||
{
|
{
|
||||||
private ILogger<SearchController> _logger;
|
private ILogger<SearchController> _logger;
|
||||||
private ISearchService _service;
|
private ISearchService _service;
|
||||||
|
private IAuthService _auth;
|
||||||
|
|
||||||
public SearchController(ILogger<SearchController> logger, ISearchService service)
|
public SearchController(ILogger<SearchController> logger, ISearchService service, IAuthService auth)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_service = service;
|
_service = service;
|
||||||
|
_auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> handleSearch([FromQuery] string searchTerm, string serverId)
|
public async Task<IActionResult> handleSearch([FromQuery] string searchTerm, string serverId)
|
||||||
{
|
{
|
||||||
|
var sessionToken = Request.Cookies["session"];
|
||||||
|
|
||||||
|
if (!await _auth.IsAuthenticated(sessionToken))
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
var decodedSearchTerm = HttpUtility.UrlDecode(searchTerm);
|
var decodedSearchTerm = HttpUtility.UrlDecode(searchTerm);
|
||||||
var results = await _service.Search(decodedSearchTerm, serverId);
|
var results = await _service.Search(decodedSearchTerm, serverId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,27 @@ namespace JellyGlass.Controllers;
|
||||||
public class ServersController : ControllerBase
|
public class ServersController : ControllerBase
|
||||||
{
|
{
|
||||||
private ILogger<ServersController> _logger;
|
private ILogger<ServersController> _logger;
|
||||||
private IServerService _service;
|
private IServerService _serverService;
|
||||||
|
private IAuthService _authService;
|
||||||
|
|
||||||
public ServersController(ILogger<ServersController> logger, IServerService service)
|
public ServersController(ILogger<ServersController> logger, IServerService serverService, IAuthService authService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_service = service;
|
_serverService = serverService;
|
||||||
|
_authService = authService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> getServers()
|
public async Task<IActionResult> getServers()
|
||||||
{
|
{
|
||||||
var servers = await _service.GetServers();
|
var sessionToken = Request.Cookies["session"];
|
||||||
|
|
||||||
|
if (!await _authService.IsAuthenticated(sessionToken))
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var servers = await _serverService.GetServers();
|
||||||
|
|
||||||
return Ok(servers);
|
return Ok(servers);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
backend/src/Exceptions/AuthRepositoryExceptions.cs
Normal file
16
backend/src/Exceptions/AuthRepositoryExceptions.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace JellyGlass.Exceptions;
|
||||||
|
|
||||||
|
public class AuthRepositoryException : System.Exception
|
||||||
|
{
|
||||||
|
public AuthRepositoryException() { }
|
||||||
|
public AuthRepositoryException(string message) : base(message) { }
|
||||||
|
public AuthRepositoryException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
public class AuthNotFoundException : AuthRepositoryException
|
||||||
|
{
|
||||||
|
public AuthNotFoundException() { }
|
||||||
|
public AuthNotFoundException(string message) : base(message) { }
|
||||||
|
public AuthNotFoundException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
16
backend/src/Exceptions/AuthServiceExceptions.cs
Normal file
16
backend/src/Exceptions/AuthServiceExceptions.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace JellyGlass.Exceptions;
|
||||||
|
|
||||||
|
public class AuthServiceException : Exception
|
||||||
|
{
|
||||||
|
public AuthServiceException() { }
|
||||||
|
public AuthServiceException(string message) : base(message) { }
|
||||||
|
public AuthServiceException(string message, Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
public class LoginFailedException : AuthServiceException
|
||||||
|
{
|
||||||
|
public LoginFailedException() { }
|
||||||
|
public LoginFailedException(string message) : base(message) { }
|
||||||
|
public LoginFailedException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
15
backend/src/Exceptions/SessionRepositoryExceptions.cs
Normal file
15
backend/src/Exceptions/SessionRepositoryExceptions.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace JellyGlass.Exceptions;
|
||||||
|
|
||||||
|
public class SessionRepositoryException : Exception
|
||||||
|
{
|
||||||
|
public SessionRepositoryException() { }
|
||||||
|
public SessionRepositoryException(string message) : base(message) { }
|
||||||
|
public SessionRepositoryException(string message, Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SessionNotFoundException : SessionRepositoryException
|
||||||
|
{
|
||||||
|
public SessionNotFoundException() : base() { }
|
||||||
|
public SessionNotFoundException(string message) : base(message) { }
|
||||||
|
public SessionNotFoundException(string message, Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|
|
||||||
88
backend/src/Migrations/20260303190433_auth.Designer.cs
generated
Normal file
88
backend/src/Migrations/20260303190433_auth.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
// <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("20260303190433_auth")]
|
||||||
|
partial class auth
|
||||||
|
{
|
||||||
|
/// <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<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
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.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/src/Migrations/20260303190433_auth.cs
Normal file
60
backend/src/Migrations/20260303190433_auth.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace JellyGlassBackend.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class auth : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Logins",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Username = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
HashedPassword = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Logins", x => x.Username);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Sessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
SessionToken = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
LoginUsername = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
ExpiresOn = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Sessions", x => x.SessionToken);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Sessions_Logins_LoginUsername",
|
||||||
|
column: x => x.LoginUsername,
|
||||||
|
principalTable: "Logins",
|
||||||
|
principalColumn: "Username");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Sessions_LoginUsername",
|
||||||
|
table: "Sessions",
|
||||||
|
column: "LoginUsername");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Sessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Logins");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
using JellyGlass.Repositories;
|
using JellyGlass.Repositories;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
@ -37,6 +38,47 @@ namespace JellyGlassBackend.Migrations
|
||||||
|
|
||||||
b.ToTable("Servers");
|
b.ToTable("Servers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("JellyGlass.Models.UserLogin", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
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
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
backend/src/Models/LoginDTO.cs
Normal file
7
backend/src/Models/LoginDTO.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace JellyGlass.Models;
|
||||||
|
|
||||||
|
public class LoginDTO
|
||||||
|
{
|
||||||
|
public required string Username { get; set; }
|
||||||
|
public required string Password { get; set; }
|
||||||
|
}
|
||||||
10
backend/src/Models/UserLogin.cs
Normal file
10
backend/src/Models/UserLogin.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace JellyGlass.Models;
|
||||||
|
|
||||||
|
public class UserLogin
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public required string Username { get; set; }
|
||||||
|
public required string HashedPassword { get; set; }
|
||||||
|
}
|
||||||
11
backend/src/Models/UserSession.cs
Normal file
11
backend/src/Models/UserSession.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace JellyGlass.Models;
|
||||||
|
|
||||||
|
public class UserSession
|
||||||
|
{
|
||||||
|
public required UserLogin Login { get; set; }
|
||||||
|
[Key]
|
||||||
|
public required string SessionToken { get; set; }
|
||||||
|
public required DateTime ExpiresOn { get; set; }
|
||||||
|
}
|
||||||
13
backend/src/Models/UserSessionDTO.cs
Normal file
13
backend/src/Models/UserSessionDTO.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace JellyGlass.Models;
|
||||||
|
|
||||||
|
public class UserSessionDTO
|
||||||
|
{
|
||||||
|
public UserSessionDTO(UserSession session)
|
||||||
|
{
|
||||||
|
SessionToken = session.SessionToken;
|
||||||
|
ExpiresOn = session.ExpiresOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SessionToken { get; set; }
|
||||||
|
public DateTime ExpiresOn { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -24,11 +24,15 @@ else
|
||||||
|
|
||||||
builder.Services.AddSqlite<DatabaseContext>(dbConnectionString);
|
builder.Services.AddSqlite<DatabaseContext>(dbConnectionString);
|
||||||
|
|
||||||
builder.Services.AddTransient<ILibraryService, LibraryService>();
|
|
||||||
builder.Services.AddTransient<IServerRepository, ServerRepository>();
|
builder.Services.AddTransient<IServerRepository, ServerRepository>();
|
||||||
|
builder.Services.AddTransient<ILoginRepository, LoginRepository>();
|
||||||
|
builder.Services.AddTransient<ISessionRepository, SessionRepository>();
|
||||||
|
|
||||||
|
builder.Services.AddTransient<ILibraryService, LibraryService>();
|
||||||
builder.Services.AddScoped<IClientService, ClientService>();
|
builder.Services.AddScoped<IClientService, ClientService>();
|
||||||
builder.Services.AddTransient<IServerService, ServerService>();
|
builder.Services.AddTransient<IServerService, ServerService>();
|
||||||
builder.Services.AddTransient<ISearchService, SearchService>();
|
builder.Services.AddTransient<ISearchService, SearchService>();
|
||||||
|
builder.Services.AddTransient<IAuthService, AuthService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ namespace JellyGlass.Repositories;
|
||||||
|
|
||||||
public class DatabaseContext : DbContext
|
public class DatabaseContext : DbContext
|
||||||
{
|
{
|
||||||
public DatabaseContext(DbContextOptions options)
|
public DatabaseContext(DbContextOptions<DatabaseContext> options)
|
||||||
: base(options)
|
: base(options)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<Server> Servers { get; set; }
|
public DbSet<Server> Servers { get; set; }
|
||||||
|
public DbSet<UserLogin> Logins { get; set; }
|
||||||
|
public DbSet<UserSession> Sessions { get; set; }
|
||||||
}
|
}
|
||||||
11
backend/src/Repositories/ISessionRepository.cs
Normal file
11
backend/src/Repositories/ISessionRepository.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using JellyGlass.Models;
|
||||||
|
|
||||||
|
namespace JellyGlass.Repositories;
|
||||||
|
|
||||||
|
public interface ISessionRepository
|
||||||
|
{
|
||||||
|
public Task<UserSession> CreateUserSession(UserLogin user);
|
||||||
|
public Task<UserSession?> GetUserSession(string sessionToken);
|
||||||
|
public Task RefreshSessionExpiry(string sessionToken);
|
||||||
|
public Task DeleteSession(string sessionToken);
|
||||||
|
}
|
||||||
8
backend/src/Repositories/IloginRepository.cs
Normal file
8
backend/src/Repositories/IloginRepository.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
using JellyGlass.Models;
|
||||||
|
|
||||||
|
namespace JellyGlass.Repositories;
|
||||||
|
|
||||||
|
public interface ILoginRepository
|
||||||
|
{
|
||||||
|
public Task<UserLogin> GetUserLogin(string username);
|
||||||
|
}
|
||||||
27
backend/src/Repositories/LoginRepository.cs
Normal file
27
backend/src/Repositories/LoginRepository.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
using JellyGlass.Exceptions;
|
||||||
|
using JellyGlass.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace JellyGlass.Repositories;
|
||||||
|
|
||||||
|
public class LoginRepository : ILoginRepository
|
||||||
|
{
|
||||||
|
private DatabaseContext _context;
|
||||||
|
|
||||||
|
public LoginRepository(DatabaseContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserLogin> GetUserLogin(string username)
|
||||||
|
{
|
||||||
|
var login = await _context.Logins.FirstOrDefaultAsync(l => l.Username == username);
|
||||||
|
|
||||||
|
if (login == null)
|
||||||
|
{
|
||||||
|
throw new AuthNotFoundException($"Login {username} doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return login;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
backend/src/Repositories/SessionRepository.cs
Normal file
68
backend/src/Repositories/SessionRepository.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
using JellyGlass.Exceptions;
|
||||||
|
using JellyGlass.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace JellyGlass.Repositories;
|
||||||
|
|
||||||
|
public class SessionRepository : ISessionRepository
|
||||||
|
{
|
||||||
|
private DatabaseContext _context;
|
||||||
|
|
||||||
|
public SessionRepository(DatabaseContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserSession> CreateUserSession(UserLogin user)
|
||||||
|
{
|
||||||
|
var newSession = new UserSession()
|
||||||
|
{
|
||||||
|
Login = user,
|
||||||
|
SessionToken = Guid.NewGuid().ToString(),
|
||||||
|
ExpiresOn = GenerateExpiryDate()
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.Sessions.AddAsync(newSession);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return newSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserSession?> GetUserSession(string sessionToken)
|
||||||
|
{
|
||||||
|
var session = await _context.Sessions.FirstOrDefaultAsync(s => s.SessionToken == sessionToken);
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshSessionExpiry(string sessionToken)
|
||||||
|
{
|
||||||
|
var session = await GetUserSession(sessionToken);
|
||||||
|
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
throw new SessionNotFoundException($"Couldn't find session {sessionToken} while trying ti refresh token");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.ExpiresOn = GenerateExpiryDate();
|
||||||
|
|
||||||
|
_context.Sessions.Update(session);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteSession(string sessionToken)
|
||||||
|
{
|
||||||
|
var session = await GetUserSession(sessionToken);
|
||||||
|
|
||||||
|
if (session != null)
|
||||||
|
{
|
||||||
|
_context.Sessions.Remove(session);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime GenerateExpiryDate()
|
||||||
|
{
|
||||||
|
return DateTime.Now + TimeSpan.FromDays(30); //TODO: Make this configurable
|
||||||
|
}
|
||||||
|
}
|
||||||
67
backend/src/Services/AuthService.cs
Normal file
67
backend/src/Services/AuthService.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
using JellyGlass.Exceptions;
|
||||||
|
using JellyGlass.Models;
|
||||||
|
using JellyGlass.Repositories;
|
||||||
|
|
||||||
|
namespace JellyGlass.Services;
|
||||||
|
|
||||||
|
public class AuthService : IAuthService
|
||||||
|
{
|
||||||
|
private ILoginRepository _loginRepo;
|
||||||
|
private ISessionRepository _sessionRepo;
|
||||||
|
|
||||||
|
public AuthService(ILoginRepository loginRepo, ISessionRepository sessionRepo)
|
||||||
|
{
|
||||||
|
_loginRepo = loginRepo;
|
||||||
|
_sessionRepo = sessionRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserSessionDTO> AuthenticateUser(string username, string password)
|
||||||
|
{
|
||||||
|
UserLogin login;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
login = await _loginRepo.GetUserLogin(username);
|
||||||
|
}
|
||||||
|
catch (AuthNotFoundException)
|
||||||
|
{
|
||||||
|
throw new LoginFailedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (BCrypt.Net.BCrypt.Verify(password, login.HashedPassword))
|
||||||
|
{
|
||||||
|
var session = await _sessionRepo.CreateUserSession(login);
|
||||||
|
return new UserSessionDTO(session);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new LoginFailedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsAuthenticated(string? sessionToken)
|
||||||
|
{
|
||||||
|
if (sessionToken == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = await _sessionRepo.GetUserSession(sessionToken);
|
||||||
|
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (session.ExpiresOn < DateTime.Now)
|
||||||
|
{
|
||||||
|
await _sessionRepo.DeleteSession(sessionToken);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _sessionRepo.RefreshSessionExpiry(sessionToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
backend/src/Services/IAuthService.cs
Normal file
9
backend/src/Services/IAuthService.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
using JellyGlass.Models;
|
||||||
|
|
||||||
|
namespace JellyGlass.Services;
|
||||||
|
|
||||||
|
public interface IAuthService
|
||||||
|
{
|
||||||
|
public Task<UserSessionDTO> AuthenticateUser(string username, string password);
|
||||||
|
public Task<bool> IsAuthenticated(string? sessionToken);
|
||||||
|
}
|
||||||
18
frontend/package-lock.json
generated
18
frontend/package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|
@ -19,6 +20,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|
@ -1819,6 +1821,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-cookie": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
|
@ -3285,6 +3294,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/js-cookie": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
import { Navbar as BsNavbar, Container } from "react-bootstrap";
|
import { Navbar as BsNavbar, Button, Container } from "react-bootstrap";
|
||||||
// import styles from "./Navbar.module.scss";
|
// import styles from "./Navbar.module.scss";
|
||||||
import Searchbar from "../Searchbar/Searchbar";
|
import Searchbar from "../Searchbar/Searchbar";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const [searchText, setSearchText] = useState<string>("");
|
const [searchText, setSearchText] = useState<string>("");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const session = Cookies.get("session");
|
||||||
|
|
||||||
|
function onLogout() {
|
||||||
|
Cookies.remove("session");
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
|
|
||||||
function onSearch() {
|
function onSearch() {
|
||||||
navigate(`/search?search=${searchText}`);
|
navigate(`/search?search=${searchText}`);
|
||||||
|
|
@ -22,9 +29,10 @@ const Navbar = () => {
|
||||||
<BsNavbar.Toggle />
|
<BsNavbar.Toggle />
|
||||||
<BsNavbar.Collapse>
|
<BsNavbar.Collapse>
|
||||||
<Container className="justify-content-center d-flex">
|
<Container className="justify-content-center d-flex">
|
||||||
<Searchbar text={searchText} setText={setSearchText} onSearch={onSearch} />
|
<Searchbar text={searchText} setText={setSearchText} onSearch={onSearch} enabled={session !== undefined} />
|
||||||
</Container>
|
</Container>
|
||||||
</BsNavbar.Collapse>
|
</BsNavbar.Collapse>
|
||||||
|
{session && <Button onClick={onLogout}>Logout</Button>}
|
||||||
</Container>
|
</Container>
|
||||||
</BsNavbar>
|
</BsNavbar>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,9 @@
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.searchbarDisabled {
|
||||||
|
@extend .searchbar;
|
||||||
|
background-color: lightgray;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,10 @@ interface SearchbarProps {
|
||||||
text: string,
|
text: string,
|
||||||
setText: (text: string) => void;
|
setText: (text: string) => void;
|
||||||
onSearch?: () => void;
|
onSearch?: () => void;
|
||||||
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Searchbar = ({ text, setText, onSearch }: SearchbarProps) => {
|
const Searchbar = ({ text, setText, onSearch, enabled = true }: SearchbarProps) => {
|
||||||
|
|
||||||
function onKeyPressed(event: React.KeyboardEvent<HTMLInputElement>) {
|
function onKeyPressed(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
if (onSearch === undefined) {
|
if (onSearch === undefined) {
|
||||||
|
|
@ -20,7 +21,7 @@ const Searchbar = ({ text, setText, onSearch }: SearchbarProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input className={styles.searchbar} type="text" placeholder="Search" value={text} onChange={e => setText(e.target.value)} onKeyUp={onKeyPressed} />
|
<input className={enabled ? styles.searchbar : styles.searchbarDisabled} type="text" placeholder="Search" value={text} onChange={e => setText(e.target.value)} onKeyUp={onKeyPressed} disabled={!enabled} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ const ServerList = () => {
|
||||||
setServers(serverList);
|
setServers(serverList);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
setServers([]);
|
setServers([]);
|
||||||
|
if (err.response && err.response.status === 401) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
alert(err);
|
alert(err);
|
||||||
})
|
})
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
13
frontend/src/Lib/Auth.ts
Normal file
13
frontend/src/Lib/Auth.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import axios from "axios"
|
||||||
|
import { apiUrl } from "./api"
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
sessionToken: string;
|
||||||
|
expiresOn: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logIn = async (username: string, password: string) => {
|
||||||
|
const response = await axios.post<Session>(`${apiUrl}/auth`, `username=${encodeURI(username)}&password=${encodeURI(password)}`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ export interface SearchResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const search = async (searchTerm: string, serverId: string): Promise<Array<SearchResult>> => {
|
export const search = async (searchTerm: string, serverId: string): Promise<Array<SearchResult>> => {
|
||||||
const response = await axios.get<Array<SearchResult>>(encodeURI(`${apiUrl}/search?searchTerm=${searchTerm}&serverId=${serverId}`));
|
const response = await axios.get<Array<SearchResult>>(encodeURI(`${apiUrl}/search?searchTerm=${searchTerm}&serverId=${serverId}`), { withCredentials: true });
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,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`);
|
const response = await axios.get<Array<Server>>(`${apiUrl}/servers`, { withCredentials: true });
|
||||||
|
|
||||||
console.log(response);
|
console.log(response);
|
||||||
|
|
||||||
|
|
|
||||||
9
frontend/src/Pages/Login/Login.module.scss
Normal file
9
frontend/src/Pages/Login/Login.module.scss
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.form {
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 50px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formButton {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
54
frontend/src/Pages/Login/Login.tsx
Normal file
54
frontend/src/Pages/Login/Login.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Button, Form, FormGroup } from "react-bootstrap";
|
||||||
|
import { logIn } from "../../Lib/Auth";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import styles from "./Login.module.scss";
|
||||||
|
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const [username, setUsername] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Cookies.get("session") !== undefined) {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
logIn(username, password).then(sessionToken => {
|
||||||
|
Cookies.set("session", sessionToken.sessionToken, { expires: Date.parse(sessionToken.expiresOn) });
|
||||||
|
navigate("/");
|
||||||
|
}).catch(err => {
|
||||||
|
alert(err);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFormKeypress(e: React.KeyboardEvent<HTMLFormElement>) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.form}>
|
||||||
|
<Form onKeyUp={onFormKeypress}>
|
||||||
|
<h3>Login</h3>
|
||||||
|
<FormGroup>
|
||||||
|
<Form.Label>Username</Form.Label>
|
||||||
|
<Form.Control type="text" value={username} onChange={e => setUsername(e.target.value)} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Form.Label>Password</Form.Label>
|
||||||
|
<Form.Control type="password" value={password} onChange={e => setPassword(e.target.value)} />
|
||||||
|
</FormGroup>
|
||||||
|
<Button className={styles.formButton} variant="primary" onClick={onSubmit}>Login</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
|
|
@ -3,35 +3,51 @@ import { getServerList, type Server } from "../../Lib/Servers";
|
||||||
import ServerSearch from "../../Components/ServerSearch/ServerSearch";
|
import ServerSearch from "../../Components/ServerSearch/ServerSearch";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { Spinner } from "react-bootstrap";
|
import { Spinner } from "react-bootstrap";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [servers, setServers] = useState<Array<Server>>([]);
|
const [servers, setServers] = useState<Array<Server>>([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const sessionCookie = Cookies.get("session");
|
||||||
|
|
||||||
const searchTerm = searchParams.get("search") || "";
|
const searchTerm = searchParams.get("search") || "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!sessionCookie) {
|
||||||
|
navigate("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (searchTerm === "") {
|
if (searchTerm === "") {
|
||||||
alert(`Error search term missing: ${searchTerm}`);
|
alert(`Error search term missing: ${searchTerm}`);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (servers.length > 0) {
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
setServers([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
getServerList().then(servers => {
|
getServerList().then(servers => {
|
||||||
if (servers.length === 0) {
|
if (servers.length === 0) {
|
||||||
alert("No servers found");
|
alert("No servers found");
|
||||||
}
|
}
|
||||||
setServers(servers);
|
|
||||||
|
const workingServers: Array<Server> = [];
|
||||||
|
|
||||||
|
servers.forEach(s => {
|
||||||
|
if (!s.errored) {
|
||||||
|
workingServers.push(s);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (workingServers.length === 0) {
|
||||||
|
alert("No working servers");
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
setServers(workingServers);
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
alert(e);
|
alert(e);
|
||||||
});
|
});
|
||||||
}, [searchTerm]);
|
}, [searchTerm, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
import ServerList from "./Components/ServerList/ServerList"
|
import { useEffect } from "react";
|
||||||
|
import ServerList from "./Components/ServerList/ServerList";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const sessionCookie = Cookies.get("session");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sessionCookie) {
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
|
}, [navigate, sessionCookie]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", padding: "20px", display: "flex", flexDirection: "column", alignItems: "center" }}>
|
<div style={{ width: "100%", padding: "20px", display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||||
<h1>Available Servers</h1>
|
<h1>Available Servers</h1>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import Index from './index.tsx'
|
||||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||||
import Navbar from './Components/Navbar/Navbar.tsx'
|
import Navbar from './Components/Navbar/Navbar.tsx'
|
||||||
import Search from './Pages/Search/Search.tsx';
|
import Search from './Pages/Search/Search.tsx';
|
||||||
|
import Login from './Pages/Login/Login.tsx';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|
@ -13,6 +14,7 @@ createRoot(document.getElementById('root')!).render(
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/search" element={<Search />} />
|
<Route path="/search" element={<Search />} />
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue