mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Some fixes from last release (#1884)
* Removed SecurityEvent middleware solution. It was out of scope originally. * Fixed manage users still calling pending when the api is no more * Added back the online indicator on manage users
This commit is contained in:
parent
93bd7d7c19
commit
d070da2834
@ -47,7 +47,6 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||
public DbSet<FolderPath> FolderPath { get; set; } = null!;
|
||||
public DbSet<Device> Device { get; set; } = null!;
|
||||
public DbSet<ServerStatistics> ServerStatistics { get; set; } = null!;
|
||||
public DbSet<SecurityEvent> SecurityEvent { get; set; } = null!;
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
|
1872
API/Data/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs
generated
Normal file
1872
API/Data/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
API/Data/Migrations/20230316233133_RemoveSecurityEvent.cs
Normal file
40
API/Data/Migrations/20230316233133_RemoveSecurityEvent.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveSecurityEvent : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SecurityEvent");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SecurityEvent",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
IpAddress = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RequestMethod = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RequestPath = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UserAgent = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SecurityEvent", x => x.Id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -944,35 +944,6 @@ namespace API.Data.Migrations
|
||||
b.ToTable("ReadingListItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SecurityEvent", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RequestMethod")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RequestPath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SecurityEvent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1,27 +0,0 @@
|
||||
using API.Entities;
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
public interface ISecurityEventRepository
|
||||
{
|
||||
void Add(SecurityEvent securityEvent);
|
||||
}
|
||||
|
||||
public class SecurityEventRepository : ISecurityEventRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public SecurityEventRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Add(SecurityEvent securityEvent)
|
||||
{
|
||||
_context.SecurityEvent.Add(securityEvent);
|
||||
}
|
||||
}
|
@ -25,7 +25,6 @@ public interface IUnitOfWork
|
||||
ISiteThemeRepository SiteThemeRepository { get; }
|
||||
IMangaFileRepository MangaFileRepository { get; }
|
||||
IDeviceRepository DeviceRepository { get; }
|
||||
ISecurityEventRepository SecurityEventRepository { get; }
|
||||
bool Commit();
|
||||
Task<bool> CommitAsync();
|
||||
bool HasChanges();
|
||||
@ -63,7 +62,6 @@ public class UnitOfWork : IUnitOfWork
|
||||
public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper);
|
||||
public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context);
|
||||
public IDeviceRepository DeviceRepository => new DeviceRepository(_context, _mapper);
|
||||
public ISecurityEventRepository SecurityEventRepository => new SecurityEventRepository(_context, _mapper);
|
||||
|
||||
/// <summary>
|
||||
/// Commits changes to the DB. Completes the open transaction.
|
||||
|
@ -1,14 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace API.Entities;
|
||||
|
||||
public class SecurityEvent
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string IpAddress { get; set; }
|
||||
public string RequestMethod { get; set; }
|
||||
public string RequestPath { get; set; }
|
||||
public string UserAgent { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime CreatedAtUtc { get; set; }
|
||||
}
|
@ -13,7 +13,6 @@ namespace API.Logging;
|
||||
public static class LogLevelOptions
|
||||
{
|
||||
public const string LogFile = "config/logs/kavita.log";
|
||||
public const string SecurityLogFile = "config/logs/security.log";
|
||||
public const bool LogRollingEnabled = true;
|
||||
/// <summary>
|
||||
/// Controls the Logging Level of the Application
|
||||
|
@ -1,62 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.AccessControl;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Logging;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace API.Middleware;
|
||||
|
||||
public class SecurityEventMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SecurityEventMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
|
||||
_logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.File(Path.Join(Directory.GetCurrentDirectory(), "config/logs/", "security.log"), rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var ipAddress = context.Connection.RemoteIpAddress?.ToString();
|
||||
var requestMethod = context.Request.Method;
|
||||
var requestPath = context.Request.Path;
|
||||
var userAgent = context.Request.Headers["User-Agent"];
|
||||
|
||||
var securityEvent = new SecurityEvent
|
||||
{
|
||||
IpAddress = ipAddress,
|
||||
RequestMethod = requestMethod,
|
||||
RequestPath = requestPath,
|
||||
UserAgent = userAgent,
|
||||
CreatedAt = DateTime.Now,
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
using (var scope = context.RequestServices.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
dbContext.Add(securityEvent);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_logger.Debug("Request Processed: {@SecurityEvent}", securityEvent);
|
||||
}
|
||||
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
@ -274,7 +274,6 @@ public class Startup
|
||||
|
||||
|
||||
app.UseMiddleware<ExceptionMiddleware>();
|
||||
app.UseMiddleware<SecurityEventMiddleware>();
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
|
@ -309,7 +309,7 @@ public static class Configuration
|
||||
}
|
||||
#endregion
|
||||
|
||||
private class AppSettings
|
||||
private sealed class AppSettings
|
||||
{
|
||||
public string TokenKey { get; set; }
|
||||
public int Port { get; set; }
|
||||
|
@ -36,10 +36,6 @@ export class MemberService {
|
||||
return this.httpClient.get<boolean>(this.baseUrl + 'users/has-reading-progress?libraryId=' + librayId);
|
||||
}
|
||||
|
||||
getPendingInvites() {
|
||||
return this.httpClient.get<Array<Member>>(this.baseUrl + 'users/pending');
|
||||
}
|
||||
|
||||
addSeriesToWantToRead(seriesIds: Array<number>) {
|
||||
return this.httpClient.post<Array<Member>>(this.baseUrl + 'want-to-read/add-series', {seriesIds});
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ export class MessageHubService {
|
||||
private hubConnection!: HubConnection;
|
||||
|
||||
private messagesSource = new ReplaySubject<Message<any>>(1);
|
||||
private onlineUsersSource = new BehaviorSubject<number[]>([]); // UserIds
|
||||
private onlineUsersSource = new BehaviorSubject<string[]>([]); // UserNames
|
||||
|
||||
/**
|
||||
* Any events that come from the backend
|
||||
@ -142,7 +142,7 @@ export class MessageHubService {
|
||||
.start()
|
||||
.catch(err => console.error(err));
|
||||
|
||||
this.hubConnection.on(EVENTS.OnlineUsers, (usernames: number[]) => {
|
||||
this.hubConnection.on(EVENTS.OnlineUsers, (usernames: string[]) => {
|
||||
this.onlineUsersSource.next(usernames);
|
||||
});
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal">
|
||||
<p>
|
||||
Invite a user to your server. Enter their email in and we will send them an email to create an account. If you do not want to use our email service, you can <a href="https://wiki.kavitareader.com/en/guides/misc/email" rel="noopener noreferrer" target="_blank" rel="noopener noreferrer">host your own</a>
|
||||
Invite a user to your server. Enter their email in and we will send them an email to create an account. If you do not want to use our email service, you can <a href="https://wiki.kavitareader.com/en/guides/misc/email" rel="noopener noreferrer" target="_blank">host your own</a>
|
||||
email service or use a fake email (Forgot User will not work). A link will be presented regardless and can be used to setup the account manually.
|
||||
</p>
|
||||
|
||||
|
@ -10,7 +10,6 @@
|
||||
<li *ngFor="let member of members; let idx = index;" class="list-group-item no-hover">
|
||||
<div>
|
||||
<h4>
|
||||
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="false && (messageHub.onlineUsers$ | async)?.includes(member.id)"></i>
|
||||
<span id="member-name--{{idx}}">{{member.username | titlecase}} </span>
|
||||
<span *ngIf="member.username === loggedInUsername">
|
||||
<i class="fas fa-star" aria-hidden="true"></i>
|
||||
@ -30,7 +29,7 @@
|
||||
<div>Last Active:
|
||||
<span *ngIf="member.lastActive === '0001-01-01T00:00:00'; else activeDate">Never</span>
|
||||
<ng-template #activeDate>
|
||||
{{member.lastActive | date: 'short'}}
|
||||
{{member.lastActive | date: 'short'}} <i class="presence fa fa-circle ms-1" title="Online Now" aria-hidden="true" *ngIf="(messageHub.onlineUsers$ | async)?.includes(member.username)"></i>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div *ngIf="!hasAdminRole(member)">Sharing: {{formatLibraries(member)}}</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
.presence {
|
||||
font-size: 12px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.user-info > div {
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { catchError, take } from 'rxjs/operators';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
import { Member } from 'src/app/_models/auth/member';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ResetPasswordModalComponent } from '../_modals/reset-password-modal/reset-password-modal.component';
|
||||
@ -23,7 +22,6 @@ import { Router } from '@angular/router';
|
||||
export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
|
||||
members: Member[] = [];
|
||||
pendingInvites: Member[] = [];
|
||||
loggedInUsername = '';
|
||||
loadingMembers = false;
|
||||
|
||||
@ -47,8 +45,6 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadMembers();
|
||||
|
||||
this.loadPendingInvites();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@ -75,24 +71,6 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
loadPendingInvites() {
|
||||
this.pendingInvites = [];
|
||||
this.memberService.getPendingInvites().subscribe(members => {
|
||||
this.pendingInvites = members;
|
||||
// Show logged in user at the top of the list
|
||||
this.pendingInvites.sort((a: Member, b: Member) => {
|
||||
if (a.username === this.loggedInUsername) return 1;
|
||||
if (b.username === this.loggedInUsername) return 1;
|
||||
|
||||
const nameA = a.username.toUpperCase();
|
||||
const nameB = b.username.toUpperCase();
|
||||
if (nameA < nameB) return -1;
|
||||
if (nameA > nameB) return 1;
|
||||
return 0;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
canEditMember(member: Member): boolean {
|
||||
return this.loggedInUsername !== member.username;
|
||||
}
|
||||
@ -111,7 +89,6 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
this.memberService.deleteMember(member.username).subscribe(() => {
|
||||
setTimeout(() => {
|
||||
this.loadMembers();
|
||||
this.loadPendingInvites();
|
||||
this.toastr.success(member.username + ' has been deleted.');
|
||||
}, 30); // SetTimeout because I've noticed this can run super fast and not give enough time for data to flush
|
||||
});
|
||||
@ -121,10 +98,12 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
inviteUser() {
|
||||
const modalRef = this.modalService.open(InviteUserComponent, {size: 'lg'});
|
||||
modalRef.closed.subscribe((successful: boolean) => {
|
||||
this.loadPendingInvites();
|
||||
this.loadMembers();
|
||||
});
|
||||
}
|
||||
|
||||
log(o: any) {console.log(o)}
|
||||
|
||||
resendEmail(member: Member) {
|
||||
this.serverService.isServerAccessible().subscribe(canAccess => {
|
||||
this.accountService.resendConfirmationEmail(member.id).subscribe(async (email) => {
|
||||
|
@ -5,9 +5,17 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-template #useLink>
|
||||
<ng-container *ngIf="external; else internal">
|
||||
<a class="side-nav-item" [href]="link" [ngClass]="{'closed': (navService.sideNavCollapsed$ | async), 'active': highlighted}" rel="noopener noreferrer" target="_blank">
|
||||
<ng-container [ngTemplateOutlet]="inner"></ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-template #internal>
|
||||
<a class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': (navService.sideNavCollapsed$ | async), 'active': highlighted}" [routerLink]="link">
|
||||
<ng-container [ngTemplateOutlet]="inner"></ng-container>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
|
||||
|
||||
|
@ -25,6 +25,10 @@ export class SideNavItemComponent implements OnInit, OnDestroy {
|
||||
* If a link should be generated when clicked. By default (undefined), no link will be generated
|
||||
*/
|
||||
@Input() link: string | undefined;
|
||||
/**
|
||||
* If external, link will be used as full href and rel will be applied
|
||||
*/
|
||||
@Input() external: boolean = false;
|
||||
|
||||
@Input() comparisonMethod: 'startsWith' | 'equals' = 'equals';
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
</app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-bookmark" title="Bookmarks" link="/bookmarks/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-regular fa-rectangle-list" title="All Series" link="/all-series/" *ngIf="libraries.length > 0"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-heart" title="Donate" link="https://opencollective.com/kavita" [external]="true"></app-side-nav-item>
|
||||
<div class="mb-2 mt-3 ms-2 me-2" *ngIf="libraries.length > 10 && (navService?.sideNavCollapsed$ | async) === false">
|
||||
<label for="filter" class="form-label visually-hidden">Filter</label>
|
||||
<div class="form-group">
|
||||
|
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.1.22"
|
||||
"version": "0.7.1.23"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user