Security (Phase 1): - Add authentication middleware on /graphql endpoint - Filter company queries by user access (prevent IDOR) - Add role-based authorization on mutations (owner/accountant) - Reduce API key cache TTL from 24h to 5 minutes - Hide exception details in production GraphQL errors - Fix RBAC in frontend companyStore (was hardcoded) Wiring broken features (Phase 2): - Wire Kassekladde submit/void/copy to GraphQL mutations - Wire Kontooversigt account creation to createAccount mutation - Wire Settings save to updateCompany mutation - Wire CreateFiscalYearModal and CloseFiscalYearWizard to mutations - Replace Momsindberetning mock data with real useVatReport query - Remove Dashboard hardcoded percentages and fake VAT deadline - Fix Kreditnotaer invoice selector to use real data - Fix mutation retry from 1 to 0 (prevent duplicate operations) Accounting compliance (Phase 3): - Add balanced entry validation (debit==credit) in JournalEntryDraftAggregate - Add fiscal year boundary enforcement (status, date range checks) - Add PostedAt timestamp to posted events (Bogføringsloven §7) - Add account number uniqueness check within company - Add fiscal year overlap and gap checks - Add sequential invoice auto-numbering - Fix InvoiceLine VAT rate to use canonical VatCodes - Fix SAF-T account type mapping (financial → Expense) - Add DraftLine validation (cannot have both debit and credit > 0) UX improvements (Phase 4): - Fix Danish character encoding across 15+ files (ø, æ, å) - Deploy DemoDataDisclaimer on pages with mock/incomplete data - Adopt PageHeader component universally across all pages - Standardize active/inactive filtering to Switch pattern - Fix dead buttons in Header (Help, Notifications) - Remove hardcoded mock data from Settings - Fix Sidebar controlled state and Kontooversigt navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
45 lines
1.9 KiB
C#
45 lines
1.9 KiB
C#
using Books.Api.Authorization;
|
|
using Books.Api.Domain.Companies;
|
|
using Books.Api.Domain.UserAccess;
|
|
using Books.Api.EventFlow.Repositories;
|
|
using Books.Api.GraphQL.Types;
|
|
using GraphQL;
|
|
using GraphQL.Types;
|
|
|
|
namespace Books.Api.GraphQL.Queries;
|
|
|
|
public class BooksQuery : ObjectGraphType
|
|
{
|
|
public BooksQuery()
|
|
{
|
|
Name = "Query";
|
|
Description = "Root query for the Books API";
|
|
|
|
// companies: [CompanyType]
|
|
Field<ListGraphType<CompanyType>>("companies")
|
|
.Description("Get all companies accessible to the current user")
|
|
.ResolveAsync(async ctx =>
|
|
{
|
|
var accessService = ctx.RequestServices!.GetRequiredService<ICompanyAccessService>();
|
|
var repository = ctx.RequestServices!.GetRequiredService<ICompanyRepository>();
|
|
var userAccesses = await accessService.GetUserCompaniesAsync(ctx.CancellationToken);
|
|
var companyIds = userAccesses.Select(a => CompanyId.With(a.CompanyId)).ToList();
|
|
if (companyIds.Count == 0) return Enumerable.Empty<object>();
|
|
return await repository.GetByIds(companyIds, ctx.CancellationToken);
|
|
});
|
|
|
|
// company(id: ID!): CompanyType
|
|
Field<CompanyType>("company")
|
|
.Description("Get a company by ID")
|
|
.Argument<NonNullGraphType<IdGraphType>>("id", "The company ID")
|
|
.ResolveAsync(async ctx =>
|
|
{
|
|
var id = ctx.GetArgument<string>("id");
|
|
var accessService = ctx.RequestServices!.GetRequiredService<ICompanyAccessService>();
|
|
await accessService.RequireAccessAsync(id, CompanyRole.Viewer, ctx.CancellationToken);
|
|
var repository = ctx.RequestServices!.GetRequiredService<ICompanyRepository>();
|
|
var companies = await repository.GetByIds([CompanyId.With(id)], ctx.CancellationToken);
|
|
return companies.FirstOrDefault();
|
|
});
|
|
}
|
|
}
|