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:
Joe Milazzo 2023-03-16 19:03:56 -05:00 committed by GitHub
parent 93bd7d7c19
commit d070da2834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1940 additions and 177 deletions

View File

@ -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)

File diff suppressed because it is too large Load Diff

View 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);
});
}
}
}

View File

@ -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")

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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; }
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -274,7 +274,6 @@ public class Startup
app.UseMiddleware<ExceptionMiddleware>();
app.UseMiddleware<SecurityEventMiddleware>();
if (env.IsDevelopment())
{

View File

@ -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; }

View File

@ -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});
}

View File

@ -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);
});

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +1,6 @@
.presence {
font-size: 12px;
color: var(--primary-color);
}
.user-info > div {

View File

@ -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) => {

View File

@ -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>

View File

@ -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';

View File

@ -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">

View File

@ -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": [
{