- add Login

This commit is contained in:
Roland 2026-05-21 11:04:42 +02:00
parent 9b8ac0ef24
commit ca62eafaad
23 changed files with 1279 additions and 284 deletions

View file

@ -1,13 +1,13 @@
## WORKSPACE EXECUTIVE SUMMARY (YAML)
# Generated: 2026-05-20T06:11:22.0417511+02:00
# Generated: 2026-05-21T10:41:25.5071284+02:00
# Root: E:\ollama_net\ollama\AuthorBuddy.Web
Architecture:
Type: Backend (C#/.NET)
Scale: 34 relevant source files
Scale: 38 relevant source files
Components: 0
Controllers: 0
Models: 8
Services: 14
Services: 16
---
- File: AuthorBuddy.Web.csproj
Preview: "<Project Sdk='Microsoft.NET.Sdk.Web'> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>en..."
@ -18,89 +18,70 @@ Architecture:
- File: AuthorBuddy.Web.slnx
Preview: "<Solution> <Project Path='AuthorBuddy.Web.csproj' /> </Solution> "
- File: Program.cs
Preview: "using AuthorBuddy.Web.Components; using AuthorBuddy.Web.Data; using AuthorBuddy.Web.Models; using AuthorBuddy.Web.Services; using LiteDB; var builder ..."
Preview: "using AuthorBuddy.Web.Components; using AuthorBuddy.Web.Models; using AuthorBuddy.Web.Services; using LiteDB; using Microsoft.AspNetCore.Authenticatio..."
- File: Data\DocumentMetaEntity.cs
Symbols: [DocumentMetaEntity]
Preview: "namespace AuthorBuddy.Web.Data; public class DocumentMetaEntity { public int Id { get; set; } public int? ProjectId { get; set; } public string ..."
- File: Data\DocumentVector.cs
Symbols: [DocumentVector]
Preview: "namespace AuthorBuddy.Web.Data; public class DocumentVector { public int Id { get; set; } public int DocumentMetaId { get; set; } public int Pro..."
- File: Data\ProjectEntity.cs
Symbols: [ProjectEntity]
Preview: "namespace AuthorBuddy.Web.Data; public class ProjectEntity { public int Id { get; set; } public string Name { get; set; } = string.Empty; public..."
- File: Data\SettingEntity.cs
Symbols: [SettingEntity]
Preview: "using LiteDB; namespace AuthorBuddy.Web.Data; public class SettingEntity { public int Id { get; set; } public string Key { get; set; } = string.Em..."
- File: Data\UserEntity.cs
Symbols: [UserEntity]
Preview: "namespace AuthorBuddy.Web.Data; public class UserEntity { public int Id { get; set; } public string Name { get; set; } = string.Empty; public bo..."
- File: Models\Chapter.cs
Symbols: [Chapter]
Preview: "namespace AuthorBuddy.Web.Models; public class Chapter { public string Title { get; set; } = string.Empty; public string FilePath { get; set; } = ..."
- File: Models\Constants.cs
Symbols: [Constants]
Preview: "namespace AuthorBuddy.Web.Models; public static class Constants { public static readonly string[] SubFolders = { '00_Drafts', '01_Fakten/Per..."
- File: Models\FileTreeItem.cs
Symbols: [FileTreeItem]
Preview: "namespace AuthorBuddy.Web.Models; public class FileTreeItem { public string Name { get; set; } = string.Empty; public string FullPath { get; set; ..."
- File: Models\LLM_Model.cs
Symbols: [LLM_Model, LLM_Model]
Preview: "using System.Text.Json; namespace AuthorBuddy.Web.Models { public class LLM_Model { public LLM_Model(JsonElement model) { if (mode..."
- File: Models\ModelStatus.cs
Symbols: [ModelStatus]
Preview: " //namespace AuthorBuddy.Web.Models; //public class ModelStatus //{ // public string Status { get; private set; } = 'Unknown'; // public double Prog..."
- File: Models\OllamaSettings.cs
Symbols: [OllamaSettings]
Preview: "namespace AuthorBuddy.Web.Models; public class OllamaSettings { public string StyleModel { get; set; } = 'gemma4:e4b'; public string EmbedModel { ..."
- File: Models\Project.cs
Symbols: [Project]
Preview: "namespace AuthorBuddy.Web.Models; public class Project { public int Id { get; internal set; } public string Name { get; internal set; } = string.E..."
- File: Models\WikipediaResult.cs
Symbols: [WikipediaResult]
Preview: "namespace AuthorBuddy.Web.Models; public class WikipediaResult { public string Title { get; set; } = string.Empty; public string Snippet { get; se..."
- File: Properties\launchSettings.json
Preview: "{ '$schema': 'https://json.schemastore.org/launchsettings.json', 'profiles': { 'http': { 'commandName': 'Project', 'dotnetRunMessages': ..."
- File: Services\AuthService.cs
Preview: "using AuthorBuddy.Web.Data; using LiteDB; namespace AuthorBuddy.Web.Services; public class AuthService : IAuthService { private readonly ILiteDataba..."
- File: Services\CustomAuthStateProvider.cs
Preview: "using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; namespace AuthorBuddy.Web.Services; public class CustomAuthStatePro..."
- File: Services\FileScannerService.cs
Symbols: [FileScannerService]
Preview: "using AuthorBuddy.Web.Data; using LiteDB; namespace AuthorBuddy.Web.Services; public class FileScannerService : IFileScannerService { private readon..."
- File: Services\IAuthService.cs
Preview: "using AuthorBuddy.Web.Data; namespace AuthorBuddy.Web.Services; public interface IAuthService { Task<bool> LoginAsync(string username, string passwo..."
- File: Services\IFileScannerService.cs
Symbols: [IFileScannerService]
Preview: "namespace AuthorBuddy.Web.Services; public interface IFileScannerService { Task ScanProjectFolderAsync(int projectId, string rootPath, IProgress<str..."
- File: Services\IOllamaService.cs
Symbols: [IOllamaService]
Preview: " using AuthorBuddy.Web.Models; namespace AuthorBuddy.Web.Services; public interface IOllamaService { Task<IEnumerable<LLM_Model>> GetLocalModelsAsyn..."
- File: Services\IProjectService.cs
Symbols: [IProjectService]
Preview: "using AuthorBuddy.Web.Models; namespace AuthorBuddy.Web.Services; public interface IProjectService { Task<int> CreateNewProjectAsync(string projectN..."
- File: Services\IRagService.cs
Symbols: [IRagServi, RagSearchResult]
Preview: "namespace AuthorBuddy.Web.Services; public class RagSearchResult { public string Text { get; set; } = string.Empty; public string FilePath { get; ..."
- File: Services\ISettingsService.cs
Symbols: [ISettingsService]
Preview: "using AuthorBuddy.Web.Data; namespace AuthorBuddy.Web.Services; public interface ISettingsService { Task<string?> GetOllamaUrlAsync(); Task<string..."
- File: Services\IThemeService.cs
Symbols: [IThemeService]
Preview: "namespace AuthorBuddy.Web.Services; public interface IThemeService { Task ApplyTheme(string theme); } "
- File: Services\IWikipediaService.cs
Symbols: [IWikipediaService]
Preview: "using AuthorBuddy.Web.Models; namespace AuthorBuddy.Web.Services; public interface IWikipediaService { Task<List<WikipediaResult>> SearchAsync(strin..."
- File: Services\OllamaService.cs
Symbols: [OllamaService]
Preview: "using AuthorBuddy.Web.Models; using System.Text.Json; using static System.Net.Mime.MediaTypeNames; namespace AuthorBuddy.Web.Services; public class Ol..."
- File: Services\ProjectService.cs
Symbols: [ProjectService]
Preview: "using AuthorBuddy.Web.Data; using AuthorBuddy.Web.Models; using LiteDB; namespace AuthorBuddy.Web.Services; public class ProjectService : IProjectServ..."
- File: Services\RagService.cs
Symbols: [RagService]
Preview: "using AuthorBuddy.Web.Data; using LiteDB; using System.Security.Cryptography; using System.Text; namespace AuthorBuddy.Web.Services; public class RagS..."
- File: Services\SettingsService.cs
Symbols: [SettingsService]
Preview: "using AuthorBuddy.Web.Data; using AuthorBuddy.Web.Models; using LiteDB; namespace AuthorBuddy.Web.Services; public class SettingsService : ISettingsSe..."
- File: Services\ThemeService.cs
Symbols: [ThemeService, ThemeService]
Preview: "using Microsoft.JSInterop; namespace AuthorBuddy.Web.Services; public class ThemeService : IThemeService { private readonly IJSRuntime _js; public..."
- File: Services\TotpHelper.cs
Preview: "using System.Security.Cryptography; namespace AuthorBuddy.Web.Services; public static class TotpHelper { private const int TotpSize = 6; private c..."
- File: Services\WikipediaService.cs
Symbols: [WikipediaService]
Preview: "using AuthorBuddy.Web.Models; using System.Text.Json; using System.Web; namespace AuthorBuddy.Web.Services; public class WikipediaService : IWikipedia..."
- File: Components\Layout\ReconnectModal.razor.js
Preview: "// Set up event handlers const reconnectModal = document.getElementById('components-reconnect-modal'); reconnectModal.addEventListener('components-rec..."

View file

@ -2,6 +2,18 @@
"Version": 1,
"WorkspaceRootPath": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\totphelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\totphelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
@ -26,18 +38,10 @@
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\settingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\settingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\isettingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\isettingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\projectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\projectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
@ -126,7 +130,7 @@
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 2,
"SelectedChildIndex": 3,
"Children": [
{
"$type": "Bookmark",
@ -138,20 +142,106 @@
},
{
"$type": "Document",
"DocumentIndex": 0,
"DocumentIndex": 3,
"Title": "OllamaAssistant.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
"RelativeDocumentMoniker": "Components\\Pages\\OllamaAssistant.razor",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
"RelativeToolTip": "Components\\Pages\\OllamaAssistant.razor",
"ViewState": "AgIAAAkAAAAAAAAAAAAAABAAAAARAAAAAAAAAA==",
"ViewState": "AgIAAAkAAAAAAAAAAAAAABEAAAARAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-20T05:14:39.037Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "MainLayout.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor*",
"RelativeToolTip": "Components\\Layout\\MainLayout.razor*",
"ViewState": "AgIAAMkAAAAAAAAAAAAQwOcAAAARAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:04:36.444Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 6,
"Title": "OllamaSettings.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeDocumentMoniker": "Models\\OllamaSettings.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeToolTip": "Models\\OllamaSettings.cs",
"ViewState": "AgIAAAYAAAAAAAAAAAAkwBQAAAASAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T13:39:46.179Z"
},
{
"$type": "Document",
"DocumentIndex": 17,
"Title": "AppDbContext.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeDocumentMoniker": "Data\\AppDbContext.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeToolTip": "Data\\AppDbContext.cs",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-14T10:50:13.284Z"
},
{
"$type": "Document",
"DocumentIndex": 11,
"Title": "FileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeDocumentMoniker": "Services\\FileScannerService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeToolTip": "Services\\FileScannerService.cs",
"ViewState": "AgIAABIAAAAAAAAAAAAQwCUAAAAWAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T06:02:53.46Z"
},
{
"$type": "Document",
"DocumentIndex": 7,
"Title": "IOllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeDocumentMoniker": "Services\\IOllamaService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeToolTip": "Services\\IOllamaService.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAqwAkAAAAPAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T07:20:19.212Z"
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "Program.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
"RelativeDocumentMoniker": "Program.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs*",
"RelativeToolTip": "Program.cs*",
"ViewState": "AgIAABgAAAAAAAAAAAAAADAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-20T04:07:21.54Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 2,
"Title": "TotpHelper.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
"RelativeDocumentMoniker": "Services\\TotpHelper.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
"RelativeToolTip": "Services\\TotpHelper.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-21T08:20:49.461Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 12,
"Title": "Project.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\Project.cs",
"RelativeDocumentMoniker": "Models\\Project.cs",
@ -161,72 +251,9 @@
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-15T15:57:10.2Z"
},
{
"$type": "Document",
"DocumentIndex": 6,
"Title": "MainLayout.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
"RelativeToolTip": "Components\\Layout\\MainLayout.razor",
"ViewState": "AgIAALYAAAAAAAAAAAApwNMAAABAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:04:36.444Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 3,
"Title": "OllamaSettings.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeDocumentMoniker": "Models\\OllamaSettings.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeToolTip": "Models\\OllamaSettings.cs",
"ViewState": "AgIAAAYAAAAAAAAAAAAkwBQAAAASAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T13:39:46.179Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 16,
"Title": "AppDbContext.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeDocumentMoniker": "Data\\AppDbContext.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeToolTip": "Data\\AppDbContext.cs",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-14T10:50:13.284Z"
},
{
"$type": "Document",
"DocumentIndex": 10,
"Title": "FileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeDocumentMoniker": "Services\\FileScannerService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeToolTip": "Services\\FileScannerService.cs",
"ViewState": "AgIAABIAAAAAAAAAAAAQwCUAAAAWAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T06:02:53.46Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 4,
"Title": "IOllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeDocumentMoniker": "Services\\IOllamaService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeToolTip": "Services\\IOllamaService.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAqwAkAAAAPAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T07:20:19.212Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "Settings.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Settings.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Settings.razor",
@ -234,12 +261,11 @@
"RelativeToolTip": "Components\\Pages\\Settings.razor",
"ViewState": "AgIAAAwAAAAAAAAAAAAAAIYAAAAlAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:43:26.6Z",
"EditorCaption": ""
"WhenOpened": "2026-05-15T16:43:26.6Z"
},
{
"$type": "Document",
"DocumentIndex": 5,
"DocumentIndex": 8,
"Title": "SettingsService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\SettingsService.cs",
"RelativeDocumentMoniker": "Services\\SettingsService.cs",
@ -247,12 +273,11 @@
"RelativeToolTip": "Services\\SettingsService.cs",
"ViewState": "AgIAAEUBAAAAAAAAAAAQwFwBAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T06:05:12.928Z",
"EditorCaption": ""
"WhenOpened": "2026-05-16T06:05:12.928Z"
},
{
"$type": "Document",
"DocumentIndex": 2,
"DocumentIndex": 5,
"Title": "OllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeDocumentMoniker": "Services\\OllamaService.cs",
@ -260,12 +285,11 @@
"RelativeToolTip": "Services\\OllamaService.cs",
"ViewState": "AgIAAFkAAAAAAAAAAAAqwGsAAAAeAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T06:43:46.404Z",
"EditorCaption": ""
"WhenOpened": "2026-05-16T06:43:46.404Z"
},
{
"$type": "Document",
"DocumentIndex": 12,
"DocumentIndex": 13,
"Title": "RagService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\RagService.cs",
"RelativeDocumentMoniker": "Services\\RagService.cs",
@ -277,7 +301,7 @@
},
{
"$type": "Document",
"DocumentIndex": 7,
"DocumentIndex": 9,
"Title": "ISettingsService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ISettingsService.cs",
"RelativeDocumentMoniker": "Services\\ISettingsService.cs",
@ -285,25 +309,11 @@
"RelativeToolTip": "Services\\ISettingsService.cs",
"ViewState": "AgIAABoAAAAAAAAAAAA8wCoAAAAPAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T13:21:26.951Z",
"EditorCaption": ""
"WhenOpened": "2026-05-16T13:21:26.951Z"
},
{
"$type": "Document",
"DocumentIndex": 8,
"Title": "Program.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
"RelativeDocumentMoniker": "Program.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
"RelativeToolTip": "Program.cs",
"ViewState": "AgIAAAMAAAAAAAAAAAApwBoAAAANAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-20T04:07:21.54Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 13,
"DocumentIndex": 14,
"Title": "IRagService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IRagService.cs",
"RelativeDocumentMoniker": "Services\\IRagService.cs",
@ -315,7 +325,7 @@
},
{
"$type": "Document",
"DocumentIndex": 9,
"DocumentIndex": 10,
"Title": "ProjectService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ProjectService.cs",
"RelativeDocumentMoniker": "Services\\ProjectService.cs",
@ -323,12 +333,11 @@
"RelativeToolTip": "Services\\ProjectService.cs",
"ViewState": "AgIAADQAAAAAAAAAAAAqwEYAAAAWAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-15T15:56:55.229Z",
"EditorCaption": ""
"WhenOpened": "2026-05-15T15:56:55.229Z"
},
{
"$type": "Document",
"DocumentIndex": 15,
"DocumentIndex": 16,
"Title": "NavMenu.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\NavMenu.razor",
"RelativeDocumentMoniker": "Components\\Layout\\NavMenu.razor",
@ -340,7 +349,7 @@
},
{
"$type": "Document",
"DocumentIndex": 14,
"DocumentIndex": 15,
"Title": "Editor.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Editor.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Editor.razor",
@ -353,7 +362,7 @@
},
{
"$type": "Document",
"DocumentIndex": 17,
"DocumentIndex": 18,
"Title": "ThemeService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ThemeService.cs",
"RelativeDocumentMoniker": "Services\\ThemeService.cs",
@ -365,7 +374,7 @@
},
{
"$type": "Document",
"DocumentIndex": 18,
"DocumentIndex": 19,
"Title": "App.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\App.razor",
"RelativeDocumentMoniker": "Components\\App.razor",
@ -377,7 +386,7 @@
},
{
"$type": "Document",
"DocumentIndex": 21,
"DocumentIndex": 22,
"Title": "Models.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Models.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Models.razor",
@ -389,7 +398,7 @@
},
{
"$type": "Document",
"DocumentIndex": 19,
"DocumentIndex": 20,
"Title": "LLM_Model.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\LLM_Model.cs",
"RelativeDocumentMoniker": "Models\\LLM_Model.cs",
@ -401,7 +410,7 @@
},
{
"$type": "Document",
"DocumentIndex": 20,
"DocumentIndex": 21,
"Title": "ModelStatus.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\ModelStatus.cs",
"RelativeDocumentMoniker": "Models\\ModelStatus.cs",
@ -413,7 +422,7 @@
},
{
"$type": "Document",
"DocumentIndex": 24,
"DocumentIndex": 25,
"Title": "MainLayout.razor.css",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor.css",
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor.css",
@ -425,7 +434,7 @@
},
{
"$type": "Document",
"DocumentIndex": 22,
"DocumentIndex": 23,
"Title": "IFileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IFileScannerService.cs",
"RelativeDocumentMoniker": "Services\\IFileScannerService.cs",
@ -437,7 +446,7 @@
},
{
"$type": "Document",
"DocumentIndex": 23,
"DocumentIndex": 24,
"Title": "bootstrap-utilities.css",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
"RelativeDocumentMoniker": "wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
@ -449,7 +458,7 @@
},
{
"$type": "Document",
"DocumentIndex": 26,
"DocumentIndex": 27,
"Title": "ProjectEntity.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\ProjectEntity.cs",
"RelativeDocumentMoniker": "Data\\ProjectEntity.cs",
@ -461,7 +470,7 @@
},
{
"$type": "Document",
"DocumentIndex": 25,
"DocumentIndex": 26,
"Title": "IProjectService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IProjectService.cs",
"RelativeDocumentMoniker": "Services\\IProjectService.cs",
@ -473,7 +482,7 @@
},
{
"$type": "Document",
"DocumentIndex": 27,
"DocumentIndex": 28,
"Title": "AuthorBuddy.Web.csproj",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
"RelativeDocumentMoniker": "AuthorBuddy.Web.csproj",
@ -485,7 +494,7 @@
},
{
"$type": "Document",
"DocumentIndex": 28,
"DocumentIndex": 29,
"Title": "appsettings.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
"RelativeDocumentMoniker": "appsettings.json",

View file

@ -2,14 +2,26 @@
"Version": 1,
"WorkspaceRootPath": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\login.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\login.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\totphelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\totphelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\settings.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\settings.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
@ -34,10 +46,6 @@
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\isettingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\isettingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\projectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\projectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
@ -126,7 +134,7 @@
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 2,
"SelectedChildIndex": 3,
"Children": [
{
"$type": "Bookmark",
@ -136,22 +144,121 @@
"$type": "Bookmark",
"Name": "RST:0:0:{b3f9f954-0ca5-0091-76b4-34a47f44c954}"
},
{
"$type": "Document",
"DocumentIndex": 2,
"Title": "Login.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Login.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Login.razor",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Login.razor",
"RelativeToolTip": "Components\\Pages\\Login.razor",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-21T08:47:41.996Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "MainLayout.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
"RelativeToolTip": "Components\\Layout\\MainLayout.razor",
"ViewState": "AgIAAI0AAAAAAAAAAAAAAHIBAAAgAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:04:36.444Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "Program.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
"RelativeDocumentMoniker": "Program.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
"RelativeToolTip": "Program.cs",
"ViewState": "AgIAAAsAAAAAAAAAAAApwB8AAAAeAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-20T04:07:21.54Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 7,
"Title": "OllamaSettings.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeDocumentMoniker": "Models\\OllamaSettings.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeToolTip": "Models\\OllamaSettings.cs",
"ViewState": "AgIAAAYAAAAAAAAAAAAkwBQAAAASAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T13:39:46.179Z"
},
{
"$type": "Document",
"DocumentIndex": 18,
"Title": "AppDbContext.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeDocumentMoniker": "Data\\AppDbContext.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeToolTip": "Data\\AppDbContext.cs",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-14T10:50:13.284Z"
},
{
"$type": "Document",
"DocumentIndex": 12,
"Title": "FileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeDocumentMoniker": "Services\\FileScannerService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeToolTip": "Services\\FileScannerService.cs",
"ViewState": "AgIAABIAAAAAAAAAAAAQwCUAAAAWAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T06:02:53.46Z"
},
{
"$type": "Document",
"DocumentIndex": 8,
"Title": "IOllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeDocumentMoniker": "Services\\IOllamaService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeToolTip": "Services\\IOllamaService.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAqwAkAAAAPAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T07:20:19.212Z"
},
{
"$type": "Document",
"DocumentIndex": 4,
"Title": "OllamaAssistant.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
"RelativeDocumentMoniker": "Components\\Pages\\OllamaAssistant.razor",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor*",
"RelativeToolTip": "Components\\Pages\\OllamaAssistant.razor*",
"ViewState": "AgIAAAkAAAAAAAAAAAAAABEAAABsAAAAAAAAAA==",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
"RelativeToolTip": "Components\\Pages\\OllamaAssistant.razor",
"ViewState": "AgIAAAkAAAAAAAAAAAAAABEAAAARAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-20T05:14:39.037Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 11,
"DocumentIndex": 3,
"Title": "TotpHelper.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
"RelativeDocumentMoniker": "Services\\TotpHelper.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
"RelativeToolTip": "Services\\TotpHelper.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-21T08:20:49.461Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 13,
"Title": "Project.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\Project.cs",
"RelativeDocumentMoniker": "Models\\Project.cs",
@ -161,72 +268,9 @@
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-15T15:57:10.2Z"
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "MainLayout.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
"RelativeToolTip": "Components\\Layout\\MainLayout.razor",
"ViewState": "AgIAAH8AAAAAAAAAAAApwJMAAAAyAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:04:36.444Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 4,
"Title": "OllamaSettings.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeDocumentMoniker": "Models\\OllamaSettings.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeToolTip": "Models\\OllamaSettings.cs",
"ViewState": "AgIAAAYAAAAAAAAAAAAkwBQAAAASAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T13:39:46.179Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 16,
"Title": "AppDbContext.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeDocumentMoniker": "Data\\AppDbContext.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeToolTip": "Data\\AppDbContext.cs",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-14T10:50:13.284Z"
},
{
"$type": "Document",
"DocumentIndex": 10,
"Title": "FileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeDocumentMoniker": "Services\\FileScannerService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeToolTip": "Services\\FileScannerService.cs",
"ViewState": "AgIAABIAAAAAAAAAAAAQwCUAAAAWAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T06:02:53.46Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 5,
"Title": "IOllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeDocumentMoniker": "Services\\IOllamaService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeToolTip": "Services\\IOllamaService.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAqwAkAAAAPAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T07:20:19.212Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 2,
"Title": "Settings.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Settings.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Settings.razor",
@ -234,12 +278,11 @@
"RelativeToolTip": "Components\\Pages\\Settings.razor",
"ViewState": "AgIAAAwAAAAAAAAAAAAAAIYAAAAlAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:43:26.6Z",
"EditorCaption": ""
"WhenOpened": "2026-05-15T16:43:26.6Z"
},
{
"$type": "Document",
"DocumentIndex": 6,
"DocumentIndex": 9,
"Title": "SettingsService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\SettingsService.cs",
"RelativeDocumentMoniker": "Services\\SettingsService.cs",
@ -247,12 +290,11 @@
"RelativeToolTip": "Services\\SettingsService.cs",
"ViewState": "AgIAAEUBAAAAAAAAAAAQwFwBAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T06:05:12.928Z",
"EditorCaption": ""
"WhenOpened": "2026-05-16T06:05:12.928Z"
},
{
"$type": "Document",
"DocumentIndex": 3,
"DocumentIndex": 6,
"Title": "OllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeDocumentMoniker": "Services\\OllamaService.cs",
@ -260,12 +302,11 @@
"RelativeToolTip": "Services\\OllamaService.cs",
"ViewState": "AgIAAFkAAAAAAAAAAAAqwGsAAAAeAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T06:43:46.404Z",
"EditorCaption": ""
"WhenOpened": "2026-05-16T06:43:46.404Z"
},
{
"$type": "Document",
"DocumentIndex": 12,
"DocumentIndex": 14,
"Title": "RagService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\RagService.cs",
"RelativeDocumentMoniker": "Services\\RagService.cs",
@ -277,7 +318,7 @@
},
{
"$type": "Document",
"DocumentIndex": 7,
"DocumentIndex": 10,
"Title": "ISettingsService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ISettingsService.cs",
"RelativeDocumentMoniker": "Services\\ISettingsService.cs",
@ -285,25 +326,11 @@
"RelativeToolTip": "Services\\ISettingsService.cs",
"ViewState": "AgIAABoAAAAAAAAAAAA8wCoAAAAPAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-16T13:21:26.951Z",
"EditorCaption": ""
"WhenOpened": "2026-05-16T13:21:26.951Z"
},
{
"$type": "Document",
"DocumentIndex": 8,
"Title": "Program.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
"RelativeDocumentMoniker": "Program.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
"RelativeToolTip": "Program.cs",
"ViewState": "AgIAAAMAAAAAAAAAAAApwBoAAAANAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-20T04:07:21.54Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 13,
"DocumentIndex": 15,
"Title": "IRagService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IRagService.cs",
"RelativeDocumentMoniker": "Services\\IRagService.cs",
@ -315,7 +342,7 @@
},
{
"$type": "Document",
"DocumentIndex": 9,
"DocumentIndex": 11,
"Title": "ProjectService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ProjectService.cs",
"RelativeDocumentMoniker": "Services\\ProjectService.cs",
@ -323,12 +350,11 @@
"RelativeToolTip": "Services\\ProjectService.cs",
"ViewState": "AgIAADQAAAAAAAAAAAAqwEYAAAAWAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-15T15:56:55.229Z",
"EditorCaption": ""
"WhenOpened": "2026-05-15T15:56:55.229Z"
},
{
"$type": "Document",
"DocumentIndex": 15,
"DocumentIndex": 17,
"Title": "NavMenu.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\NavMenu.razor",
"RelativeDocumentMoniker": "Components\\Layout\\NavMenu.razor",
@ -340,7 +366,7 @@
},
{
"$type": "Document",
"DocumentIndex": 14,
"DocumentIndex": 16,
"Title": "Editor.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Editor.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Editor.razor",
@ -353,7 +379,7 @@
},
{
"$type": "Document",
"DocumentIndex": 17,
"DocumentIndex": 19,
"Title": "ThemeService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ThemeService.cs",
"RelativeDocumentMoniker": "Services\\ThemeService.cs",
@ -365,7 +391,7 @@
},
{
"$type": "Document",
"DocumentIndex": 18,
"DocumentIndex": 20,
"Title": "App.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\App.razor",
"RelativeDocumentMoniker": "Components\\App.razor",
@ -377,7 +403,7 @@
},
{
"$type": "Document",
"DocumentIndex": 21,
"DocumentIndex": 23,
"Title": "Models.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Models.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Models.razor",
@ -389,7 +415,7 @@
},
{
"$type": "Document",
"DocumentIndex": 19,
"DocumentIndex": 21,
"Title": "LLM_Model.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\LLM_Model.cs",
"RelativeDocumentMoniker": "Models\\LLM_Model.cs",
@ -401,7 +427,7 @@
},
{
"$type": "Document",
"DocumentIndex": 20,
"DocumentIndex": 22,
"Title": "ModelStatus.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\ModelStatus.cs",
"RelativeDocumentMoniker": "Models\\ModelStatus.cs",
@ -413,7 +439,7 @@
},
{
"$type": "Document",
"DocumentIndex": 24,
"DocumentIndex": 26,
"Title": "MainLayout.razor.css",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor.css",
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor.css",
@ -425,7 +451,7 @@
},
{
"$type": "Document",
"DocumentIndex": 22,
"DocumentIndex": 24,
"Title": "IFileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IFileScannerService.cs",
"RelativeDocumentMoniker": "Services\\IFileScannerService.cs",
@ -437,7 +463,7 @@
},
{
"$type": "Document",
"DocumentIndex": 23,
"DocumentIndex": 25,
"Title": "bootstrap-utilities.css",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
"RelativeDocumentMoniker": "wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
@ -449,7 +475,7 @@
},
{
"$type": "Document",
"DocumentIndex": 26,
"DocumentIndex": 28,
"Title": "ProjectEntity.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\ProjectEntity.cs",
"RelativeDocumentMoniker": "Data\\ProjectEntity.cs",
@ -461,7 +487,7 @@
},
{
"$type": "Document",
"DocumentIndex": 25,
"DocumentIndex": 27,
"Title": "IProjectService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IProjectService.cs",
"RelativeDocumentMoniker": "Services\\IProjectService.cs",
@ -473,7 +499,7 @@
},
{
"$type": "Document",
"DocumentIndex": 27,
"DocumentIndex": 29,
"Title": "AuthorBuddy.Web.csproj",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
"RelativeDocumentMoniker": "AuthorBuddy.Web.csproj",
@ -485,7 +511,7 @@
},
{
"$type": "Document",
"DocumentIndex": 28,
"DocumentIndex": 30,
"Title": "appsettings.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
"RelativeDocumentMoniker": "appsettings.json",

View file

@ -15,11 +15,11 @@
</head>
<body>
<CascadingAuthenticationState>
<Routes @rendermode="InteractiveServer" />
</CascadingAuthenticationState>
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>

View file

@ -4,9 +4,13 @@
@inject ISettingsService SettingsService
@inject IThemeService ThemeService
@inject OllamaSettings OllamaSettings
@inject IAuthService AuthService
@inject CustomAuthStateProvider AuthStateProvider
<div class="page">
<div class="main-content">
@if (_isAuthenticated)
{
<aside class="sidebar">
<div class="nav-section">
<button class="nav-section-header" @onclick='() => ToggleSection("file")'>
@ -150,7 +154,22 @@
</div>
</div>
}
@if (!string.IsNullOrEmpty(_username))
{
<div class="nav-section" style="border-top:1px solid var(--border-color);margin-top:8px;padding-top:4px;">
<div class="nav-section-header" style="cursor:default;">
<span style="font-size:11px;font-weight:normal;">@T("Angemeldet als:", "Logged in as:") @_username</span>
</div>
<div class="nav-section-body">
<button class="nav-item" @onclick="NavigateToSetup2FA">@T("2FA verwalten", "Manage 2FA")</button>
<button class="nav-item" @onclick="Logout">@T("Abmelden", "Logout")</button>
</div>
</div>
}
</aside>
}
<main class="detail-panel">
@Body
@ -169,6 +188,8 @@
@code {
private string _language = "de";
private string _username = string.Empty;
private bool _isAuthenticated;
private string? activeSection;
private bool showProjectList;
private List<Project>? projects;
@ -196,9 +217,13 @@
_language = await SettingsService.GetLanguageAsync() ?? "de";
statusText = T("Bereit", "Ready");
Navigation.LocationChanged += OnLocationChanged;
await LoadProjects();
await LoadProjectContext();
await AutoLoadLastProject();
}
private async Task LoadCurrentUser()
{
var user = await AuthService.GetCurrentUserAsync();
_username = user?.Name ?? string.Empty;
_isAuthenticated = user != null;
}
private async Task AutoLoadLastProject()
@ -221,6 +246,29 @@
var theme = OllamaSettings.Theme;
if (!string.IsNullOrEmpty(theme))
await ThemeService.ApplyTheme(theme);
// Auto-login runs AFTER circuit establishment (correct scoped services)
await LoadCurrentUser();
#if DEBUG
if (!_isAuthenticated && Program.DebugAutoLogin)
{
var users = await SettingsService.GetUsersAsync();
var first = users.FirstOrDefault();
if (first != null)
{
await AuthService.AdminBypassLoginAsync(first.Id);
AuthStateProvider.NotifyStateChanged();
await LoadCurrentUser();
}
}
#endif
if (_isAuthenticated)
{
await LoadProjects();
await LoadProjectContext();
await AutoLoadLastProject();
}
StateHasChanged();
}
}
@ -230,9 +278,13 @@
}
private async void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
{
await LoadCurrentUser();
if (_isAuthenticated)
{
await LoadProjects();
await LoadProjectContext();
}
StateHasChanged();
}
@ -304,6 +356,21 @@
Navigation.NavigateTo(path);
}
private void NavigateToSetup2FA()
{
activeSection = null;
Navigation.NavigateTo("/setup-2fa");
}
private async Task Logout()
{
activeSection = null;
await AuthService.LogoutAsync();
_username = string.Empty;
AuthStateProvider.NotifyStateChanged();
Navigation.NavigateTo("/login", true);
}
private async Task SelectProject(Project project)
{
activeSection = "files";

View file

@ -10,6 +10,7 @@
@implements IDisposable
@rendermode InteractiveServer
@attribute [Authorize]
<PageTitle>@T("Editor - Author Buddy", "Editor - Author Buddy")</PageTitle>

View file

@ -1,4 +1,5 @@
@page "/"
@attribute [Authorize]
@inject IOllamaService OllamaService
@inject IProjectService ProjectService
@inject ISettingsService SettingsService

View file

@ -0,0 +1,166 @@
@page "/login"
@inject IAuthService AuthService
@inject CustomAuthStateProvider AuthStateProvider
@inject NavigationManager Navigation
@inject ISettingsService SettingsService
@rendermode InteractiveServer
<PageTitle>@T("Anmelden - Author Buddy", "Login - Author Buddy")</PageTitle>
<div class="auth-page">
<div class="auth-card">
<h2>@T("Anmelden", "Login")</h2>
@if (!show2FA)
{
<div class="form-group">
<label for="username">@T("Benutzername:", "Username:")</label>
<input id="username" @bind="username" @onkeydown="HandleKeyDown" />
</div>
<div class="form-group">
<label for="password">@T("Passwort:", "Password:")</label>
<input id="password" type="password" @bind="password" @onkeydown="HandleKeyDown" />
</div>
<div class="auth-actions">
<button @onclick="DoLogin" disabled="@isBusy">@T("Anmelden", "Login")</button>
<a href="/register">@T("Registrieren", "Register")</a>
</div>
}
else
{
<p>@T("Gib den 6-stelligen Code aus deiner Authenticator-App ein:", "Enter the 6-digit code from your authenticator app:")</p>
<div class="form-group">
<label for="code">@T("2FA-Code:", "2FA code:")</label>
<input id="code" @bind="twoFactorCode" maxlength="6" @onkeydown="HandleKeyDown" />
</div>
<div class="auth-actions">
<button @onclick="Verify2FA" disabled="@isBusy">@T("Bestätigen", "Verify")</button>
</div>
}
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="auth-error">@errorMessage</div>
}
</div>
</div>
@code {
private string _language = "de";
private string username = string.Empty;
private string password = string.Empty;
private string twoFactorCode = string.Empty;
private string errorMessage = string.Empty;
private bool isBusy;
private bool show2FA;
private string T(string de, string en) => _language == "en" ? en : de;
protected override async Task OnInitializedAsync()
{
_language = await SettingsService.GetLanguageAsync() ?? "de";
if (AuthService.IsLoggedIn)
{
Navigation.NavigateTo("/");
return;
}
var users = await SettingsService.GetUsersAsync();
if (users.Count == 0)
{
Navigation.NavigateTo("/register");
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && AuthService.IsLoggedIn)
Navigation.NavigateTo("/");
}
private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
if (show2FA)
await Verify2FA();
else
await DoLogin();
}
}
private async Task DoLogin()
{
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
{
errorMessage = T("Bitte Benutzername und Passwort eingeben.", "Please enter username and password.");
return;
}
isBusy = true;
errorMessage = string.Empty;
try
{
var result = await AuthService.LoginAsync(username, password);
if (AuthService.Is2FARequired)
{
show2FA = true;
password = string.Empty;
}
else if (result)
{
AuthStateProvider.NotifyStateChanged();
Navigation.NavigateTo("/");
}
else
{
errorMessage = T("Benutzername oder Passwort falsch.", "Invalid username or password.");
}
}
catch (Exception ex)
{
errorMessage = $"{T("Fehler:", "Error:")} {ex.Message}";
}
finally
{
isBusy = false;
}
}
private async Task Verify2FA()
{
if (string.IsNullOrWhiteSpace(twoFactorCode) || twoFactorCode.Length != 6)
{
errorMessage = T("Bitte gib einen gültigen 6-stelligen Code ein.", "Please enter a valid 6-digit code.");
return;
}
isBusy = true;
errorMessage = string.Empty;
try
{
var result = await AuthService.Verify2FAAsync(twoFactorCode);
if (result)
{
AuthStateProvider.NotifyStateChanged();
Navigation.NavigateTo("/");
}
else
{
errorMessage = T("Ungültiger Code. Bitte versuche es erneut.", "Invalid code. Please try again.");
}
}
catch (Exception ex)
{
errorMessage = $"{T("Fehler:", "Error:")} {ex.Message}";
}
finally
{
isBusy = false;
}
}
}

View file

@ -1,4 +1,5 @@
@page "/models"
@attribute [Authorize]
@inject IOllamaService OllamaService
@inject ISettingsService SettingsService
@inject NavigationManager Navigation

View file

@ -1,4 +1,5 @@
@page "/assistant"
@attribute [Authorize]
@inject IOllamaService OllamaService
@inject ISettingsService SettingsService
@inject NavigationManager Navigation

View file

@ -0,0 +1,108 @@
@page "/register"
@inject IAuthService AuthService
@inject CustomAuthStateProvider AuthStateProvider
@inject NavigationManager Navigation
@inject ISettingsService SettingsService
@rendermode InteractiveServer
<PageTitle>@T("Registrieren - Author Buddy", "Register - Author Buddy")</PageTitle>
<div class="auth-page">
<div class="auth-card">
<h2>@T("Registrieren", "Register")</h2>
<div class="form-group">
<label for="username">@T("Benutzername:", "Username:")</label>
<input id="username" @bind="username" @onkeydown="HandleKeyDown" />
</div>
<div class="form-group">
<label for="password">@T("Passwort:", "Password:")</label>
<input id="password" type="password" @bind="password" @onkeydown="HandleKeyDown" />
</div>
<div class="form-group">
<label for="confirmPassword">@T("Passwort bestätigen:", "Confirm password:")</label>
<input id="confirmPassword" type="password" @bind="confirmPassword" @onkeydown="HandleKeyDown" />
</div>
<div class="auth-actions">
<button @onclick="DoRegister" disabled="@isBusy">@T("Registrieren", "Register")</button>
<a href="/login">@T("Zurück zum Login", "Back to login")</a>
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="auth-error">@errorMessage</div>
}
</div>
</div>
@code {
private string _language = "de";
private string username = string.Empty;
private string password = string.Empty;
private string confirmPassword = string.Empty;
private string errorMessage = string.Empty;
private bool isBusy;
private string T(string de, string en) => _language == "en" ? en : de;
protected override async Task OnInitializedAsync()
{
_language = await SettingsService.GetLanguageAsync() ?? "de";
}
private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter")
await DoRegister();
}
private async Task DoRegister()
{
if (string.IsNullOrWhiteSpace(username))
{
errorMessage = T("Bitte Benutzername eingeben.", "Please enter a username.");
return;
}
if (string.IsNullOrWhiteSpace(password))
{
errorMessage = T("Bitte Passwort eingeben.", "Please enter a password.");
return;
}
if (password != confirmPassword)
{
errorMessage = T("Passwörter stimmen nicht überein.", "Passwords do not match.");
return;
}
if (password.Length < 4)
{
errorMessage = T("Passwort muss mindestens 4 Zeichen lang sein.", "Password must be at least 4 characters long.");
return;
}
isBusy = true;
errorMessage = string.Empty;
try
{
var result = await AuthService.RegisterAsync(username, password);
if (result)
{
AuthStateProvider.NotifyStateChanged();
Navigation.NavigateTo("/");
}
else
{
errorMessage = T("Benutzername ist bereits vergeben.", "Username is already taken.");
}
}
catch (Exception ex)
{
errorMessage = $"{T("Fehler:", "Error:")} {ex.Message}";
}
finally
{
isBusy = false;
}
}
}

View file

@ -1,4 +1,5 @@
@page "/settings"
@attribute [Authorize]
@inject ISettingsService SettingsService
@inject IThemeService ThemeService
@inject IOllamaService OllamaService
@ -30,6 +31,10 @@
</div>
</div>
<div class="settings-section" style="margin-bottom:16px;">
<button class="btn btn-secondary" @onclick='() => Navigation.NavigateTo("/setup-2fa")'>@T("2FA einrichten", "Set up 2FA")</button>
</div>
<div class="settings-layout">
<div class="settings-column" >

View file

@ -0,0 +1,197 @@
@page "/setup-2fa"
@inject IAuthService AuthService
@inject NavigationManager Navigation
@inject ISettingsService SettingsService
@inject IJSRuntime JSRuntime
@rendermode InteractiveServer
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
<PageTitle>@T("2FA einrichten - Author Buddy", "2FA setup - Author Buddy")</PageTitle>
<div class="auth-page">
<div class="auth-card">
<h2>@T("Zwei-Faktor-Authentifizierung", "Two-Factor Authentication")</h2>
@if (isEnabled)
{
<div class="auth-success">
<p>@T("2FA ist aktiviert.", "2FA is enabled.")</p>
<button @onclick="Disable2FA" disabled="@isBusy">@T("2FA deaktivieren", "Disable 2FA")</button>
</div>
}
else if (showSecret)
{
<p>@T("Scanne den folgenden QR-Code mit deiner Authenticator-App (z.B. Google Authenticator, Microsoft Authenticator) oder gib den geheimen Schlüssel manuell ein:", "Scan the QR code below with your authenticator app (e.g. Google Authenticator, Microsoft Authenticator) or enter the secret key manually:")</p>
<div class="qr-container">
<img src="@qrCodeDataUri" alt="QR Code" />
</div>
<div class="form-group">
<label>@T("Geheimer Schlüssel:", "Secret key:")</label>
<div class="secret-display">
<code>@secret</code>
<button @onclick="CopySecret">@T("Kopieren", "Copy")</button>
</div>
</div>
<div class="form-group">
<label for="code">@T("Code zur Bestätigung eingeben:", "Enter verification code:")</label>
<input id="code" @bind="verificationCode" maxlength="6" @onkeydown="HandleKeyDown" />
</div>
<div class="auth-actions">
<button @onclick="Enable2FA" disabled="@isBusy || string.IsNullOrWhiteSpace(verificationCode)">@T("2FA aktivieren", "Enable 2FA")</button>
<button @onclick="Cancel">@T("Abbrechen", "Cancel")</button>
</div>
}
else
{
<p>@T("Richte die Zwei-Faktor-Authentifizierung ein, um dein Konto zusätzlich zu schützen.", "Set up two-factor authentication to add an extra layer of security to your account.")</p>
<div class="auth-actions">
<button @onclick="StartSetup">@T("2FA einrichten", "Set up 2FA")</button>
</div>
}
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="auth-error">@errorMessage</div>
}
</div>
</div>
@code {
private string _language = "de";
private string secret = string.Empty;
private string verificationCode = string.Empty;
private string errorMessage = string.Empty;
private string? qrCodeDataUri;
private bool showSecret;
private bool isEnabled;
private bool isBusy;
private string? username;
private string T(string de, string en) => _language == "en" ? en : de;
protected override async Task OnInitializedAsync()
{
_language = await SettingsService.GetLanguageAsync() ?? "de";
isEnabled = await AuthService.Is2FAEnabledAsync();
var user = await AuthService.GetCurrentUserAsync();
username = user?.Name;
}
private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter")
await Enable2FA();
}
private async Task StartSetup()
{
isBusy = true;
try
{
secret = await AuthService.Generate2FASecretAsync();
var uri = AuthService.Get2FAProvisioningUri(secret, username ?? "user");
qrCodeDataUri = await GenerateQRCodeDataUri(uri);
showSecret = true;
}
catch (Exception ex)
{
errorMessage = $"{T("Fehler:", "Error:")} {ex.Message}";
}
finally
{
isBusy = false;
}
}
private async Task Enable2FA()
{
if (string.IsNullOrWhiteSpace(verificationCode) || verificationCode.Length != 6)
{
errorMessage = T("Bitte gib einen gültigen 6-stelligen Code ein.", "Please enter a valid 6-digit code.");
return;
}
if (!TotpHelper.VerifyCode(secret, verificationCode))
{
errorMessage = T("Code ungültig. Bitte versuche es erneut.", "Invalid code. Please try again.");
return;
}
isBusy = true;
errorMessage = string.Empty;
try
{
var result = await AuthService.Enable2FAAsync(secret);
if (result)
{
isEnabled = true;
showSecret = false;
}
else
{
errorMessage = T("Fehler beim Aktivieren von 2FA.", "Failed to enable 2FA.");
}
}
catch (Exception ex)
{
errorMessage = $"{T("Fehler:", "Error:")} {ex.Message}";
}
finally
{
isBusy = false;
}
}
private async Task Disable2FA()
{
isBusy = true;
try
{
var result = await AuthService.Disable2FAAsync();
if (result)
{
isEnabled = false;
showSecret = false;
secret = string.Empty;
}
}
catch (Exception ex)
{
errorMessage = $"{T("Fehler:", "Error:")} {ex.Message}";
}
finally
{
isBusy = false;
}
}
private async Task<string?> GenerateQRCodeDataUri(string uri)
{
try
{
var encoded = System.Web.HttpUtility.UrlEncode(uri);
return $"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={encoded}";
}
catch
{
return null;
}
}
private async Task CopySecret()
{
if (!string.IsNullOrEmpty(secret))
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", secret);
}
private void Cancel()
{
Navigation.NavigateTo("/");
}
}

View file

@ -0,0 +1,14 @@
@inject NavigationManager Navigation
@inject AuthenticationStateProvider AuthStateProvider
@rendermode InteractiveServer
@code {
protected override async Task OnInitializedAsync()
{
var state = await AuthStateProvider.GetAuthenticationStateAsync();
if (state.User.Identity?.IsAuthenticated == true)
return;
Navigation.NavigateTo("/login");
}
}

View file

@ -1,6 +1,10 @@
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View file

@ -5,6 +5,8 @@
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.JSInterop
@using AuthorBuddy.Web
@using AuthorBuddy.Web.Components

View file

@ -5,4 +5,7 @@ public class UserEntity
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public bool IsCurrent { get; set; }
public string PasswordHash { get; set; } = string.Empty;
public string TwoFactorSecret { get; set; } = string.Empty;
public bool TwoFactorEnabled { get; set; }
}

View file

@ -1,8 +1,9 @@
using AuthorBuddy.Web.Components;
using AuthorBuddy.Web.Data;
using AuthorBuddy.Web.Models;
using AuthorBuddy.Web.Services;
using LiteDB;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Authorization;
var builder = WebApplication.CreateBuilder(args);
@ -20,6 +21,18 @@ builder.Services.AddScoped<IThemeService, ThemeService>();
builder.Services.AddSingleton<IProjectService, ProjectService>();
builder.Services.AddSingleton<IRagService, RagService>();
builder.Services.AddSingleton<IWikipediaService, WikipediaService>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<CustomAuthStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp =>
sp.GetRequiredService<CustomAuthStateProvider>());
builder.Services.AddAuthorizationCore();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/login";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromDays(30);
});
var app = builder.Build();
@ -30,6 +43,10 @@ if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseAntiforgery();
@ -38,3 +55,8 @@ app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
public partial class Program
{
public const bool DebugAutoLogin = true;
}

176
Services/AuthService.cs Normal file
View file

@ -0,0 +1,176 @@
using AuthorBuddy.Web.Data;
using LiteDB;
namespace AuthorBuddy.Web.Services;
public class AuthService : IAuthService
{
private readonly ILiteDatabase _db;
private readonly ISettingsService _settingsService;
private UserEntity? _currentUser;
private string? _pending2FASecret;
private bool _requires2FA;
public AuthService(ILiteDatabase db, ISettingsService settingsService)
{
_db = db;
_settingsService = settingsService;
}
public bool IsLoggedIn => _currentUser != null;
public bool Is2FARequired => _requires2FA;
public async Task<bool> LoginAsync(string username, string password)
{
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindOne(u => u.Name == username);
if (user == null || string.IsNullOrEmpty(user.PasswordHash))
return false;
if (!TotpHelper.VerifyPassword(password, user.PasswordHash))
return false;
if (user.TwoFactorEnabled)
{
_pending2FASecret = user.TwoFactorSecret;
_requires2FA = true;
return true;
}
await SetAuthenticatedUser(user);
return true;
}
public async Task LogoutAsync()
{
_currentUser = null;
_requires2FA = false;
_pending2FASecret = null;
await Task.CompletedTask;
}
public async Task AdminBypassLoginAsync(int userId)
{
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindById(userId);
if (user != null)
await SetAuthenticatedUser(user);
}
public async Task<bool> RegisterAsync(string username, string password)
{
var col = _db.GetCollection<UserEntity>("users");
var existing = col.FindOne(u => u.Name == username);
if (existing != null) return false;
var user = new UserEntity
{
Name = username,
PasswordHash = TotpHelper.HashPassword(password),
IsCurrent = true,
TwoFactorEnabled = false,
TwoFactorSecret = string.Empty
};
col.Insert(user);
await SetAuthenticatedUser(user);
return true;
}
public Task<UserEntity?> GetCurrentUserAsync()
{
return Task.FromResult(_currentUser);
}
public async Task<bool> Verify2FAAsync(string code)
{
if (string.IsNullOrEmpty(_pending2FASecret)) return false;
if (!TotpHelper.VerifyCode(_pending2FASecret, code))
return false;
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindOne(u => u.TwoFactorSecret == _pending2FASecret);
if (user == null) return false;
await SetAuthenticatedUser(user);
return true;
}
public Task<string> Generate2FASecretAsync()
{
return Task.FromResult(TotpHelper.GenerateSecret());
}
public string Get2FAProvisioningUri(string secret, string username)
{
return TotpHelper.GetProvisioningUri(secret, username);
}
public async Task<bool> Enable2FAAsync(string secret)
{
if (_currentUser == null) return false;
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindById(_currentUser.Id);
if (user == null) return false;
user.TwoFactorSecret = secret;
user.TwoFactorEnabled = true;
col.Update(user);
_currentUser.TwoFactorSecret = secret;
_currentUser.TwoFactorEnabled = true;
return true;
}
public async Task<bool> Disable2FAAsync()
{
if (_currentUser == null) return false;
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindById(_currentUser.Id);
if (user == null) return false;
user.TwoFactorSecret = string.Empty;
user.TwoFactorEnabled = false;
col.Update(user);
_currentUser.TwoFactorSecret = string.Empty;
_currentUser.TwoFactorEnabled = false;
return true;
}
public async Task<bool> Is2FAEnabledAsync()
{
if (_currentUser == null) return false;
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindById(_currentUser.Id);
return user?.TwoFactorEnabled ?? false;
}
public async Task<string?> GetCurrent2FASecretAsync()
{
if (_currentUser == null) return null;
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindById(_currentUser.Id);
return user?.TwoFactorSecret;
}
private async Task SetAuthenticatedUser(UserEntity user)
{
_currentUser = user;
_requires2FA = false;
_pending2FASecret = null;
_settingsService.CurrentProjectId = null;
_settingsService.CurrentFilePath = null;
var col = _db.GetCollection<UserEntity>("users");
var all = col.FindAll().ToList();
foreach (var u in all)
{
u.IsCurrent = u.Id == user.Id;
col.Update(u);
}
await _settingsService.InitializeAsync();
}
}

View file

@ -0,0 +1,36 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
namespace AuthorBuddy.Web.Services;
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly IAuthService _authService;
private AuthenticationState _anonymous;
public CustomAuthStateProvider(IAuthService authService)
{
_authService = authService;
_anonymous = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var user = await _authService.GetCurrentUserAsync();
if (user != null)
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
}, "custom");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
return _anonymous;
}
public void NotifyStateChanged()
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
}

21
Services/IAuthService.cs Normal file
View file

@ -0,0 +1,21 @@
using AuthorBuddy.Web.Data;
namespace AuthorBuddy.Web.Services;
public interface IAuthService
{
Task<bool> LoginAsync(string username, string password);
Task LogoutAsync();
Task<bool> RegisterAsync(string username, string password);
Task AdminBypassLoginAsync(int userId);
Task<UserEntity?> GetCurrentUserAsync();
bool IsLoggedIn { get; }
bool Is2FARequired { get; }
Task<bool> Verify2FAAsync(string code);
Task<string> Generate2FASecretAsync();
string Get2FAProvisioningUri(string secret, string username);
Task<bool> Enable2FAAsync(string secret);
Task<bool> Disable2FAAsync();
Task<bool> Is2FAEnabledAsync();
Task<string?> GetCurrent2FASecretAsync();
}

View file

@ -280,18 +280,14 @@ public class SettingsService : ISettingsService
var settings = _db.GetCollection<SettingEntity>("settings");
var userExists = users.FindAll().Any();
if (!userExists)
{
var defaultUser = new UserEntity { Name = "Standardbenutzer", IsCurrent = true };
users.Insert(defaultUser);
_currentUserId = defaultUser.Id;
}
else
if (userExists)
{
var current = users.FindOne(u => u.IsCurrent);
_currentUserId = current?.Id ?? users.FindAll().First().Id;
}
if (_currentUserId == null) return Task.CompletedTask;
var lang = _ollamaConfig.Language;
var entries = new (string Key, string Default)[]

158
Services/TotpHelper.cs Normal file
View file

@ -0,0 +1,158 @@
using System.Security.Cryptography;
namespace AuthorBuddy.Web.Services;
public static class TotpHelper
{
private const int TotpSize = 6;
private const int TotpStepSeconds = 30;
private const int WindowSize = 1;
public static string GenerateSecret()
{
var secret = RandomNumberGenerator.GetBytes(20);
return Base32Encode(secret);
}
public static string GetProvisioningUri(string secret, string username, string issuer = "AuthorBuddy")
{
return $"otpauth://totp/{Uri.EscapeDataString(issuer)}:{Uri.EscapeDataString(username)}" +
$"?secret={secret}&issuer={Uri.EscapeDataString(issuer)}&algorithm=SHA1&digits={TotpSize}&period={TotpStepSeconds}";
}
public static bool VerifyCode(string secret, string code)
{
if (string.IsNullOrWhiteSpace(code) || code.Length != TotpSize) return false;
if (!int.TryParse(code, out _)) return false;
var secretBytes = Base32Decode(secret);
var counter = GetCurrentCounter();
for (int i = -WindowSize; i <= WindowSize; i++)
{
var expected = GenerateTotp(secretBytes, counter + i);
if (expected == code) return true;
}
return false;
}
private static long GetCurrentCounter()
{
var unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return unix / TotpStepSeconds;
}
private static string GenerateTotp(byte[] secret, long counter)
{
var counterBytes = BitConverter.GetBytes(counter);
if (BitConverter.IsLittleEndian) Array.Reverse(counterBytes);
byte[] hash;
using (var hmac = new HMACSHA1(secret))
{
hash = hmac.ComputeHash(counterBytes);
}
int offset = hash[^1] & 0xf;
int binary = ((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
int otp = binary % (int)Math.Pow(10, TotpSize);
return otp.ToString($"D{TotpSize}");
}
private static string Base32Encode(byte[] data)
{
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
int bitCount = 0;
int currentByte = 0;
var result = new System.Text.StringBuilder();
foreach (var b in data)
{
currentByte = (currentByte << 8) | b;
bitCount += 8;
while (bitCount >= 5)
{
bitCount -= 5;
int index = (currentByte >> bitCount) & 0x1f;
result.Append(alphabet[index]);
}
}
if (bitCount > 0)
{
int index = (currentByte << (5 - bitCount)) & 0x1f;
result.Append(alphabet[index]);
}
return result.ToString();
}
private static byte[] Base32Decode(string base32)
{
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
base32 = base32.Trim().ToUpperInvariant()
.Replace('0', 'O').Replace('1', 'I').Replace('8', 'B');
int bitCount = 0;
int currentByte = 0;
var bytes = new System.Collections.Generic.List<byte>();
foreach (var c in base32)
{
int index = alphabet.IndexOf(c);
if (index < 0) continue;
currentByte = (currentByte << 5) | index;
bitCount += 5;
if (bitCount >= 8)
{
bitCount -= 8;
bytes.Add((byte)((currentByte >> bitCount) & 0xff));
}
}
return [.. bytes];
}
public static string HashPassword(string password)
{
var salt = RandomNumberGenerator.GetBytes(16);
var hash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
100_000,
HashAlgorithmName.SHA256,
32);
return $"{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}";
}
public static bool VerifyPassword(string password, string hashedPassword)
{
if (string.IsNullOrWhiteSpace(hashedPassword)) return false;
var parts = hashedPassword.Split('.');
if (parts.Length != 2) return false;
try
{
var salt = Convert.FromBase64String(parts[0]);
var storedHash = Convert.FromBase64String(parts[1]);
var computedHash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
100_000,
HashAlgorithmName.SHA256,
32);
return CryptographicOperations.FixedTimeEquals(storedHash, computedHash);
}
catch
{
return false;
}
}
}