diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index fc0194f..059e5da 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -10,7 +10,7 @@ on: paths: ["apps/api/**"] env: - DOTNET_VERSION: "8.0.x" + DOTNET_VERSION: "9.0.x" defaults: run: @@ -31,16 +31,16 @@ jobs: dotnet-version: ${{ env.DOTNET_VERSION }} - name: 📦 Install dependencies - run: dotnet restore "Kijk.Api/Kijk.Api.csproj" + run: dotnet restore "src/Api/Api.csproj" - name: 🧱 Build - run: dotnet build "Kijk.Api/Kijk.Api.csproj" -c Release --no-restore + run: dotnet build "src/Api/Api.csproj" -c Release --no-restore - name: 🧹 Format - run: dotnet format --verify-no-changes --verbosity detailed --exclude 'Kijk.Api/Persistence/Migrations/' + run: dotnet format --verify-no-changes --verbosity detailed --exclude '**/Migrations/**' - name: 🧪 Test - run: dotnet test "Kijk.Api/Kijk.Api.csproj" --configuration Release --no-build + run: dotnet test "src/Api/Api.csproj" --configuration Release --no-build - name: 🔍 Audit run: | diff --git a/apps/api/.editorconfig b/apps/api/.editorconfig index e239d5b..7c1c11a 100644 --- a/apps/api/.editorconfig +++ b/apps/api/.editorconfig @@ -5,152 +5,391 @@ root = true # All files [*] indent_style = space +indent_size = 4 +tab_width = 4 +end_of_line = lf +insert_final_newline = false +max_line_length = 150 -# Microsoft .NET properties -csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, async, unsafe, volatile, readonly, file, required:suggestion -csharp_preserve_single_line_blocks = true +# Xml files +[*.xml] +indent_size = 2 -# ReSharper properties -resharper_blank_lines_before_single_line_comment = 1 -resharper_braces_redundant = false -resharper_csharp_insert_final_newline = true -resharper_csharp_keep_blank_lines_in_code = 1 -resharper_csharp_keep_blank_lines_in_declarations = 1 -resharper_csharp_max_line_length = 151 -resharper_csharp_wrap_after_declaration_lpar = true -resharper_csharp_wrap_after_invocation_lpar = true -resharper_csharp_wrap_parameters_style = chop_if_long -resharper_keep_existing_declaration_parens_arrangement = false -resharper_keep_existing_enum_arrangement = false -resharper_keep_existing_invocation_parens_arrangement = false - -# XML project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +# Json files +[*.json] indent_size = 2 -# XML config files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +# yaml files +[*.{yml,yaml}] indent_size = 2 -# Code files -[*.{cs,csx,vb,vbx}] -indent_size = 4 -insert_final_newline = true -charset = utf-8-bom -############################### -# .NET Coding Conventions # -############################### +# Ignore paths +[src/Api/Persistence/Migrations/*] +generated_code = true + +#### .NET Coding Conventions #### [*.{cs,vb}] # Organize usings +dotnet_separate_import_directive_groups = true dotnet_sort_system_directives_first = true -# this. preferences +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent dotnet_style_qualification_for_field = false:silent -dotnet_style_qualification_for_property = false:silent dotnet_style_qualification_for_method = false:silent -dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_property = false:silent + # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:silent dotnet_style_predefined_type_for_member_access = true:silent + # Parentheses preferences dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + # Modifier preferences dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent -dotnet_style_readonly_field = true:suggestion + # Expression-level preferences -dotnet_style_object_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_null_propagation = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent -dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion -# dotnet_diagnostic.CA1813.severity = suggestion -dotnet_diagnostic.CA1802.severity = warning -dotnet_diagnostic.CA1810.severity = warning +# Field preferences +dotnet_style_readonly_field = true:warning -############################### -# Naming Conventions # -############################### -# Style Definitions -dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# Use PascalCase for constant fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_symbols.constant_fields.applicable_kinds = field -dotnet_naming_symbols.constant_fields.applicable_accessibilities = * -dotnet_naming_symbols.constant_fields.required_modifiers = const -############################### -# C# Coding Conventions # -############################### +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +#### C# Coding Conventions #### [*.cs] +csharp_style_namespace_declarations = file_scoped +dotnet_style_prefer_collection_expression = true:suggestion # var preferences -csharp_style_var_for_built_in_types = true:silent -csharp_style_var_when_type_is_apparent = true:silent -csharp_style_var_elsewhere = true:silent +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true + # Expression-bodied members -csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = true:silent csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent + # Pattern matching preferences -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion + # Null-checking preferences -csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion + # Modifier preferences -csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion # A property with the same name was updated with a value public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, async, unsafe, volatile, readonly, file, required:suggestion in a section [*] -# Expression-level preferences +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:silent + +# Code-block preferences csharp_prefer_braces = true:silent -csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion -############################### -# C# Formatting Rules # -############################### +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + # New line preferences -csharp_new_line_before_open_brace = all -csharp_new_line_before_else = true csharp_new_line_before_catch = true +csharp_new_line_before_else = true csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all csharp_new_line_between_query_expression_clauses = true + # Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current csharp_indent_switch_labels = true -csharp_indent_labels = flush_left + # Space preferences csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_around_binary_operators = before_and_after -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_square_brackets = false + # Wrapping preferences -csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true -############################### -# VB Coding Conventions # -############################### -[*.vb] -# Modifier preferences -visual_basic_preferred_modifier_order = Partial, Default, Private, Protected, Public, Friend, NotOverridable, Overridable, MustOverride, Overloads, Overrides, MustInherit, NotInheritable, Static, Shared, Shadows, ReadOnly, WriteOnly, Dim, Const, WithEvents, Widening, Narrowing, Custom, Async:suggestion +csharp_preserve_single_line_statements = true + +#### Naming styles #### +# Naming rules +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion +dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase + +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion +dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase + +dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods +dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties +dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.events_should_be_pascalcase.symbols = events +dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables +dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase + +dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase + +dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion +dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters +dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase + +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase + +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums +dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase + +# Symbol specifications + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interfaces.required_modifiers = + +dotnet_naming_symbols.enums.applicable_kinds = enum +dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enums.required_modifiers = + +dotnet_naming_symbols.events.applicable_kinds = event +dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.events.required_modifiers = + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.methods.required_modifiers = + +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.properties.required_modifiers = + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_fields.required_modifiers = + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types_and_namespaces.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.type_parameters.applicable_kinds = namespace +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters.required_modifiers = + +dotnet_naming_symbols.private_constant_fields.applicable_kinds = field +dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_constant_fields.required_modifiers = const + +dotnet_naming_symbols.local_variables.applicable_kinds = local +dotnet_naming_symbols.local_variables.applicable_accessibilities = local +dotnet_naming_symbols.local_variables.required_modifiers = + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.applicable_accessibilities = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.parameters.applicable_accessibilities = * +dotnet_naming_symbols.parameters.required_modifiers = + +dotnet_naming_symbols.public_constant_fields.applicable_kinds = field +dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_constant_fields.required_modifiers = const + +dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function +dotnet_naming_symbols.local_functions.applicable_accessibilities = * +dotnet_naming_symbols.local_functions.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case + +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case + +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_style.s_camelcase.required_prefix = s_ +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.capitalization = camel_case + +# c# rules +dotnet_diagnostic.1591.severity = none +dotnet_diagnostic.IDE0053.severity = false +dotnet_diagnostic.IDE0055.severity = none +dotnet_diagnostic.IDE0023.severity = none +# dotnet rules +dotnet_diagnostic.CA1304.severity = none +dotnet_diagnostic.CA1305.severity = none +dotnet_diagnostic.CA1311.severity = none +dotnet_diagnostic.CA1000.severity = none +dotnet_diagnostic.CA1862.severity = suggestion +# Sonarlint rules +dotnet_diagnostic.S1133.severity = none +dotnet_diagnostic.S125.severity = warning +dotnet_diagnostic.S1135.severity = none +dotnet_diagnostic.S2094.severity = none +dotnet_diagnostic.S2325.severity = suggestion +dotnet_diagnostic.S4830.severity = none +dotnet_diagnostic.S6667.severity = none +dotnet_diagnostic.S6602.severity = error +dotnet_diagnostic.S6603.severity = none +dotnet_diagnostic.S6605.severity = none +dotnet_diagnostic.S2139.severity = none +dotnet_diagnostic.NU1901.severity = none +dotnet_diagnostic.NU1902.severity = none +dotnet_diagnostic.NU1903.severity = none +dotnet_diagnostic.NU1904.severity = none \ No newline at end of file diff --git a/apps/api/Directory.Build.props b/apps/api/Directory.Build.props new file mode 100644 index 0000000..fd4d1ab --- /dev/null +++ b/apps/api/Directory.Build.props @@ -0,0 +1,30 @@ + + + + + + + net9.0 + preview + true + enable + + + + + + + + true + true + true + latest + Recommended + $(NoWarn);1591; + + + + + + + \ No newline at end of file diff --git a/apps/api/Directory.Packages.props b/apps/api/Directory.Packages.props new file mode 100644 index 0000000..ce1d140 --- /dev/null +++ b/apps/api/Directory.Packages.props @@ -0,0 +1,54 @@ + + + + true + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Extensions/ApplicationExtensions.cs b/apps/api/Kijk.Api/Common/Extensions/ApplicationExtensions.cs deleted file mode 100644 index 16a8c96..0000000 --- a/apps/api/Kijk.Api/Common/Extensions/ApplicationExtensions.cs +++ /dev/null @@ -1,77 +0,0 @@ -using HealthChecks.UI.Client; -using Kijk.Api.Common.Models; -using Kijk.Api.Endpoints; -using Kijk.Api.Persistence; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; - -namespace Kijk.Api.Common.Extensions; - -public static class ApplicationExtensions -{ - public static IApplicationBuilder UseCustomOpenApi(this IApplicationBuilder applicationBuilder) - { - applicationBuilder.UseSwagger( - c => - { - c.RouteTemplate = "api/swagger/{documentName}/swagger.json"; - }); - applicationBuilder.UseSwaggerUI( - c => - { - // c.UseRequestInterceptor( - // "(req) => { req.headers['Authorization'] = 'Bearer ' + window?.swaggerUIRedirectOauth2?.auth?.token?.access_token; return req; }"); - c.DefaultModelsExpandDepth(0); - c.DefaultModelExpandDepth(0); - c.SwaggerEndpoint("v1/swagger.json", "Kijk Api v1.00"); - c.RoutePrefix = "api/swagger"; - }); - return applicationBuilder; - } - - public static IApplicationBuilder ApplyInitialData(this IApplicationBuilder applicationBuilder) - { - using var scope = applicationBuilder.ApplicationServices.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); - - AppDbInitializer.InitDb(dbContext); - - return applicationBuilder; - } - - public static IApplicationBuilder ApplyMigrations(this IApplicationBuilder applicationBuilder) - { - using var scope = applicationBuilder.ApplicationServices.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); - - dbContext.Database.MigrateAsync(); - Log.ForContext(typeof(AppDbInitializer)).Information("Database migrations applied"); - - return applicationBuilder; - } - - public static IApplicationBuilder MapApiEndpoints(this IApplicationBuilder applicationBuilder) - { - var app = (WebApplication)applicationBuilder; - app.Map("/", () => Results.Redirect("api/swagger")); - - var apiGroup = app.MapGroup("/api") - .RequireAuthorization(AppConstants.Policies.All) - .WithOpenApi(); - - apiGroup.RequirePerUserRateLimit(); - - apiGroup.MapTransactionsEndpoints(); - apiGroup.MapUsersApi(); - apiGroup.MapCategoriesApi(); - - return app; - } - - public static IApplicationBuilder MapHealthCheck(this IApplicationBuilder applicationBuilder) - { - var app = (IEndpointRouteBuilder)applicationBuilder; - app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse }); - - return applicationBuilder; - } -} diff --git a/apps/api/Kijk.Api/Common/Extensions/AuthorizationHandlerExtension.cs b/apps/api/Kijk.Api/Common/Extensions/AuthorizationHandlerExtension.cs deleted file mode 100644 index 45f0844..0000000 --- a/apps/api/Kijk.Api/Common/Extensions/AuthorizationHandlerExtension.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Kijk.Api.Common.Models; -using Microsoft.AspNetCore.Authorization; - -namespace Kijk.Api.Common.Extensions; - -public static class AuthorizationHandlerExtensions -{ - // Adds the current user requirement that will activate our authorization handler - public static AuthorizationPolicyBuilder RequireCurrentUser(this AuthorizationPolicyBuilder builder) - { - return builder.RequireAuthenticatedUser().AddRequirements(new CheckCurrentUserRequirement()); - } -} - -public class CheckCurrentUserRequirement : IAuthorizationRequirement -{ -} - -// This authorization handler verifies that the user exists even if there's a valid token -public class CheckCurrentUserAuthHandler(CurrentUser currentUser) : AuthorizationHandler -{ - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CheckCurrentUserRequirement requirement) - { - if (currentUser is { User.AuthId: not null } and { Principal: not null }) - { - context.Succeed(requirement); - } - - return Task.CompletedTask; - } -} diff --git a/apps/api/Kijk.Api/Common/Extensions/CurrentUserExtension.cs b/apps/api/Kijk.Api/Common/Extensions/CurrentUserExtension.cs deleted file mode 100644 index 2da72ac..0000000 --- a/apps/api/Kijk.Api/Common/Extensions/CurrentUserExtension.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Security.Claims; -using Kijk.Api.Common.Models; -using Kijk.Api.Domain.Entities; -using Kijk.Api.Persistence; -using Microsoft.AspNetCore.Authentication; - -namespace Kijk.Api.Common.Extensions; - -public static class CurrentUserExtensions -{ - public static IServiceCollection AddCurrentUser(this IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - return services; - } - - /// - /// This class gets only called if is NOT null. - /// - private sealed class ClaimsTransformation(CurrentUser currentUser, AppDbContext dbContext) : IClaimsTransformation - { - // We're not going to transform anything. We're using this as a hook into authorization - // to set the current user without adding custom middleware. - public async Task TransformAsync(ClaimsPrincipal principal) - { - var sub = principal.FindFirstValue(ClaimTypes.NameIdentifier); - - if (sub != null) - { - var email = principal.FindFirstValue(ClaimTypes.Email); - var userEntity = await dbContext.Users - .AsNoTracking() - .Where(x => x.AuthId == sub) - .Select(x => SimpleAuthUser.Create(x)) - .FirstOrDefaultAsync(); - - currentUser.Principal = principal; - // TODO use more values from token - currentUser.User = userEntity ?? new SimpleAuthUser( - Guid.NewGuid(), - sub, - AppConstants.CreateUserIdentifier, - email, - true); - } - - return await Task.FromResult(principal); - } - } -} diff --git a/apps/api/Kijk.Api/Common/Extensions/EnumerableExtensions.cs b/apps/api/Kijk.Api/Common/Extensions/EnumerableExtensions.cs deleted file mode 100644 index a3adfe7..0000000 --- a/apps/api/Kijk.Api/Common/Extensions/EnumerableExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Kijk.Api.Common.Extensions; - -public static class EnumerableExtensions -{ - public static IQueryable If(this IQueryable query, bool should, params Func, IQueryable>[] transforms) - { - return should - ? transforms.Aggregate( - query, - (current, transform) => transform.Invoke(current)) - : query; - } - - public static IEnumerable If(this IEnumerable query, bool should, params Func, IEnumerable>[] transforms) - { - return should - ? transforms.Aggregate( - query, - (current, transform) => transform.Invoke(current)) - : query; - } -} diff --git a/apps/api/Kijk.Api/Common/Extensions/RateLimitExtension.cs b/apps/api/Kijk.Api/Common/Extensions/RateLimitExtension.cs deleted file mode 100644 index cb3860b..0000000 --- a/apps/api/Kijk.Api/Common/Extensions/RateLimitExtension.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Security.Claims; -using System.Threading.RateLimiting; - -using Kijk.Api.Common.Models; - -namespace Kijk.Api.Common.Extensions; - -public static class RateLimitExtensions -{ - - public static IServiceCollection AddRateLimitPolicy(this IServiceCollection services) - { - return services.AddRateLimiter( - options => - { - options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; - - options.AddPolicy( - AppConstants.Policies.RateLimit, - context => - { - var username = context.User.FindFirstValue(ClaimTypes.NameIdentifier)!; - - // 100 Requests per 10sec per user - return RateLimitPartition.GetTokenBucketLimiter( - username, - _ => new TokenBucketRateLimiterOptions - { - ReplenishmentPeriod = TimeSpan.FromSeconds(10), - AutoReplenishment = true, - TokenLimit = 100, - TokensPerPeriod = 100, - QueueLimit = 100 - }); - }); - }); - } - - public static IEndpointConventionBuilder RequirePerUserRateLimit(this IEndpointConventionBuilder builder) - { - return builder.RequireRateLimiting(AppConstants.Policies.RateLimit); - } -} diff --git a/apps/api/Kijk.Api/Common/GlobalExceptionHandler.cs b/apps/api/Kijk.Api/Common/GlobalExceptionHandler.cs deleted file mode 100644 index c39f6b7..0000000 --- a/apps/api/Kijk.Api/Common/GlobalExceptionHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Net; -using System.Text.Json; - -using Kijk.Api.Common.Models; - -using Microsoft.AspNetCore.Diagnostics; - -using Sentry; - -namespace Kijk.Api.Common; - -public class GlobalExceptionHandler : IExceptionHandler -{ - - public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) - { - httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - httpContext.Response.ContentType = "application/json"; - - var exceptionHandlerFeature = httpContext.Features.Get(); - if (exceptionHandlerFeature is not null) - { - SentrySdk.CaptureException(exceptionHandlerFeature.Error); - Log.ForContext(typeof(ExceptionHandlerMiddleware)).Error( - "Something went wrong: {ContextFeatureError}", - exceptionHandlerFeature.Error.Message); - - var error = AppError.Unexpected( - AppErrorCodes.UnexpectedError, - $"Ups, etwas ist schief gelaufen. {exceptionHandlerFeature.Error.Message}"); - var json = JsonSerializer.Serialize(ApiResponse.Error(error)); - await httpContext.Response.WriteAsync(json, cancellationToken: cancellationToken); - } - - return true; - } -} diff --git a/apps/api/Kijk.Api/Common/Middleware/AuthResponseHandlerMiddleware.cs b/apps/api/Kijk.Api/Common/Middleware/AuthResponseHandlerMiddleware.cs deleted file mode 100644 index 331f84a..0000000 --- a/apps/api/Kijk.Api/Common/Middleware/AuthResponseHandlerMiddleware.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Net; -using System.Text.Json; -using Kijk.Api.Common.Models; - -namespace Kijk.Api.Common.Middleware; - -public static class AuthResponseHandlerMiddleware -{ - public static IApplicationBuilder UseAuthExceptionHandler(this IApplicationBuilder app) - { - app.Use( - async (context, next) => - { - await next(); - - if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized) - { - context.Response.ContentType = "application/json"; - var resp = ApiResponseBuilder.Error(AppError.Basic(AppErrorCodes.AuthenticationError, "Token is not valid")); - SentToSentry(resp); - await context.Response.WriteAsync(JsonSerializer.Serialize(resp)); - } - - if (context.Response.StatusCode == (int)HttpStatusCode.Forbidden) - { - context.Response.ContentType = "application/json"; - var resp = ApiResponseBuilder.Error(AppError.Basic(AppErrorCodes.AuthorizationError, "Role is not sufficient")); - SentToSentry(resp); - await context.Response.WriteAsync(JsonSerializer.Serialize(resp)); - } - }); - return app; - } - - private static void SentToSentry(ApiResponse> resp) - { - SentrySdk.CaptureMessage( - resp.Data?[0].Message ?? "AuthError: Token or role is not valid", - opt => - { - opt.SetExtra("Response", resp); - opt.SetExtra("Code", resp.Data?[0].Code); - }); - } -} diff --git a/apps/api/Kijk.Api/Common/Models/ApiResponse.cs b/apps/api/Kijk.Api/Common/Models/ApiResponse.cs deleted file mode 100644 index 50b36b8..0000000 --- a/apps/api/Kijk.Api/Common/Models/ApiResponse.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Kijk.Api.Common.Models; - -public record ApiResponse -{ - public T? Data { get; init; } - - public ResponseStatus Status { get; set; } - - public string? Message { get; set; } - - public static implicit operator ApiResponse(T data) => Success(data); - - public static ApiResponse Success(T data, string? message = default) - { - return new ApiResponse { Status = ResponseStatus.Success, Data = data, Message = message }; - } - - public static ApiResponse> Error(string errorMessage) - { - return new ApiResponse> { Status = ResponseStatus.Error, Message = errorMessage, Data = new List() }; - } - - public static ApiResponse> Error(List error, string? errorMessage = default) - { - return new ApiResponse> { Status = ResponseStatus.Error, Message = errorMessage, Data = error }; - } - - public static ApiResponse> Error(AppError appError, string? errorMessage = default) - { - return new ApiResponse> { Status = ResponseStatus.Error, Message = errorMessage, Data = new List { appError } }; - } -} diff --git a/apps/api/Kijk.Api/Common/Models/CurrentUser.cs b/apps/api/Kijk.Api/Common/Models/CurrentUser.cs deleted file mode 100644 index 03e209b..0000000 --- a/apps/api/Kijk.Api/Common/Models/CurrentUser.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Security.Claims; -using Kijk.Api.Domain.Entities; - -namespace Kijk.Api.Common.Models; - -public class CurrentUser -{ - private const string PermissionsClaim = "permissions"; - - public required ClaimsPrincipal Principal { get; set; } - - public required SimpleAuthUser User { get; set; } - - public Guid Id => this.User.Id; - - public string AuthId => this.User.AuthId ?? throw new ArgumentNullException(this.User.AuthId, "'AuthId' not found"); - - public string Name => this.User.Name ?? throw new ArgumentNullException(this.User.Name, "'Name' not found"); - - public string Email => this.User.Email ?? throw new ArgumentNullException(ClaimTypes.Upn, "'Upn/Email' not found"); - - public List Permissions => Principal.FindAll(PermissionsClaim).Select(x => x.Value).ToList(); - - public bool IsAdmin => this.Permissions.Contains(AppConstants.Roles.Admin); - - public bool IsUser => this.Permissions.Contains(AppConstants.Roles.Admin); -} diff --git a/apps/api/Kijk.Api/Dockerfile b/apps/api/Kijk.Api/Dockerfile deleted file mode 100644 index 21b472b..0000000 --- a/apps/api/Kijk.Api/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base -USER $APP_UID -WORKDIR /app -EXPOSE 8080 -EXPOSE 8081 - -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["Kijk.Api/Kijk.Api.csproj", "Kijk.Api/"] -RUN dotnet restore "Kijk.Api/Kijk.Api.csproj" -COPY . . -WORKDIR "/src/Kijk.Api" -RUN dotnet build "Kijk.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/build - -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "Kijk.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Kijk.Api.dll"] diff --git a/apps/api/Kijk.Api/Kijk.Api.csproj b/apps/api/Kijk.Api/Kijk.Api.csproj deleted file mode 100644 index 1fe7373..0000000 --- a/apps/api/Kijk.Api/Kijk.Api.csproj +++ /dev/null @@ -1,67 +0,0 @@ - - - - net8.0 - enable - enable - - aspnet-Kijk.Api-01C3D95B-517D-4E64-8BDD-4EADF1BF84FE - Linux - - true - $(NoWarn);1591 - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - appsettings.json - - - .dockerignore - - - appsettings.json - - - - - - - - diff --git a/apps/api/Kijk.sln b/apps/api/Kijk.sln index f29b182..11b0637 100644 --- a/apps/api/Kijk.sln +++ b/apps/api/Kijk.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kijk.Api", "Kijk.Api\Kijk.Api.csproj", "{864226C8-F64E-450A-B026-783052D78AAB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api", "src\Api\Api.csproj", "{864226C8-F64E-450A-B026-783052D78AAB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{61A8A4BF-DF87-44EA-9C2F-6D432917846E}" ProjectSection(SolutionItems) = preProject @@ -14,8 +14,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt README.md = README.md fly.toml = fly.toml ..\..\.github\workflows\api.yml = ..\..\.github\workflows\api.yml + Directory.Packages.props = Directory.Packages.props + Directory.Build.props = Directory.Build.props EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{061A6C51-5834-45BC-A557-4FFE1CDF615D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,4 +31,7 @@ Global {864226C8-F64E-450A-B026-783052D78AAB}.Release|Any CPU.ActiveCfg = Release|Any CPU {864226C8-F64E-450A-B026-783052D78AAB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {864226C8-F64E-450A-B026-783052D78AAB} = {061A6C51-5834-45BC-A557-4FFE1CDF615D} + EndGlobalSection EndGlobal diff --git a/apps/api/global.json b/apps/api/global.json index c65c9ea..a27a2b8 100644 --- a/apps/api/global.json +++ b/apps/api/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "8.0.0", - "rollForward": "latestMinor", - "allowPrerelease": true + "version": "9.0.0", + "rollForward": "latestMajor", + "allowPrerelease": false } } \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 8c8287a..e78a8f5 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -16,8 +16,7 @@ "scripts": { "dev": "dotnet watch run --project Kijk.Api/Kijk.Api.csproj --launch-profile https", "build": "dotnet build \"Kijk.Api/Kijk.Api.csproj\" -c Release", - "lint": "dotnet format --verify-no-changes --verbosity detailed --exclude 'Kijk.Api/Persistence/Migrations/'", - "lint:fix": "dotnet format --exclude 'Kijk.Api/Persistence/Migrations/'", - "format": "dotnet format --verify-no-changes --verbosity detailed --exclude 'Kijk.Api/Persistence/Migrations/'" + "lint": "dotnet format --verify-no-changes --verbosity detailed --exclude '**/Migrations/'", + "lint:fix": "dotnet format --exclude '**/Migrations/'" } } diff --git a/apps/api/src/Api/Api.csproj b/apps/api/src/Api/Api.csproj new file mode 100644 index 0000000..ecadc2f --- /dev/null +++ b/apps/api/src/Api/Api.csproj @@ -0,0 +1,72 @@ + + + + Kijk.Api + + aspnet-Kijk.Api-01C3D95B-517D-4E64-8BDD-4EADF1BF84FE + Linux + + ./ + --file-name kijk + true + true + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + appsettings.json + + + .dockerignore + + + appsettings.json + + + + + + + + diff --git a/apps/api/src/Api/Common/Exceptions/NullException.cs b/apps/api/src/Api/Common/Exceptions/NullException.cs new file mode 100644 index 0000000..5776273 --- /dev/null +++ b/apps/api/src/Api/Common/Exceptions/NullException.cs @@ -0,0 +1,7 @@ +namespace Kijk.Api.Common.Exceptions; + +/// +/// Exception thrown when a null value is passed to a method that does not accept it. +/// +/// The exception message. +public class NullException(string message) : Exception(message); \ No newline at end of file diff --git a/apps/api/src/Api/Common/Extensions/ApplicationExtensions.cs b/apps/api/src/Api/Common/Extensions/ApplicationExtensions.cs new file mode 100644 index 0000000..0272e57 --- /dev/null +++ b/apps/api/src/Api/Common/Extensions/ApplicationExtensions.cs @@ -0,0 +1,84 @@ +using HealthChecks.UI.Client; + +using Kijk.Api.Common.Models; +using Kijk.Api.Endpoints; + +using Microsoft.AspNetCore.Diagnostics.HealthChecks; + +using Swashbuckle.AspNetCore.SwaggerUI; + +namespace Kijk.Api.Common.Extensions; + +public static class ApplicationExtensions +{ + public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder applicationBuilder) + { + applicationBuilder.UseSerilogRequestLogging( + options => options.GetLevel = new Func((ctx, _, ex) => + { + if (ex == null && ctx.Response.StatusCode <= 499) + { + return LogEventLevel.Information; + } + + if (ctx.Request.Path.StartsWithSegments("/api/health") && ex is OperationCanceledException) + { + // If the incoming HTTP request for a healthcheck is aborted, don't log the resultant OperationCanceledException + // as an error. beware that the ASP.NET DefaultHealthCheckService ensures that if the exception occurs + // within the healthcheck implementation (and the request wasn't aborted) a failed healthcheck is logged + // see https://github.com/dotnet/aspnetcore/blob/ce9e1ae5500c3f0c4b9bd682fd464b3493e48e61/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs#L121 + return LogEventLevel.Information; + } + + return LogEventLevel.Error; + })); + + return applicationBuilder; + } + + public static IApplicationBuilder UseOpenApi(this IApplicationBuilder applicationBuilder) + { + applicationBuilder.UseSwaggerUI( + c => + { + c.SwaggerEndpoint("/api/openapi.json", "Kijk Api v1.0"); + c.DefaultModelsExpandDepth(0); + c.DefaultModelExpandDepth(0); + c.DocExpansion(DocExpansion.None); + c.DocumentTitle = "SwaggerUI - Kijk Api"; + c.RoutePrefix = "api/swagger"; + }); + return applicationBuilder; + } + + public static void ApplyInitialData(this IApplicationBuilder _) + { + // using var scope = applicationBuilder.ApplicationServices.CreateScope(); + // using var dbContext = scope.ServiceProvider.GetRequiredService(); + + // TODO move into sql generation script + // this breaks the auto generation of the openapi file + // AppDbInitializer.InitDb(dbContext); + } + + public static WebApplication MapApiEndpoints(this WebApplication app) + { + var apiGroup = app.MapGroup("/api") + .RequireAuthorization(AppConstants.Policies.All) + .RequirePerUserRateLimit(); + + apiGroup.MapTransactionsEndpoints(); + apiGroup.MapUsersApi(); + apiGroup.MapCategoriesApi(); + + return app; + } + + public static IApplicationBuilder MapHealthCheck(this IApplicationBuilder applicationBuilder) + { + var app = (IEndpointRouteBuilder)applicationBuilder; + app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse }); + + return applicationBuilder; + } +} \ No newline at end of file diff --git a/apps/api/src/Api/Common/Extensions/BearerAuthSchemeTransformer.cs b/apps/api/src/Api/Common/Extensions/BearerAuthSchemeTransformer.cs new file mode 100644 index 0000000..baeac94 --- /dev/null +++ b/apps/api/src/Api/Common/Extensions/BearerAuthSchemeTransformer.cs @@ -0,0 +1,54 @@ +using Kijk.Api.Common.Exceptions; +using Kijk.Api.Common.Options; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi.Models; + +namespace Kijk.Api.Common.Extensions; + +/// +/// Schema transformer for the OpenApi document to add the Oauth2 (Bearer) authentication. +/// +/// +/// +public sealed class BearerAuthSchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider, IConfiguration configuration) + : IOpenApiDocumentTransformer +{ + public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + var appSettings = configuration.GetSection(AuthOptions.SectionName).Get(); + + if (appSettings is null) + { + throw new NullException($"Keine AuthOptions gefunden, {appSettings}"); + } + + var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync(); + if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer")) + { + var requirements = new Dictionary + { + ["Bearer"] = new() + { + Type = SecuritySchemeType.Http, + Scheme = "bearer", + In = ParameterLocation.Header, + BearerFormat = "Json Web Token", + Description = "JWT Authorization header using the Bearer scheme." + } + }; + document.Components ??= new OpenApiComponents(); + document.Components.SecuritySchemes = requirements; + // Apply it as a requirement for all operations + foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations)) + { + operation.Value.Security.Add(new OpenApiSecurityRequirement + { + [new OpenApiSecurityScheme { Reference = new OpenApiReference { Id = "Bearer", Type = ReferenceType.SecurityScheme } }] = + Array.Empty() + }); + } + } + } +} \ No newline at end of file diff --git a/apps/api/src/Api/Common/Extensions/EnumerableExtensions.cs b/apps/api/src/Api/Common/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..794d580 --- /dev/null +++ b/apps/api/src/Api/Common/Extensions/EnumerableExtensions.cs @@ -0,0 +1,16 @@ +namespace Kijk.Api.Common.Extensions; + +public static class EnumerableExtensions +{ + public static IQueryable If(this IQueryable query, bool should, params Func, IQueryable>[] transforms) => should + ? transforms.Aggregate( + query, + (current, transform) => transform.Invoke(current)) + : query; + + public static IEnumerable If(this IEnumerable query, bool should, params Func, IEnumerable>[] transforms) => should + ? transforms.Aggregate( + query, + (current, transform) => transform.Invoke(current)) + : query; +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Extensions/HostExtensions.cs b/apps/api/src/Api/Common/Extensions/HostExtensions.cs similarity index 99% rename from apps/api/Kijk.Api/Common/Extensions/HostExtensions.cs rename to apps/api/src/Api/Common/Extensions/HostExtensions.cs index 07bf5ef..c2b688d 100644 --- a/apps/api/Kijk.Api/Common/Extensions/HostExtensions.cs +++ b/apps/api/src/Api/Common/Extensions/HostExtensions.cs @@ -23,4 +23,4 @@ public static WebApplicationBuilder AddLogging(this WebApplicationBuilder builde return builder; } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Extensions/OptionsExtension.cs b/apps/api/src/Api/Common/Extensions/OptionsExtension.cs similarity index 59% rename from apps/api/Kijk.Api/Common/Extensions/OptionsExtension.cs rename to apps/api/src/Api/Common/Extensions/OptionsExtension.cs index 696fe08..f1bcf06 100644 --- a/apps/api/Kijk.Api/Common/Extensions/OptionsExtension.cs +++ b/apps/api/src/Api/Common/Extensions/OptionsExtension.cs @@ -8,16 +8,10 @@ namespace Kijk.Api.Common.Extensions; public static class OptionsExtensions { public static IServiceCollection ConfigureOptions(this IServiceCollection services, IConfiguration configuration) - where TOptions : class, IConfigOptions - { - return services.Configure(configuration.GetSection(TOptions.SectionName)); - } + where TOptions : class, IConfigOptions => services.Configure(configuration.GetSection(TOptions.SectionName)); public static TOptions? GetConfigurationSection(this IHostApplicationBuilder builder) - where TOptions : class, IConfigOptions - { - return builder.Configuration - .GetSection(TOptions.SectionName) - .Get(); - } -} + where TOptions : class, IConfigOptions => builder.Configuration + .GetSection(TOptions.SectionName) + .Get(); +} \ No newline at end of file diff --git a/apps/api/src/Api/Common/Extensions/RateLimitExtension.cs b/apps/api/src/Api/Common/Extensions/RateLimitExtension.cs new file mode 100644 index 0000000..ca16456 --- /dev/null +++ b/apps/api/src/Api/Common/Extensions/RateLimitExtension.cs @@ -0,0 +1,38 @@ +using System.Security.Claims; +using System.Threading.RateLimiting; + +using Kijk.Api.Common.Models; + +namespace Kijk.Api.Common.Extensions; + +public static class RateLimitExtensions +{ + public static IServiceCollection AddRateLimitPolicy(this IServiceCollection services) => services.AddRateLimiter( + options => + { + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + + options.AddPolicy(AppConstants.Policies.RateLimit, context => + { + var username = context.User.FindFirstValue(ClaimTypes.NameIdentifier)!; + + // 100 Requests per 10sec per user + return RateLimitPartition.GetTokenBucketLimiter( + username, + _ => new TokenBucketRateLimiterOptions + { + ReplenishmentPeriod = TimeSpan.FromSeconds(10), + AutoReplenishment = true, + TokenLimit = 100, + TokensPerPeriod = 100, + QueueLimit = 100 + }); + }); + }); + + public static RouteGroupBuilder RequirePerUserRateLimit(this RouteGroupBuilder builder) + { + builder.RequireRateLimiting(AppConstants.Policies.RateLimit); + return builder; + } +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Extensions/RouteExtensions.cs b/apps/api/src/Api/Common/Extensions/RouteExtensions.cs similarity index 66% rename from apps/api/Kijk.Api/Common/Extensions/RouteExtensions.cs rename to apps/api/src/Api/Common/Extensions/RouteExtensions.cs index 67bb9d4..58f08cd 100644 --- a/apps/api/Kijk.Api/Common/Extensions/RouteExtensions.cs +++ b/apps/api/src/Api/Common/Extensions/RouteExtensions.cs @@ -1,4 +1,5 @@ using System.Net; + using Kijk.Api.Common.Filters; namespace Kijk.Api.Common.Extensions; @@ -11,9 +12,7 @@ public static class RouteExtensions /// /// /// - public static RouteHandlerBuilder WithRequestValidation(this RouteHandlerBuilder builder) where TRequest : class - { - return builder.AddEndpointFilter>() - .Produces((int)HttpStatusCode.BadRequest); - } -} + public static RouteHandlerBuilder WithRequestValidation(this RouteHandlerBuilder builder) where TRequest : class => builder + .AddEndpointFilter>() + .Produces((int)HttpStatusCode.BadRequest); +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Extensions/ServiceExtensions.cs b/apps/api/src/Api/Common/Extensions/ServiceExtensions.cs similarity index 56% rename from apps/api/Kijk.Api/Common/Extensions/ServiceExtensions.cs rename to apps/api/src/Api/Common/Extensions/ServiceExtensions.cs index 001254f..24c79aa 100644 --- a/apps/api/Kijk.Api/Common/Extensions/ServiceExtensions.cs +++ b/apps/api/src/Api/Common/Extensions/ServiceExtensions.cs @@ -1,10 +1,13 @@ using System.Globalization; -using System.IO.Compression; -using System.Reflection; using System.Security.Claims; using System.Text.Json.Serialization; + +using EntityFramework.Exceptions.PostgreSQL; + using Humanizer; -using Kijk.Api.Common.Filters; + +using Kijk.Api.Common.Exceptions; +using Kijk.Api.Common.Middleware; using Kijk.Api.Common.Models; using Kijk.Api.Common.Options; using Kijk.Api.Modules.App; @@ -13,12 +16,10 @@ using Kijk.Api.Modules.Users; using Kijk.Api.Persistence; using Kijk.Api.Persistence.Interceptors; + using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.ResponseCompression; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.IdentityModel.Tokens; -using Microsoft.OpenApi.Models; namespace Kijk.Api.Common.Extensions; @@ -51,94 +52,58 @@ public static IServiceCollection AddCorsPolicy(this IServiceCollection services, throw new ArgumentNullException($"{allowedOrigins}", "Cors appsettings is null"); } - services.AddCors( - options => - { - options.AddPolicy( - AppConstants.Policies.Cors, - builder => - { - builder.WithOrigins(allowedOrigins) - .AllowAnyMethod() - .AllowAnyHeader(); - }); - }); + services.AddCors(options => options.AddPolicy( + AppConstants.Policies.Cors, builder => builder.WithOrigins(allowedOrigins) + .AllowAnyMethod() + .AllowAnyHeader())); return services; } public static IServiceCollection AddOpenApi(this IServiceCollection services, IConfiguration configuration) { - var authSettings = configuration.GetSection(AuthOptions.SectionName).Get(); + var appSettings = configuration.GetSection(AuthOptions.SectionName).Get(); - if (authSettings is null) + if (appSettings is null) { - throw new Exception($"No auth settings found, {authSettings}"); + throw new NullException($"Keine AzureAdSettings gefunden, {appSettings}"); } - services.AddEndpointsApiExplorer(); - services.AddSwaggerGen( - o => - { - o.UseInlineDefinitionsForEnums(); - o.SwaggerDoc("v1", new() { Version = "v1", Title = "Kijk API", Description = "Kijk API to manage your houses" }); - - o.AddSecurityDefinition( - "bearerAuth", - new OpenApiSecurityScheme - { - Type = SecuritySchemeType.Http, - Scheme = "bearer", - BearerFormat = "JWT", - Description = "JWT Authorization header using the Bearer scheme." - }); - o.AddSecurityRequirement( - new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "bearerAuth" } }, - new string[] { } - } - }); - - var xmlFilename = $"{typeof(Program).Assembly.GetName().Name}.xml"; - o.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); - }); + services.AddOpenApi("openapi", opt => opt.AddDocumentTransformer((document, _, _) => + { + document.Info = new() { Version = "v1", Title = "Kijk API", Description = "Kijk API to manage your houses", }; + opt.AddDocumentTransformer(); + return Task.CompletedTask; + })); return services; } - public static IServiceCollection AddDatabase(this IServiceCollection services) + public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration) { - services.AddScoped(); + services.AddSingleton(); - services.AddDbContext(); + services.AddDbContextPool((sp, optionsBuilder) => + { + var connectionString = configuration.GetConnectionString("DefaultConnection"); + // TODO use MapEnum and use database enums + optionsBuilder.UseNpgsql(connectionString, opt => opt.EnableRetryOnFailure()) + .UseExceptionProcessor() + .UseSnakeCaseNamingConvention() + .AddInterceptors(sp.GetServices()); + }); return services; } public static IServiceCollection AddCompression(this IServiceCollection services) { - services.AddResponseCompression( - options => - { - options.EnableForHttps = true; - options.Providers.Add(); - options.Providers.Add(); - options.MimeTypes = ResponseCompressionDefaults.MimeTypes; - }); - - services.Configure( - options => - { - options.Level = CompressionLevel.Optimal; - }); - - services.Configure( - options => - { - options.Level = CompressionLevel.SmallestSize; - }); - + services.AddResponseCompression(options => + { + options.EnableForHttps = true; + options.Providers.Add(); + options.Providers.Add(); + options.MimeTypes = ResponseCompressionDefaults.MimeTypes; + }); return services; } @@ -154,9 +119,7 @@ public static IServiceCollection AddControllerOptions(this IServiceCollection se { services.ConfigureHttpJsonOptions(options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())); - services.AddControllers().AddJsonOptions( - options => - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); + services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); return services; } @@ -166,7 +129,7 @@ public static IServiceCollection AddHealthCheck(this IServiceCollection services var conString = configuration.GetConnectionString(ConnectionOptions.SectionName); if (conString is null) { - throw new Exception($"No connection string found, {conString}"); + throw new NullException($"No connection string found, {conString}"); } services.AddHealthChecks().AddNpgSql(conString, tags: ["database", "postgresql"]); @@ -180,6 +143,7 @@ public static IServiceCollection AddAuth(this IServiceCollection services, IConf .AddJwtBearer( x => { + // Authority is the URL of your clerk instance x.Authority = configuration["Auth:Authority"]; x.TokenValidationParameters = new TokenValidationParameters() { @@ -187,7 +151,7 @@ public static IServiceCollection AddAuth(this IServiceCollection services, IConf ValidateAudience = false, NameClaimType = ClaimTypes.NameIdentifier }; - x.Events = new JwtBearerEvents() + x.Events = new JwtBearerEvents { // Additional validation for AZP claim OnTokenValidated = context => @@ -195,31 +159,27 @@ public static IServiceCollection AddAuth(this IServiceCollection services, IConf var azp = context.Principal?.FindFirstValue("azp"); // AuthorizedParty is the base URL of your frontend. - if (string.IsNullOrEmpty(azp) || !azp.Equals(configuration["Auth:AuthorizedParty"])) + if (string.IsNullOrEmpty(azp) || !azp.Equals(configuration["Auth:AuthorizedParty"], StringComparison.Ordinal)) + { context.Fail("AZP Claim is invalid or missing"); + } return Task.CompletedTask; } }; }); - // State that represents the current user from the request - services.AddCurrentUser(); + services.AddScoped(); + services.AddTransient(); services.AddAuthorizationBuilder() - .AddPolicy(AppConstants.Policies.All, policy => policy.RequireClaim("id").RequireCurrentUser().Build()); + .AddPolicy(AppConstants.Policies.All, policy => policy.RequireClaim("id").RequireAuthenticatedUser().Build()); // .AddPolicy(AppConstants.Policies.User, policy => policy.RequireRole(AppConstants.Roles.User).RequireCurrentUser().Build()) // .AddPolicy(AppConstants.Policies.Admin, policy => policy.RequireRole(AppConstants.Roles.Admin).RequireCurrentUser().Build()) - // add current user handler - services.AddScoped(); - return services; } - public static IServiceCollection AddCache(this IServiceCollection services) - { - return services; - } -} + public static IServiceCollection AddCache(this IServiceCollection services) => services; +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Filters/ValidationFilter.cs b/apps/api/src/Api/Common/Filters/ValidationFilter.cs similarity index 99% rename from apps/api/Kijk.Api/Common/Filters/ValidationFilter.cs rename to apps/api/src/Api/Common/Filters/ValidationFilter.cs index c1e9029..c9e6ea0 100644 --- a/apps/api/Kijk.Api/Common/Filters/ValidationFilter.cs +++ b/apps/api/src/Api/Common/Filters/ValidationFilter.cs @@ -19,4 +19,4 @@ public class ValidationFilter(IValidator validator) : IEndpointFilter wher return await next.Invoke(context); } -} +} \ No newline at end of file diff --git a/apps/api/src/Api/Common/Middleware/AuthResponseHandlerMiddleware.cs b/apps/api/src/Api/Common/Middleware/AuthResponseHandlerMiddleware.cs new file mode 100644 index 0000000..3e8fcc1 --- /dev/null +++ b/apps/api/src/Api/Common/Middleware/AuthResponseHandlerMiddleware.cs @@ -0,0 +1,42 @@ +using System.Net; +using System.Text.Json; + +using Kijk.Api.Common.Models; + +namespace Kijk.Api.Common.Middleware; + +public static class AuthResponseHandlerMiddleware +{ + public static IApplicationBuilder UseAuthExceptionHandler(this IApplicationBuilder app) + { + app.Use(async (context, next) => + { + await next(); + + if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized) + { + context.Response.ContentType = "application/json"; + var resp = ApiResponseBuilder.Error(AppError.Basic(AppErrorCodes.AuthenticationError, "Token is not valid")); + SentToSentry(resp); + await context.Response.WriteAsync(JsonSerializer.Serialize(resp)); + } + + if (context.Response.StatusCode == (int)HttpStatusCode.Forbidden) + { + context.Response.ContentType = "application/json"; + var resp = ApiResponseBuilder.Error(AppError.Basic(AppErrorCodes.AuthorizationError, "Role is not sufficient")); + SentToSentry(resp); + await context.Response.WriteAsync(JsonSerializer.Serialize(resp)); + } + }); + return app; + } + + private static void SentToSentry(ApiResponse> resp) => SentrySdk.CaptureMessage( + resp.Data?[0].Message ?? "AuthError: Token or role is not valid", + opt => + { + opt.SetExtra("Response", resp); + opt.SetExtra("Code", resp.Data?[0].Code); + }); +} \ No newline at end of file diff --git a/apps/api/src/Api/Common/Middleware/CurrentUserMiddleware.cs b/apps/api/src/Api/Common/Middleware/CurrentUserMiddleware.cs new file mode 100644 index 0000000..763c36d --- /dev/null +++ b/apps/api/src/Api/Common/Middleware/CurrentUserMiddleware.cs @@ -0,0 +1,92 @@ +using System.Security.Claims; +using System.Text.Json; + +using Kijk.Api.Common.Models; +using Kijk.Api.Persistence; + +namespace Kijk.Api.Common.Middleware; + +public class CurrentUserMiddleware(AppDbContext dbContext, CurrentUser currentUser) : IMiddleware +{ + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var (isSuccess, errorMessage) = await SetCurrentUser(context); + if (isSuccess) + { + await next(context); + } + else + { + await HandleError(context, errorMessage); + } + } + + private async Task<(bool, string?)> SetCurrentUser(HttpContext context) + { + if (context.Request.Path == "/" || context.Request.Path == "/api/swagger" || + (context.Request.Path.HasValue && context.Request.Path.Value.StartsWith("/api/openapi", StringComparison.OrdinalIgnoreCase))) + { + return (true, null); + } + + var extAuthId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); + if (extAuthId == null) + { + return (false, $"User for id '{extAuthId}' was not found"); + } + + var email = context.User.FindFirstValue(ClaimTypes.Email); + var userEntity = await GetUserFromDb(extAuthId); + currentUser.Principal = context.User; + + if (context.Request.Path.ToString().Contains("sign-in") && userEntity is null) + { + // TODO use more values from token + currentUser.User = new SimpleAuthUser( + Guid.NewGuid(), + extAuthId, + Guid.Empty, + AppConstants.CreateUserIdentifier, + email, + true); + return (true, null); + } + + if (userEntity is null) + { + return (false, $"User for id '{extAuthId}' was not found"); + } + + if (userEntity.HouseholdId == Guid.Empty) + { + return (false, "User has no household"); + } + + currentUser.User = userEntity; + return (true, null); + } + + private Task GetUserFromDb(string sub) => dbContext.Users + .Include(x => x.UserHouseholds) + .Where(x => x.AuthId == sub) + .Select(x => SimpleAuthUser.Create(x)) + .AsNoTracking() + .AsSplitQuery() + .FirstOrDefaultAsync(); + + private static async Task HandleError(HttpContext context, string? errorMessage) + { + context.Response.ContentType = "application/json"; + var resp = ApiResponseBuilder.Error(AppError.Basic(AppErrorCodes.NotFoundError, errorMessage)); + SentToSentry(resp); + await context.Response.WriteAsync(JsonSerializer.Serialize(resp)); + } + + private static void SentToSentry(ApiResponse> resp) => SentrySdk.CaptureMessage( + resp.Data?[0].Message ?? "AuthError: Token or role is not valid", + opt => + { + opt.SetExtra("Response", resp); + opt.SetExtra("Code", resp.Data?[0].Code); + }); +} \ No newline at end of file diff --git a/apps/api/src/Api/Common/Middleware/ExceptionHandlerMiddleware.cs b/apps/api/src/Api/Common/Middleware/ExceptionHandlerMiddleware.cs new file mode 100644 index 0000000..5809172 --- /dev/null +++ b/apps/api/src/Api/Common/Middleware/ExceptionHandlerMiddleware.cs @@ -0,0 +1,37 @@ +using System.Net; +using System.Text.Json; + +using Kijk.Api.Common.Models; + +using Microsoft.AspNetCore.Diagnostics; + +namespace Kijk.Api.Common.Middleware; + +public static class ExceptionHandlerMiddleware +{ + public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app) + { + app.UseExceptionHandler( + appError => appError.Run( + async context => + { + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.Response.ContentType = "application/json"; + + var exceptionHandlerFeature = context.Features.Get(); + if (exceptionHandlerFeature is not null) + { + Log.ForContext(typeof(ExceptionHandlerMiddleware)).Error( + "Something went wrong: {ContextFeatureError}", + exceptionHandlerFeature.Error.Message); + + var error = AppError.Unexpected( + AppErrorCodes.UnexpectedError, + $"Ups, etwas ist schief gelaufen. {exceptionHandlerFeature.Error.Message}"); + var json = JsonSerializer.Serialize(ApiResponse.Error(error)); + await context.Response.WriteAsync(json); + } + })); + return app; + } +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Middleware/ExtendRequestLoggingMiddleware.cs b/apps/api/src/Api/Common/Middleware/ExtendRequestLoggingMiddleware.cs similarity index 86% rename from apps/api/Kijk.Api/Common/Middleware/ExtendRequestLoggingMiddleware.cs rename to apps/api/src/Api/Common/Middleware/ExtendRequestLoggingMiddleware.cs index b396226..4004038 100644 --- a/apps/api/Kijk.Api/Common/Middleware/ExtendRequestLoggingMiddleware.cs +++ b/apps/api/src/Api/Common/Middleware/ExtendRequestLoggingMiddleware.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Primitives; - -using Serilog.Context; +using Serilog.Context; namespace Kijk.Api.Common.Middleware; @@ -19,7 +17,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) private static string GetCorrelationId(HttpContext context) { - context.Request.Headers.TryGetValue(CorrelationIdHeaderName, out StringValues correlationId); + context.Request.Headers.TryGetValue(CorrelationIdHeaderName, out var correlationId); return correlationId.FirstOrDefault() ?? context.TraceIdentifier; } -} +} \ No newline at end of file diff --git a/apps/api/src/Api/Common/Models/ApiResponse.cs b/apps/api/src/Api/Common/Models/ApiResponse.cs new file mode 100644 index 0000000..2347666 --- /dev/null +++ b/apps/api/src/Api/Common/Models/ApiResponse.cs @@ -0,0 +1,28 @@ +namespace Kijk.Api.Common.Models; + +public record ApiResponse +{ + public T? Data { get; init; } + + public ResponseStatus Status { get; set; } + + public string? Message { get; set; } + + public static implicit operator ApiResponse(T data) => Success(data); + + public static ApiResponse Success(T data, string? message = default) => + new() { Status = ResponseStatus.Success, Data = data, Message = message }; + + public static ApiResponse> Error(string errorMessage) => + new() { Status = ResponseStatus.Error, Message = errorMessage, Data = [] }; + + public static ApiResponse> Error(List error, string? errorMessage = default) => + new() { Status = ResponseStatus.Error, Message = errorMessage, Data = error }; + + public static ApiResponse> Error(AppError appError, string? errorMessage = default) => new() + { + Status = ResponseStatus.Error, + Message = errorMessage, + Data = [appError] + }; +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Models/ApiResponseBuilder.cs b/apps/api/src/Api/Common/Models/ApiResponseBuilder.cs similarity index 99% rename from apps/api/Kijk.Api/Common/Models/ApiResponseBuilder.cs rename to apps/api/src/Api/Common/Models/ApiResponseBuilder.cs index f8875a2..ca313e1 100644 --- a/apps/api/Kijk.Api/Common/Models/ApiResponseBuilder.cs +++ b/apps/api/src/Api/Common/Models/ApiResponseBuilder.cs @@ -12,4 +12,4 @@ public static ApiResponse Success(T data, string? message = default) => public static ApiResponse> Error(AppError error) => ApiResponse>.Error(error); public static ApiResponse> Error(List errors) => ApiResponse>.Error(errors); -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Models/AppConstants.cs b/apps/api/src/Api/Common/Models/AppConstants.cs similarity index 99% rename from apps/api/Kijk.Api/Common/Models/AppConstants.cs rename to apps/api/src/Api/Common/Models/AppConstants.cs index 017225d..935b252 100644 --- a/apps/api/Kijk.Api/Common/Models/AppConstants.cs +++ b/apps/api/src/Api/Common/Models/AppConstants.cs @@ -45,4 +45,4 @@ public static class DefaultValues public const string CategoryName = "Uncategorized"; public const string AccountName = "Main"; } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Models/AppError.cs b/apps/api/src/Api/Common/Models/AppError.cs similarity index 83% rename from apps/api/Kijk.Api/Common/Models/AppError.cs rename to apps/api/src/Api/Common/Models/AppError.cs index e050476..b9d5434 100644 --- a/apps/api/Kijk.Api/Common/Models/AppError.cs +++ b/apps/api/src/Api/Common/Models/AppError.cs @@ -34,10 +34,7 @@ private AppError(string code, string message, ErrorType type) /// The error description. public static AppError Basic( string code = AppErrorCodes.DefaultError, - string description = "An error has occurred.") - { - return new AppError(code, $"Error, {description}", ErrorType.Basic); - } + string? description = "An error has occurred.") => new(code, $"Error, {description}", ErrorType.Basic); /// /// Creates an of type from a code and description. @@ -46,10 +43,7 @@ public static AppError Basic( /// The error description. public static AppError Unexpected( string code = AppErrorCodes.UnexpectedError, - string description = "An unexpected error has occurred.") - { - return new AppError(code, $"An 'unexpected' error, {description}", ErrorType.Unexpected); - } + string description = "An unexpected error has occurred.") => new(code, $"An 'unexpected' error, {description}", ErrorType.Unexpected); /// /// Creates an of type from a code and description. @@ -58,10 +52,7 @@ public static AppError Unexpected( /// The error description. public static AppError Validation( string code = AppErrorCodes.ValidationError, - string description = "A 'validation' error has occurred.") - { - return new AppError(code, description, ErrorType.Validation); - } + string description = "A 'validation' error has occurred.") => new(code, description, ErrorType.Validation); /// /// Creates an of type from a code and description. @@ -70,10 +61,7 @@ public static AppError Validation( /// The error description. public static AppError NotFound( string code = AppErrorCodes.NotFoundError, - string description = "A 'Not Found' error has occurred.") - { - return new AppError(code, $"'Not found' error, {description}", ErrorType.NotFound); - } + string description = "A 'Not Found' error has occurred.") => new(code, $"'Not found' error, {description}", ErrorType.NotFound); /// /// Creates an with the given numeric , @@ -85,8 +73,5 @@ public static AppError NotFound( public static AppError Custom( ErrorType type, string code, - string description) - { - return new AppError(code, description, type); - } -} + string description) => new(code, description, type); +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Models/AppErrorCodes.cs b/apps/api/src/Api/Common/Models/AppErrorCodes.cs similarity index 99% rename from apps/api/Kijk.Api/Common/Models/AppErrorCodes.cs rename to apps/api/src/Api/Common/Models/AppErrorCodes.cs index 63555ae..afcd727 100644 --- a/apps/api/Kijk.Api/Common/Models/AppErrorCodes.cs +++ b/apps/api/src/Api/Common/Models/AppErrorCodes.cs @@ -23,4 +23,4 @@ public readonly record struct AppErrorCodes public const string AuthenticationError = "E0005"; public const string AuthorizationError = "E0006"; -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Models/AppResult.cs b/apps/api/src/Api/Common/Models/AppResult.cs similarity index 63% rename from apps/api/Kijk.Api/Common/Models/AppResult.cs rename to apps/api/src/Api/Common/Models/AppResult.cs index 364a080..5363b83 100644 --- a/apps/api/Kijk.Api/Common/Models/AppResult.cs +++ b/apps/api/src/Api/Common/Models/AppResult.cs @@ -1,9 +1,11 @@ -using Kijk.Api.Common.Utils; +using System.Diagnostics.CodeAnalysis; + +using Kijk.Api.Common.Utils; namespace Kijk.Api.Common.Models; /// -/// A discriminated union of value or a, error. +/// A discriminated union of value or a, error. /// public readonly record struct AppResult { @@ -12,7 +14,6 @@ public readonly record struct AppResult "First error cannot be retrieved from a successful Result."); private readonly List? _errors = null; - private readonly TValue? _value = default; /// /// Gets a value indicating whether the state is success. @@ -27,17 +28,18 @@ public readonly record struct AppResult /// /// Gets the list of errors. If the state is not error, the list will be empty. /// - public List Errors => IsError ? _errors! : new List(); + public List Errors => IsError ? _errors! : []; /// - /// Gets the value. + /// Gets the value. /// - public TValue Value => _value!; + [field: AllowNull, MaybeNull] + public TValue Value { get; } = default!; /// /// Gets the first error. /// - public AppError FirstAppError => !IsError ? NoFirstAppError : _errors!.First(); + public AppError FirstAppError => !IsError ? NoFirstAppError : _errors![0]; private AppResult(List errors) { @@ -47,7 +49,7 @@ private AppResult(List errors) private AppResult(TValue value) { - _value = value; + Value = value; IsSuccess = true; } @@ -66,38 +68,36 @@ private AppResult(TValue value) /// /// Creates an from an error. /// - public static implicit operator AppResult(AppError appError) => new(new List { appError }); + public static implicit operator AppResult(AppError appError) + { + return new([appError]); + } /// /// Creates an from a list of errors. /// - public static implicit operator AppResult(List errors) => new(errors); - - public TResult Match(Func onValue, Func, TResult> onError) + public static implicit operator AppResult(List errors) { - return IsError ? onError(Errors) : onValue(Value); + return new(errors); } - public async Task MatchAsync(Func> onValue, Func, Task> onError) - { - return IsError ? await onError(Errors) : await onValue(Value); - } - - public IResult ToResponse(string? successMessage = default, SuccessType successType = SuccessType.Ok) - { - return this.Match( - obj => - { - var response = ApiResponse.Success(obj, successMessage); - var statusCode = ResponseUtils.ToStatusCode(successType); - return ResponseUtils.CreateTypedResult(response, statusCode); - }, - errors => - { - var firstError = errors.First(); - var response = ApiResponse.Error(errors); - var statusCode = ResponseUtils.ToStatusCode(firstError.Type); - return ResponseUtils.CreateTypedResult(response, statusCode); - }); - } -} + public TResult Match(Func onValue, Func, TResult> onError) => IsError ? onError(Errors) : onValue(Value); + + public async Task MatchAsync(Func> onValue, Func, Task> onError) => + IsError ? await onError(Errors) : await onValue(Value); + + public IResult ToResponse(string? successMessage = default, SuccessType successType = SuccessType.Ok) => this.Match( + obj => + { + var response = ApiResponse.Success(obj, successMessage); + var statusCode = ResponseUtils.ToStatusCode(successType); + return ResponseUtils.CreateTypedResult(response, statusCode); + }, + errors => + { + var firstError = errors[0]; + var response = ApiResponse.Error(errors); + var statusCode = ResponseUtils.ToStatusCode(firstError.Type); + return ResponseUtils.CreateTypedResult(response, statusCode); + }); +} \ No newline at end of file diff --git a/apps/api/src/Api/Common/Models/CurrentUser.cs b/apps/api/src/Api/Common/Models/CurrentUser.cs new file mode 100644 index 0000000..310cfd4 --- /dev/null +++ b/apps/api/src/Api/Common/Models/CurrentUser.cs @@ -0,0 +1,30 @@ +using System.Security.Claims; + +using Kijk.Api.Common.Exceptions; + +namespace Kijk.Api.Common.Models; + +public class CurrentUser +{ + private const string PermissionsClaim = "permissions"; + + public required ClaimsPrincipal Principal { get; set; } + + public required SimpleAuthUser User { get; set; } + + public Guid Id => this.User.Id; + + public string AuthId => this.User.AuthId ?? throw new NullException("'AuthId' not found"); + + public string Name => this.User.Name ?? throw new NullException("'Name' not found"); + + public string Email => this.User.Email ?? throw new NullException("'Upn/Email' not found"); + + public IEnumerable Permissions => Principal.FindAll(PermissionsClaim).Select(x => x.Value); + + public bool IsAdmin => this.Permissions.Contains(AppConstants.Roles.Admin); + + public bool IsUser => this.Permissions.Contains(AppConstants.Roles.Admin); + + public Guid ActiveHouseholdId => this.User.HouseholdId; +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Models/Dtos.cs b/apps/api/src/Api/Common/Models/Dtos.cs similarity index 75% rename from apps/api/Kijk.Api/Common/Models/Dtos.cs rename to apps/api/src/Api/Common/Models/Dtos.cs index a9e3850..6e7d450 100644 --- a/apps/api/Kijk.Api/Common/Models/Dtos.cs +++ b/apps/api/src/Api/Common/Models/Dtos.cs @@ -47,8 +47,16 @@ public static CategoryDto Create(Category category) => new(category.Id, category.Name, category.Color, category.Type, category.CreatorType); } -public record SimpleAuthUser(Guid Id, string AuthId, string Name, string? Email, bool? FirstTime = false) +public record SimpleAuthUser(Guid Id, string AuthId, Guid HouseholdId, string Name, string? Email, bool? FirstTime = false) { public static SimpleAuthUser Create(User user) => - new(user.Id, user.AuthId, user.Name, user.Email, user.FirstTime); + new(user.Id, user.AuthId, user.GetActiveHouseHoldId(), user.Name, user.Email, user.FirstTime); } + +public record EnergyConsumptionDto(Guid Id, string Name, string? Description, decimal Value, EnergyConsumptionType Type, DateTime CreatedAt) +{ + public static EnergyConsumptionDto Create(EnergyConsumption energyConsumption) => + new( + energyConsumption.Id, energyConsumption.Name, energyConsumption.Description, energyConsumption.Value, energyConsumption.Type, + energyConsumption.CreatedAt); +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Models/Enums.cs b/apps/api/src/Api/Common/Models/Enums.cs similarity index 99% rename from apps/api/Kijk.Api/Common/Models/Enums.cs rename to apps/api/src/Api/Common/Models/Enums.cs index d5aab36..e136453 100644 --- a/apps/api/Kijk.Api/Common/Models/Enums.cs +++ b/apps/api/src/Api/Common/Models/Enums.cs @@ -84,4 +84,4 @@ public enum BudgetStatus Active, Completed, Pending -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Models/ResponseStatus.cs b/apps/api/src/Api/Common/Models/ResponseStatus.cs similarity index 98% rename from apps/api/Kijk.Api/Common/Models/ResponseStatus.cs rename to apps/api/src/Api/Common/Models/ResponseStatus.cs index 15ee505..93f8a92 100644 --- a/apps/api/Kijk.Api/Common/Models/ResponseStatus.cs +++ b/apps/api/src/Api/Common/Models/ResponseStatus.cs @@ -7,4 +7,4 @@ public enum ResponseStatus { Success, Error -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Models/ResultTypes.cs b/apps/api/src/Api/Common/Models/ResultTypes.cs similarity index 99% rename from apps/api/Kijk.Api/Common/Models/ResultTypes.cs rename to apps/api/src/Api/Common/Models/ResultTypes.cs index 8e1eca7..1a18f06 100644 --- a/apps/api/Kijk.Api/Common/Models/ResultTypes.cs +++ b/apps/api/src/Api/Common/Models/ResultTypes.cs @@ -28,4 +28,4 @@ public enum SuccessType { Ok, Created -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Options/AuthOptions.cs b/apps/api/src/Api/Common/Options/AuthOptions.cs similarity index 99% rename from apps/api/Kijk.Api/Common/Options/AuthOptions.cs rename to apps/api/src/Api/Common/Options/AuthOptions.cs index adc5764..8e9d32d 100644 --- a/apps/api/Kijk.Api/Common/Options/AuthOptions.cs +++ b/apps/api/src/Api/Common/Options/AuthOptions.cs @@ -10,4 +10,4 @@ public class AuthOptions : IConfigOptions public string Authority { get; set; } = default!; public string AuthorizedParty { get; set; } = default!; -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Options/ConnectionOptions.cs b/apps/api/src/Api/Common/Options/ConnectionOptions.cs similarity index 99% rename from apps/api/Kijk.Api/Common/Options/ConnectionOptions.cs rename to apps/api/src/Api/Common/Options/ConnectionOptions.cs index 95c3157..e2fcd7b 100644 --- a/apps/api/Kijk.Api/Common/Options/ConnectionOptions.cs +++ b/apps/api/src/Api/Common/Options/ConnectionOptions.cs @@ -6,4 +6,4 @@ public class ConnectionOptions : IConfigOptions { public static string SectionName => "DefaultConnection"; -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Options/IConfigOption.cs b/apps/api/src/Api/Common/Options/IConfigOption.cs similarity index 98% rename from apps/api/Kijk.Api/Common/Options/IConfigOption.cs rename to apps/api/src/Api/Common/Options/IConfigOption.cs index 5372503..9196990 100644 --- a/apps/api/Kijk.Api/Common/Options/IConfigOption.cs +++ b/apps/api/src/Api/Common/Options/IConfigOption.cs @@ -6,4 +6,4 @@ public interface IConfigOptions { static abstract string SectionName { get; } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Utils/LoggerUtils.cs b/apps/api/src/Api/Common/Utils/LoggerUtils.cs similarity index 76% rename from apps/api/Kijk.Api/Common/Utils/LoggerUtils.cs rename to apps/api/src/Api/Common/Utils/LoggerUtils.cs index 6bf2c49..0d12923 100644 --- a/apps/api/Kijk.Api/Common/Utils/LoggerUtils.cs +++ b/apps/api/src/Api/Common/Utils/LoggerUtils.cs @@ -5,12 +5,9 @@ namespace Kijk.Api.Common.Utils; public static class LoggerUtils { - public static ReloadableLogger CreateRootLogger() - { - return new LoggerConfiguration() + public static ReloadableLogger CreateRootLogger() => new LoggerConfiguration() .MinimumLevel.Override("Microsoft", LogEventLevel.Information) .Enrich.FromLogContext() .WriteTo.Console(theme: AnsiConsoleTheme.Code) .CreateBootstrapLogger(); - } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Common/Utils/ResponseUtils.cs b/apps/api/src/Api/Common/Utils/ResponseUtils.cs similarity index 85% rename from apps/api/Kijk.Api/Common/Utils/ResponseUtils.cs rename to apps/api/src/Api/Common/Utils/ResponseUtils.cs index 7cf2472..3396b68 100644 --- a/apps/api/Kijk.Api/Common/Utils/ResponseUtils.cs +++ b/apps/api/src/Api/Common/Utils/ResponseUtils.cs @@ -26,8 +26,5 @@ public static int ToStatusCode(ErrorType errorType) return statusCode; } - public static IResult CreateTypedResult(ApiResponse response, int statusCode) - { - return TypedResults.Json(response, contentType: "application/json", statusCode: statusCode); - } -} + public static IResult CreateTypedResult(ApiResponse response, int statusCode) => TypedResults.Json(response, contentType: "application/json", statusCode: statusCode); +} \ No newline at end of file diff --git a/apps/api/src/Api/Dockerfile b/apps/api/src/Api/Dockerfile new file mode 100644 index 0000000..9bbb7d9 --- /dev/null +++ b/apps/api/src/Api/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Api/Api.csproj", "Api/"] +RUN dotnet restore "Api/Api.csproj" +COPY . . +WORKDIR "/src/Kijk.Api" +RUN dotnet build "Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Api.dll"] diff --git a/apps/api/Kijk.Api/Domain/BaseEntity.cs b/apps/api/src/Api/Domain/BaseEntity.cs similarity index 99% rename from apps/api/Kijk.Api/Domain/BaseEntity.cs rename to apps/api/src/Api/Domain/BaseEntity.cs index d639b96..6fe76f5 100644 --- a/apps/api/Kijk.Api/Domain/BaseEntity.cs +++ b/apps/api/src/Api/Domain/BaseEntity.cs @@ -9,4 +9,4 @@ public abstract class BaseEntity public DateTime? UpdatedAt { get; set; } public DateTime? DeletedAt { get; set; } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/Account.cs b/apps/api/src/Api/Domain/Entities/Account.cs similarity index 99% rename from apps/api/Kijk.Api/Domain/Entities/Account.cs rename to apps/api/src/Api/Domain/Entities/Account.cs index b77d921..09affad 100644 --- a/apps/api/Kijk.Api/Domain/Entities/Account.cs +++ b/apps/api/src/Api/Domain/Entities/Account.cs @@ -35,4 +35,4 @@ public static Account Create(string name, Guid userId, Guid householdId, Account HouseholdId = householdId, UserId = userId }; -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/Budget.cs b/apps/api/src/Api/Domain/Entities/Budget.cs similarity index 94% rename from apps/api/Kijk.Api/Domain/Entities/Budget.cs rename to apps/api/src/Api/Domain/Entities/Budget.cs index d81b06a..9a59385 100644 --- a/apps/api/Kijk.Api/Domain/Entities/Budget.cs +++ b/apps/api/src/Api/Domain/Entities/Budget.cs @@ -31,9 +31,7 @@ public static Budget Create( Guid householdId, Guid userId, Category category, - DateOnly? endDate = default) - { - return new() + DateOnly? endDate = default) => new() { Id = Guid.NewGuid(), Name = name, @@ -47,5 +45,4 @@ public static Budget Create( UserId = userId, Category = category }; - } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/Category.cs b/apps/api/src/Api/Domain/Entities/Category.cs similarity index 99% rename from apps/api/Kijk.Api/Domain/Entities/Category.cs rename to apps/api/src/Api/Domain/Entities/Category.cs index d683d8b..5d656f8 100644 --- a/apps/api/Kijk.Api/Domain/Entities/Category.cs +++ b/apps/api/src/Api/Domain/Entities/Category.cs @@ -34,4 +34,4 @@ public static Category Create( Type = type, Users = user is null ? [] : [user] }; -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/EnergyConsumption.cs b/apps/api/src/Api/Domain/Entities/EnergyConsumption.cs similarity index 75% rename from apps/api/Kijk.Api/Domain/Entities/EnergyConsumption.cs rename to apps/api/src/Api/Domain/Entities/EnergyConsumption.cs index 7aebd42..56dce70 100644 --- a/apps/api/Kijk.Api/Domain/Entities/EnergyConsumption.cs +++ b/apps/api/src/Api/Domain/Entities/EnergyConsumption.cs @@ -9,6 +9,11 @@ public sealed class EnergyConsumption : BaseEntity public required decimal Value { get; set; } public required EnergyConsumptionType Type { get; set; } + /// + /// Represents the date of the energy consumption. + /// + public required DateTime Date { get; set; } + public Guid HouseholdId { get; set; } public static EnergyConsumption Create( @@ -16,16 +21,15 @@ public static EnergyConsumption Create( EnergyConsumptionType type, decimal value, Guid householdId, - string? description = default) - { - return new() + DateTime date, + string? description = default) => new() { Id = Guid.NewGuid(), Name = name, Description = description, Type = type, Value = value, + Date = date, HouseholdId = householdId }; - } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/EnergyConsumptionLimit.cs b/apps/api/src/Api/Domain/Entities/EnergyConsumptionLimit.cs similarity index 94% rename from apps/api/Kijk.Api/Domain/Entities/EnergyConsumptionLimit.cs rename to apps/api/src/Api/Domain/Entities/EnergyConsumptionLimit.cs index a4b048d..7059fa5 100644 --- a/apps/api/Kijk.Api/Domain/Entities/EnergyConsumptionLimit.cs +++ b/apps/api/src/Api/Domain/Entities/EnergyConsumptionLimit.cs @@ -26,9 +26,7 @@ public static EnergyConsumptionLimit Create( DateTime lastOccurrence, User createdBy, Guid householdId, - string? description = default) - { - return new() + string? description = default) => new() { Id = Guid.NewGuid(), Name = name, @@ -41,6 +39,5 @@ public static EnergyConsumptionLimit Create( CreatedBy = createdBy, HouseholdId = householdId }; - } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/Household.cs b/apps/api/src/Api/Domain/Entities/Household.cs similarity index 99% rename from apps/api/Kijk.Api/Domain/Entities/Household.cs rename to apps/api/src/Api/Domain/Entities/Household.cs index 0ae06c1..4d185bf 100644 --- a/apps/api/Kijk.Api/Domain/Entities/Household.cs +++ b/apps/api/src/Api/Domain/Entities/Household.cs @@ -22,4 +22,4 @@ public static Household Create(string name, bool isActive, string? description = Description = description, IsActive = isActive }; -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/RecurringTransactions.cs b/apps/api/src/Api/Domain/Entities/RecurringTransactions.cs similarity index 95% rename from apps/api/Kijk.Api/Domain/Entities/RecurringTransactions.cs rename to apps/api/src/Api/Domain/Entities/RecurringTransactions.cs index 22a1ef1..0d06250 100644 --- a/apps/api/Kijk.Api/Domain/Entities/RecurringTransactions.cs +++ b/apps/api/src/Api/Domain/Entities/RecurringTransactions.cs @@ -42,9 +42,7 @@ public static RecurringTransactions Create( Account account, Guid householdId, Frequency frequency, - DateOnly? endDate = default) - { - return new() + DateOnly? endDate = default) => new() { Id = Guid.NewGuid(), Name = name, @@ -57,5 +55,4 @@ public static RecurringTransactions Create( Frequency = frequency, HouseholdId = householdId }; - } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/Transaction.cs b/apps/api/src/Api/Domain/Entities/Transaction.cs similarity index 94% rename from apps/api/Kijk.Api/Domain/Entities/Transaction.cs rename to apps/api/src/Api/Domain/Entities/Transaction.cs index 530f9a6..7ef3364 100644 --- a/apps/api/Kijk.Api/Domain/Entities/Transaction.cs +++ b/apps/api/src/Api/Domain/Entities/Transaction.cs @@ -25,9 +25,7 @@ public static Transaction Create( TransactionType type, DateTime executedAt, Account account, - Category category) - { - return new() + Category category) => new() { Id = Guid.NewGuid(), Name = name, @@ -38,5 +36,4 @@ public static Transaction Create( Account = account, Category = category }; - } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/User.cs b/apps/api/src/Api/Domain/Entities/User.cs similarity index 79% rename from apps/api/Kijk.Api/Domain/Entities/User.cs rename to apps/api/src/Api/Domain/Entities/User.cs index 688999b..d13ad51 100644 --- a/apps/api/Kijk.Api/Domain/Entities/User.cs +++ b/apps/api/src/Api/Domain/Entities/User.cs @@ -19,6 +19,12 @@ public sealed class User : BaseEntity public List Categories { get; set; } = []; + /// + /// It should never be empty as it is set when the user is created. + /// + /// + public Guid GetActiveHouseHoldId() => UserHouseholds.Find(x => x.IsDefault)?.HouseholdId ?? Guid.Empty; + public User SetDefaultCategories(bool? useDefaultCategories, List defaultCategories) { if (useDefaultCategories == true) @@ -38,16 +44,13 @@ public static User Create( string name, string? email, List? categories = null, - bool firstTime = false) - { - return new User + bool firstTime = false) => new() { Id = Guid.NewGuid(), AuthId = authId, Name = name, Email = email, FirstTime = firstTime, - Categories = categories ?? [], + Categories = categories ?? [] }; - } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Domain/Entities/UserHousehold.cs b/apps/api/src/Api/Domain/Entities/UserHousehold.cs similarity index 99% rename from apps/api/Kijk.Api/Domain/Entities/UserHousehold.cs rename to apps/api/src/Api/Domain/Entities/UserHousehold.cs index cd90332..7f87264 100644 --- a/apps/api/Kijk.Api/Domain/Entities/UserHousehold.cs +++ b/apps/api/src/Api/Domain/Entities/UserHousehold.cs @@ -25,4 +25,4 @@ public static UserHousehold Create(User user, Household household, Role role, bo Role = role, IsDefault = isDefault, }; -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Endpoints/CategoriesEndpoint.cs b/apps/api/src/Api/Endpoints/CategoriesEndpoint.cs similarity index 99% rename from apps/api/Kijk.Api/Endpoints/CategoriesEndpoint.cs rename to apps/api/src/Api/Endpoints/CategoriesEndpoint.cs index 5892d48..9fa6821 100644 --- a/apps/api/Kijk.Api/Endpoints/CategoriesEndpoint.cs +++ b/apps/api/src/Api/Endpoints/CategoriesEndpoint.cs @@ -22,4 +22,4 @@ public static IEndpointRouteBuilder MapCategoriesApi(this IEndpointRouteBuilder return endpointRouteBuilder; } -} +} \ No newline at end of file diff --git a/apps/api/src/Api/Endpoints/EnergyConsumptionsEndpoint.cs b/apps/api/src/Api/Endpoints/EnergyConsumptionsEndpoint.cs new file mode 100644 index 0000000..2106543 --- /dev/null +++ b/apps/api/src/Api/Endpoints/EnergyConsumptionsEndpoint.cs @@ -0,0 +1,14 @@ +namespace Kijk.Api.Endpoints; + +public static class EnergyConsumptionsEndpoint +{ + public static IEndpointRouteBuilder MaEnergyConsumptionsEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) + { + var group = endpointRouteBuilder.MapGroup("/energy-consumptions") + .WithTags("EnergyConsumptions"); + + + + return endpointRouteBuilder; + } +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Endpoints/TransactionsEndpoint.cs b/apps/api/src/Api/Endpoints/TransactionsEndpoint.cs similarity index 99% rename from apps/api/Kijk.Api/Endpoints/TransactionsEndpoint.cs rename to apps/api/src/Api/Endpoints/TransactionsEndpoint.cs index 7c9f61d..95c78e6 100644 --- a/apps/api/Kijk.Api/Endpoints/TransactionsEndpoint.cs +++ b/apps/api/src/Api/Endpoints/TransactionsEndpoint.cs @@ -18,4 +18,4 @@ public static IEndpointRouteBuilder MapTransactionsEndpoints(this IEndpointRoute return endpointRouteBuilder; } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Endpoints/UsersEndpoint.cs b/apps/api/src/Api/Endpoints/UsersEndpoint.cs similarity index 99% rename from apps/api/Kijk.Api/Endpoints/UsersEndpoint.cs rename to apps/api/src/Api/Endpoints/UsersEndpoint.cs index c2b545e..fd36228 100644 --- a/apps/api/Kijk.Api/Endpoints/UsersEndpoint.cs +++ b/apps/api/src/Api/Endpoints/UsersEndpoint.cs @@ -16,4 +16,4 @@ public static IEndpointRouteBuilder MapUsersApi(this IEndpointRouteBuilder endpo return endpointRouteBuilder; } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/GlobalUsings.cs b/apps/api/src/Api/GlobalUsings.cs similarity index 81% rename from apps/api/Kijk.Api/GlobalUsings.cs rename to apps/api/src/Api/GlobalUsings.cs index ad4cc3a..c666723 100644 --- a/apps/api/Kijk.Api/GlobalUsings.cs +++ b/apps/api/src/Api/GlobalUsings.cs @@ -6,4 +6,4 @@ global using Serilog; global using Serilog.Events; -global using ILogger = Serilog.ILogger; +global using ILogger = Serilog.ILogger; \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/App/AppModule.cs b/apps/api/src/Api/Modules/App/AppModule.cs similarity index 67% rename from apps/api/Kijk.Api/Modules/App/AppModule.cs rename to apps/api/src/Api/Modules/App/AppModule.cs index 850ffa6..92ecd84 100644 --- a/apps/api/Kijk.Api/Modules/App/AppModule.cs +++ b/apps/api/src/Api/Modules/App/AppModule.cs @@ -2,8 +2,5 @@ public static class AppModule { - public static IServiceCollection RegisterAppModule(this IServiceCollection services) - { - return services; - } -} + public static IServiceCollection RegisterAppModule(this IServiceCollection services) => services; +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Categories/CategoriesModule.cs b/apps/api/src/Api/Modules/Categories/CategoriesModule.cs similarity index 99% rename from apps/api/Kijk.Api/Modules/Categories/CategoriesModule.cs rename to apps/api/src/Api/Modules/Categories/CategoriesModule.cs index 0a2e913..0349e65 100644 --- a/apps/api/Kijk.Api/Modules/Categories/CategoriesModule.cs +++ b/apps/api/src/Api/Modules/Categories/CategoriesModule.cs @@ -8,4 +8,4 @@ public static IServiceCollection AddCategoriesModule(this IServiceCollection ser return services; } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Categories/CreateCategory.Validator.cs b/apps/api/src/Api/Modules/Categories/CreateCategory.Validator.cs similarity index 89% rename from apps/api/Kijk.Api/Modules/Categories/CreateCategory.Validator.cs rename to apps/api/src/Api/Modules/Categories/CreateCategory.Validator.cs index 158dcb3..d319c59 100644 --- a/apps/api/Kijk.Api/Modules/Categories/CreateCategory.Validator.cs +++ b/apps/api/src/Api/Modules/Categories/CreateCategory.Validator.cs @@ -1,6 +1,6 @@ -// using Kijk.Api.Common.Models; +// using Api.Common.Models; // -// namespace Kijk.Api.Modules.Categories; +// namespace Api.Modules.Categories; // // public class CreateCategoryValidator : AbstractValidator // { @@ -14,4 +14,4 @@ // .NotEmpty().WithErrorCode(AppErrorCodes.ValidationError).WithMessage("'Color' must be set") // .Must(x => x.StartsWith("#")).WithErrorCode(AppErrorCodes.ValidationError).WithMessage("'Color' must start with a '#'"); // } -// } +// } \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Categories/CreateCategory.cs b/apps/api/src/Api/Modules/Categories/CreateCategory.cs similarity index 95% rename from apps/api/Kijk.Api/Modules/Categories/CreateCategory.cs rename to apps/api/src/Api/Modules/Categories/CreateCategory.cs index 443239a..f512d28 100644 --- a/apps/api/Kijk.Api/Modules/Categories/CreateCategory.cs +++ b/apps/api/src/Api/Modules/Categories/CreateCategory.cs @@ -1,5 +1,4 @@ -using Kijk.Api.Common.Extensions; -using Kijk.Api.Common.Models; +using Kijk.Api.Common.Models; using Kijk.Api.Domain.Entities; using Kijk.Api.Persistence; @@ -17,7 +16,7 @@ public CreateCategoryValidator() RuleFor(x => x.Color) .NotEmpty().WithErrorCode(AppErrorCodes.ValidationError).WithMessage("'Color' must be set") - .Must(x => x.StartsWith("#")).WithErrorCode(AppErrorCodes.ValidationError).WithMessage("'Color' must start with a '#'"); + .Must(x => x.StartsWith('#')).WithErrorCode(AppErrorCodes.ValidationError).WithMessage("'Color' must start with a '#'"); } } @@ -78,7 +77,7 @@ private static async Task Handle( return TypedResults.NotFound(ApiResponseBuilder.Error($"User with id '{currentUser.Id}' was not found")); } - if (user.Categories.Any(c => string.Equals(c.Name, createCategoryRequest.Name, StringComparison.CurrentCultureIgnoreCase))) + if (user.Categories.Any(c => string.Equals(c.Name, createCategoryRequest.Name, StringComparison.OrdinalIgnoreCase))) { return TypedResults.Conflict(ApiResponseBuilder.Error($"A category with the name '{createCategoryRequest.Name}' already exists")); } @@ -96,4 +95,4 @@ private static async Task Handle( return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Categories/DeleteCategory.cs b/apps/api/src/Api/Modules/Categories/DeleteCategory.cs similarity index 99% rename from apps/api/Kijk.Api/Modules/Categories/DeleteCategory.cs rename to apps/api/src/Api/Modules/Categories/DeleteCategory.cs index 85d39a2..60cc029 100644 --- a/apps/api/Kijk.Api/Modules/Categories/DeleteCategory.cs +++ b/apps/api/src/Api/Modules/Categories/DeleteCategory.cs @@ -74,4 +74,4 @@ private static async Task Handle(Guid id, AppDbContext dbContext, Curre return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Categories/GetAllCategories.cs b/apps/api/src/Api/Modules/Categories/GetAllCategories.cs similarity index 99% rename from apps/api/Kijk.Api/Modules/Categories/GetAllCategories.cs rename to apps/api/src/Api/Modules/Categories/GetAllCategories.cs index 8035f33..7b4570d 100644 --- a/apps/api/Kijk.Api/Modules/Categories/GetAllCategories.cs +++ b/apps/api/src/Api/Modules/Categories/GetAllCategories.cs @@ -52,4 +52,4 @@ private static async Task Handle(AppDbContext dbContext, CurrentUser cu return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Categories/UpdateCategory.cs b/apps/api/src/Api/Modules/Categories/UpdateCategory.cs similarity index 99% rename from apps/api/Kijk.Api/Modules/Categories/UpdateCategory.cs rename to apps/api/src/Api/Modules/Categories/UpdateCategory.cs index 6bc6138..2b5a8d9 100644 --- a/apps/api/Kijk.Api/Modules/Categories/UpdateCategory.cs +++ b/apps/api/src/Api/Modules/Categories/UpdateCategory.cs @@ -75,4 +75,4 @@ private static async Task Handle( return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/src/Api/Modules/EnergyConsumptions/EnergyConsumptionsModule.cs b/apps/api/src/Api/Modules/EnergyConsumptions/EnergyConsumptionsModule.cs new file mode 100644 index 0000000..2313db6 --- /dev/null +++ b/apps/api/src/Api/Modules/EnergyConsumptions/EnergyConsumptionsModule.cs @@ -0,0 +1,9 @@ +namespace Kijk.Api.Modules.EnergyConsumptions; + +public static class EnergyConsumptionsModule +{ + public static IServiceCollection AddEnergyConsumptionsModule(this IServiceCollection services) => + // services.AddScoped, CreateTransactionsValidator>(); + + services; +} \ No newline at end of file diff --git a/apps/api/src/Api/Modules/EnergyConsumptions/GetByEnergyConsumptions.cs b/apps/api/src/Api/Modules/EnergyConsumptions/GetByEnergyConsumptions.cs new file mode 100644 index 0000000..7b6c62a --- /dev/null +++ b/apps/api/src/Api/Modules/EnergyConsumptions/GetByEnergyConsumptions.cs @@ -0,0 +1,72 @@ +using System.Globalization; + +using Kijk.Api.Common.Extensions; +using Kijk.Api.Common.Models; +using Kijk.Api.Persistence; + +using Microsoft.AspNetCore.Mvc; + +namespace Kijk.Api.Modules.EnergyConsumptions; + +public static class GetByEnergyConsumptions +{ + private static readonly ILogger Logger = Log.ForContext(typeof(GetByEnergyConsumptions)); + + public static RouteGroupBuilder MapGetByEnergyConsumptions(this RouteGroupBuilder groupBuilder) + { + groupBuilder.MapGet("/", HandleAsync) + .Produces>>() + .Produces>>(StatusCodes.Status400BadRequest) + .Produces>>(StatusCodes.Status404NotFound); + + return groupBuilder; + } + + /// + /// Retrieves all energy consumptions for the current user by year, month and type. + /// + /// + /// + /// + /// + /// + /// + /// + private static async Task HandleAsync( + [FromQuery(Name = "year")] int? year, + [FromQuery(Name = "month")] string? month, + [FromQuery(Name = "type")] string? type, + AppDbContext dbContext, + CurrentUser currentUser, + CancellationToken cancellationToken) + { + try + { + var monthInt = month is not null ? DateTime.ParseExact(month, "MMMM", CultureInfo.InvariantCulture).Month : -1; + + var typeExists = Enum.TryParse(type, true, out var realType); + + var response = await dbContext.EnergyConsumptions + .AsNoTracking() + .Where(x => x.HouseholdId == currentUser.ActiveHouseholdId) + .If(year != null, q => q.Where(x => x.Date.Year == year)) + .If(monthInt != -1, q => q.Where(x => x.Date.Month == monthInt)) + .If(typeExists, q => q.Where(x => x.Type == realType)) + .Select( + x => new EnergyConsumptionDto( + x.Id, + x.Name, + x.Description, + x.Value, + x.Type, x.CreatedAt)) + .ToListAsync(cancellationToken); + + return TypedResults.Ok(ApiResponseBuilder.Success(response)); + } + catch (Exception e) + { + Logger.Warning(e, "Error: {Error}", e.Message); + return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); + } + } +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Transactions/CreateTransaction.cs b/apps/api/src/Api/Modules/Transactions/CreateTransaction.cs similarity index 97% rename from apps/api/Kijk.Api/Modules/Transactions/CreateTransaction.cs rename to apps/api/src/Api/Modules/Transactions/CreateTransaction.cs index 3d65315..7c7a977 100644 --- a/apps/api/Kijk.Api/Modules/Transactions/CreateTransaction.cs +++ b/apps/api/src/Api/Modules/Transactions/CreateTransaction.cs @@ -69,7 +69,7 @@ private static async Task Handle( var accountId = createTransactionRequest.AccountId; if (accountId == null || accountId == Guid.Empty) { - accountId = user.Accounts.FirstOrDefault(x => x.Name == AppConstants.DefaultValues.AccountName)?.Id; + accountId = user.Accounts.Find(x => x.Name == AppConstants.DefaultValues.AccountName)?.Id; } var account = await dbContext.Accounts @@ -141,4 +141,4 @@ private static async Task Handle( return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Transactions/DeleteTransaction.cs b/apps/api/src/Api/Modules/Transactions/DeleteTransaction.cs similarity index 99% rename from apps/api/Kijk.Api/Modules/Transactions/DeleteTransaction.cs rename to apps/api/src/Api/Modules/Transactions/DeleteTransaction.cs index 704a47b..3d849e1 100644 --- a/apps/api/Kijk.Api/Modules/Transactions/DeleteTransaction.cs +++ b/apps/api/src/Api/Modules/Transactions/DeleteTransaction.cs @@ -50,4 +50,4 @@ private static async Task Handle( return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Transactions/GetByIdTransaction.cs b/apps/api/src/Api/Modules/Transactions/GetByIdTransaction.cs similarity index 99% rename from apps/api/Kijk.Api/Modules/Transactions/GetByIdTransaction.cs rename to apps/api/src/Api/Modules/Transactions/GetByIdTransaction.cs index 054595a..ab920da 100644 --- a/apps/api/Kijk.Api/Modules/Transactions/GetByIdTransaction.cs +++ b/apps/api/src/Api/Modules/Transactions/GetByIdTransaction.cs @@ -53,4 +53,4 @@ private static async Task Handle(Guid id, AppDbContext dbContext, Cance return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Transactions/GetByTransactions.cs b/apps/api/src/Api/Modules/Transactions/GetByTransactions.cs similarity index 93% rename from apps/api/Kijk.Api/Modules/Transactions/GetByTransactions.cs rename to apps/api/src/Api/Modules/Transactions/GetByTransactions.cs index 770be8a..a5ac820 100644 --- a/apps/api/Kijk.Api/Modules/Transactions/GetByTransactions.cs +++ b/apps/api/src/Api/Modules/Transactions/GetByTransactions.cs @@ -1,7 +1,9 @@ using System.Globalization; + using Kijk.Api.Common.Extensions; using Kijk.Api.Common.Models; using Kijk.Api.Persistence; + using Microsoft.AspNetCore.Mvc; namespace Kijk.Api.Modules.Transactions; @@ -12,7 +14,7 @@ public static class GetByTransactions public static RouteGroupBuilder MapGetByTransactions(this RouteGroupBuilder groupBuilder) { - groupBuilder.MapGet("/", Handle) + groupBuilder.MapGet("/", HandleAsync) .Produces>>() .Produces>>(StatusCodes.Status400BadRequest) .Produces>>(StatusCodes.Status404NotFound); @@ -21,7 +23,7 @@ public static RouteGroupBuilder MapGetByTransactions(this RouteGroupBuilder grou } /// - /// Retrieves all transactions for the current user by year and month. + /// Retrieves all transactions for the current user by year and month. /// /// /// @@ -29,7 +31,7 @@ public static RouteGroupBuilder MapGetByTransactions(this RouteGroupBuilder grou /// /// /// - private static async Task Handle( + private static async Task HandleAsync( [FromQuery(Name = "year")] int? year, [FromQuery(Name = "month")] string? month, AppDbContext dbContext, @@ -69,4 +71,4 @@ private static async Task Handle( return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Transactions/GetYearsFromTransactions.cs b/apps/api/src/Api/Modules/Transactions/GetYearsFromTransactions.cs similarity index 83% rename from apps/api/Kijk.Api/Modules/Transactions/GetYearsFromTransactions.cs rename to apps/api/src/Api/Modules/Transactions/GetYearsFromTransactions.cs index a5d03ac..a3e1359 100644 --- a/apps/api/Kijk.Api/Modules/Transactions/GetYearsFromTransactions.cs +++ b/apps/api/src/Api/Modules/Transactions/GetYearsFromTransactions.cs @@ -3,7 +3,7 @@ namespace Kijk.Api.Modules.Transactions; -file record YearDto(List Years); +sealed file record YearsResponse(List Years); public static class GetYearsFromTransactions { @@ -22,16 +22,11 @@ public static RouteGroupBuilder MapGetYearsFromTransactions(this RouteGroupBuild /// /// Retrieves all years that have transactions and all years in between. /// - /// /// /// /// /// - private static async Task Handle( - HttpRequest request, - AppDbContext dbContext, - CurrentUser currentUser, - CancellationToken cancellationToken) + private static async Task Handle(AppDbContext dbContext, CurrentUser currentUser, CancellationToken cancellationToken) { try { @@ -52,12 +47,12 @@ private static async Task Handle( List years = []; var currentYear = DateTime.UtcNow.Year; var minDate = yearsWithTransactions.Count > 0 ? yearsWithTransactions.Min() : currentYear; - for (int i = minDate; i <= currentYear; i++) + for (var i = minDate; i <= currentYear; i++) { years.Add(i); } - var response = new YearDto(years.OrderByDescending(x => x).ToList()); + var response = new YearsResponse([.. years.OrderByDescending(x => x)]); return TypedResults.Ok(ApiResponseBuilder.Success(response)); } catch (Exception e) @@ -66,4 +61,4 @@ private static async Task Handle( return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Transactions/TransactionsModule.cs b/apps/api/src/Api/Modules/Transactions/TransactionsModule.cs similarity index 99% rename from apps/api/Kijk.Api/Modules/Transactions/TransactionsModule.cs rename to apps/api/src/Api/Modules/Transactions/TransactionsModule.cs index 2416d96..9df9cf3 100644 --- a/apps/api/Kijk.Api/Modules/Transactions/TransactionsModule.cs +++ b/apps/api/src/Api/Modules/Transactions/TransactionsModule.cs @@ -8,4 +8,4 @@ public static IServiceCollection AddTransactionsModule(this IServiceCollection s return services; } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Transactions/UpdateTransaction.cs b/apps/api/src/Api/Modules/Transactions/UpdateTransaction.cs similarity index 99% rename from apps/api/Kijk.Api/Modules/Transactions/UpdateTransaction.cs rename to apps/api/src/Api/Modules/Transactions/UpdateTransaction.cs index bf5ec4d..d8f704e 100644 --- a/apps/api/Kijk.Api/Modules/Transactions/UpdateTransaction.cs +++ b/apps/api/src/Api/Modules/Transactions/UpdateTransaction.cs @@ -86,4 +86,4 @@ private static async Task Handle( return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Users/GetMeUser.cs b/apps/api/src/Api/Modules/Users/GetMeUser.cs similarity index 98% rename from apps/api/Kijk.Api/Modules/Users/GetMeUser.cs rename to apps/api/src/Api/Modules/Users/GetMeUser.cs index 433b053..735ccc6 100644 --- a/apps/api/Kijk.Api/Modules/Users/GetMeUser.cs +++ b/apps/api/src/Api/Modules/Users/GetMeUser.cs @@ -3,7 +3,7 @@ namespace Kijk.Api.Modules.Users; -file record GetMeUserResponse( +sealed file record GetMeUserResponse( Guid Id, string? AuthId, string? Name, @@ -69,4 +69,4 @@ private static async Task Handle(AppDbContext dbContext, CurrentUser cu return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Users/SignInUser.cs b/apps/api/src/Api/Modules/Users/SignInUser.cs similarity index 98% rename from apps/api/Kijk.Api/Modules/Users/SignInUser.cs rename to apps/api/src/Api/Modules/Users/SignInUser.cs index 03a4992..1edff45 100644 --- a/apps/api/Kijk.Api/Modules/Users/SignInUser.cs +++ b/apps/api/src/Api/Modules/Users/SignInUser.cs @@ -1,11 +1,12 @@ using System.Security.Cryptography; + using Kijk.Api.Common.Models; using Kijk.Api.Domain.Entities; using Kijk.Api.Persistence; namespace Kijk.Api.Modules.Users; -file record SignInUserResponse( +sealed file record SignInUserResponse( Guid Id, string? AuthId, string? Name, @@ -96,4 +97,4 @@ private static async Task Handle(AppDbContext dbContext, CurrentUser cu return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Users/UpdateUser.cs b/apps/api/src/Api/Modules/Users/UpdateUser.cs similarity index 98% rename from apps/api/Kijk.Api/Modules/Users/UpdateUser.cs rename to apps/api/src/Api/Modules/Users/UpdateUser.cs index 58bfdf0..cdd4ff3 100644 --- a/apps/api/Kijk.Api/Modules/Users/UpdateUser.cs +++ b/apps/api/src/Api/Modules/Users/UpdateUser.cs @@ -5,7 +5,7 @@ namespace Kijk.Api.Modules.Users; public record UpdateUserRequest(string? UserName, bool? UseDefaultCategories); -file record UserUpdateResponse( +sealed file record UserUpdateResponse( Guid Id, string? AuthId, string? Name, @@ -82,4 +82,4 @@ private static async Task Handle( return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Users/UsersModule.cs b/apps/api/src/Api/Modules/Users/UsersModule.cs similarity index 69% rename from apps/api/Kijk.Api/Modules/Users/UsersModule.cs rename to apps/api/src/Api/Modules/Users/UsersModule.cs index 79b8e62..b8d5310 100644 --- a/apps/api/Kijk.Api/Modules/Users/UsersModule.cs +++ b/apps/api/src/Api/Modules/Users/UsersModule.cs @@ -2,8 +2,5 @@ public static class UsersModule { - public static IServiceCollection AddUsersModule(this IServiceCollection services) - { - return services; - } -} + public static IServiceCollection AddUsersModule(this IServiceCollection services) => services; +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Modules/Users/WelcomeUser.cs b/apps/api/src/Api/Modules/Users/WelcomeUser.cs similarity index 98% rename from apps/api/Kijk.Api/Modules/Users/WelcomeUser.cs rename to apps/api/src/Api/Modules/Users/WelcomeUser.cs index ded8c6d..1e4326f 100644 --- a/apps/api/Kijk.Api/Modules/Users/WelcomeUser.cs +++ b/apps/api/src/Api/Modules/Users/WelcomeUser.cs @@ -5,7 +5,7 @@ namespace Kijk.Api.Modules.Users; public record WelcomeUserRequest(string? UserName, bool? UseDefaultCategories); -file record WelcomeUserResponse( +sealed file record WelcomeUserResponse( Guid Id, string? AuthId, string? Name, @@ -85,4 +85,4 @@ private static async Task Handle( return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/AppDbContext.cs b/apps/api/src/Api/Persistence/AppDbContext.cs similarity index 63% rename from apps/api/Kijk.Api/Persistence/AppDbContext.cs rename to apps/api/src/Api/Persistence/AppDbContext.cs index 2236a85..4f9d7da 100644 --- a/apps/api/Kijk.Api/Persistence/AppDbContext.cs +++ b/apps/api/src/Api/Persistence/AppDbContext.cs @@ -1,11 +1,8 @@ -using EntityFramework.Exceptions.PostgreSQL; -using Kijk.Api.Common.Options; -using Kijk.Api.Domain.Entities; -using Microsoft.EntityFrameworkCore.Diagnostics; +using Kijk.Api.Domain.Entities; namespace Kijk.Api.Persistence; -public class AppDbContext(IConfiguration configuration, IServiceProvider serviceProvider) : DbContext +public class AppDbContext(DbContextOptions options) : DbContext(options) { public DbSet Households => Set(); public DbSet UserHouseholds => Set(); @@ -24,20 +21,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } - protected override void OnConfiguring(DbContextOptionsBuilder options) - { - options.UseNpgsql(configuration.GetConnectionString(ConnectionOptions.SectionName)) - .UseExceptionProcessor() - .UseSnakeCaseNamingConvention() - .AddInterceptors(serviceProvider.GetServices()); - } - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { configurationBuilder .Properties() - .HaveConversion(typeof(UtcDateTimeConverter)); + .HaveConversion(); base.ConfigureConventions(configurationBuilder); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/AppDbInitializer.cs b/apps/api/src/Api/Persistence/AppDbInitializer.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/AppDbInitializer.cs rename to apps/api/src/Api/Persistence/AppDbInitializer.cs index 431e844..49440df 100644 --- a/apps/api/Kijk.Api/Persistence/AppDbInitializer.cs +++ b/apps/api/src/Api/Persistence/AppDbInitializer.cs @@ -53,4 +53,4 @@ public static void InitDb(AppDbContext dbContext) throw; } } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/AccountConfig.cs b/apps/api/src/Api/Persistence/Configs/AccountConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/AccountConfig.cs rename to apps/api/src/Api/Persistence/Configs/AccountConfig.cs index 7b4f3e7..274a44a 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/AccountConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/AccountConfig.cs @@ -1,4 +1,5 @@ using Kijk.Api.Domain.Entities; + using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Kijk.Api.Persistence.Configs; @@ -24,4 +25,4 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(x => x.AccountId) .OnDelete(DeleteBehavior.Cascade); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/BudgetConfig.cs b/apps/api/src/Api/Persistence/Configs/BudgetConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/BudgetConfig.cs rename to apps/api/src/Api/Persistence/Configs/BudgetConfig.cs index eb1fe0f..59a06cf 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/BudgetConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/BudgetConfig.cs @@ -1,4 +1,5 @@ using Kijk.Api.Domain.Entities; + using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Kijk.Api.Persistence.Configs; @@ -16,4 +17,4 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Visibility).HasConversion(); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/CategoryConfig.cs b/apps/api/src/Api/Persistence/Configs/CategoryConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/CategoryConfig.cs rename to apps/api/src/Api/Persistence/Configs/CategoryConfig.cs index bda1dc4..37d97f6 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/CategoryConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/CategoryConfig.cs @@ -17,6 +17,5 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Color).HasDefaultValue(AppConstants.Colors.Default); builder.Property(x => x.Type).HasConversion(); builder.Property(x => x.CreatorType).HasConversion(); - } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/EnergyConsumptionConfig.cs b/apps/api/src/Api/Persistence/Configs/EnergyConsumptionConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/EnergyConsumptionConfig.cs rename to apps/api/src/Api/Persistence/Configs/EnergyConsumptionConfig.cs index 086db03..a67c1a4 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/EnergyConsumptionConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/EnergyConsumptionConfig.cs @@ -1,4 +1,5 @@ using Kijk.Api.Domain.Entities; + using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Kijk.Api.Persistence.Configs; @@ -15,4 +16,4 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Type).HasConversion(); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/EnergyConsumptionLimitConfig.cs b/apps/api/src/Api/Persistence/Configs/EnergyConsumptionLimitConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/EnergyConsumptionLimitConfig.cs rename to apps/api/src/Api/Persistence/Configs/EnergyConsumptionLimitConfig.cs index 2bb1ea8..430311d 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/EnergyConsumptionLimitConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/EnergyConsumptionLimitConfig.cs @@ -1,4 +1,5 @@ using Kijk.Api.Domain.Entities; + using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Kijk.Api.Persistence.Configs; @@ -19,4 +20,4 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(x => x.CreatedById) .OnDelete(DeleteBehavior.Restrict); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/HouseholdConfig.cs b/apps/api/src/Api/Persistence/Configs/HouseholdConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/HouseholdConfig.cs rename to apps/api/src/Api/Persistence/Configs/HouseholdConfig.cs index 5ab342e..af96ac9 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/HouseholdConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/HouseholdConfig.cs @@ -1,4 +1,5 @@ using Kijk.Api.Domain.Entities; + using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Kijk.Api.Persistence.Configs; @@ -28,4 +29,4 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(x => x.HouseholdId) .OnDelete(DeleteBehavior.Cascade); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/RecurringTransactionConfig.cs b/apps/api/src/Api/Persistence/Configs/RecurringTransactionConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/RecurringTransactionConfig.cs rename to apps/api/src/Api/Persistence/Configs/RecurringTransactionConfig.cs index fdba495..2347c14 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/RecurringTransactionConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/RecurringTransactionConfig.cs @@ -1,4 +1,5 @@ using Kijk.Api.Domain.Entities; + using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Kijk.Api.Persistence.Configs; @@ -19,4 +20,4 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(x => x.RecurringTransactionId) .OnDelete(DeleteBehavior.Restrict); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/TransactionConfig.cs b/apps/api/src/Api/Persistence/Configs/TransactionConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/TransactionConfig.cs rename to apps/api/src/Api/Persistence/Configs/TransactionConfig.cs index 936d0a2..c9422a5 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/TransactionConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/TransactionConfig.cs @@ -1,4 +1,5 @@ using Kijk.Api.Domain.Entities; + using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Kijk.Api.Persistence.Configs; @@ -20,4 +21,4 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(x => x.CategoryId) .OnDelete(DeleteBehavior.Restrict); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/UserConfig.cs b/apps/api/src/Api/Persistence/Configs/UserConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/UserConfig.cs rename to apps/api/src/Api/Persistence/Configs/UserConfig.cs index 2e681ee..3ac4ecb 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/UserConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/UserConfig.cs @@ -32,4 +32,4 @@ public void Configure(EntityTypeBuilder builder) builder.HasMany(x => x.Categories) .WithMany(x => x.Users); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Configs/UserHouseholdConfig.cs b/apps/api/src/Api/Persistence/Configs/UserHouseholdConfig.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Configs/UserHouseholdConfig.cs rename to apps/api/src/Api/Persistence/Configs/UserHouseholdConfig.cs index f6e6e8d..a6ea5dc 100644 --- a/apps/api/Kijk.Api/Persistence/Configs/UserHouseholdConfig.cs +++ b/apps/api/src/Api/Persistence/Configs/UserHouseholdConfig.cs @@ -1,4 +1,5 @@ using Kijk.Api.Domain.Entities; + using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Kijk.Api.Persistence.Configs; @@ -16,4 +17,4 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.Cascade); } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Interceptors/AuditableEntityInterceptor.cs b/apps/api/src/Api/Persistence/Interceptors/AuditableEntityInterceptor.cs similarity index 96% rename from apps/api/Kijk.Api/Persistence/Interceptors/AuditableEntityInterceptor.cs rename to apps/api/src/Api/Persistence/Interceptors/AuditableEntityInterceptor.cs index 3503b93..6ebe313 100644 --- a/apps/api/Kijk.Api/Persistence/Interceptors/AuditableEntityInterceptor.cs +++ b/apps/api/src/Api/Persistence/Interceptors/AuditableEntityInterceptor.cs @@ -7,7 +7,6 @@ namespace Kijk.Api.Persistence.Interceptors; public class AuditableEntityInterceptor : SaveChangesInterceptor { - public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) { UpdateEntities(eventData.Context); @@ -27,7 +26,10 @@ public override ValueTask> SavingChangesAsync( private static void UpdateEntities(DbContext? context) { - if (context == null) return; + if (context == null) + { + return; + } var changedEntries = context.ChangeTracker.Entries(); @@ -54,4 +56,4 @@ public static bool HasChangedOwnedEntities(this EntityEntry entry) => r.TargetEntry != null && r.TargetEntry.Metadata.IsOwned() && r.TargetEntry.State is EntityState.Added or EntityState.Modified); -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Persistence/Migrations/20240310184603_NewInitial.Designer.cs b/apps/api/src/Api/Persistence/Migrations/20240310184603_NewInitial.Designer.cs similarity index 91% rename from apps/api/Kijk.Api/Persistence/Migrations/20240310184603_NewInitial.Designer.cs rename to apps/api/src/Api/Persistence/Migrations/20240310184603_NewInitial.Designer.cs index 8a98d8e..4801787 100644 --- a/apps/api/Kijk.Api/Persistence/Migrations/20240310184603_NewInitial.Designer.cs +++ b/apps/api/src/Api/Persistence/Migrations/20240310184603_NewInitial.Designer.cs @@ -44,7 +44,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("category_user", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -103,7 +103,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("accounts", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Budget", b => + modelBuilder.Entity("Api.Domain.Entities.Budget", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -181,7 +181,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("budgets", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Category", b => + modelBuilder.Entity("Api.Domain.Entities.Category", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -225,7 +225,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("categories", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumption", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumption", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -280,7 +280,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("energy_consumptions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumptionLimit", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumptionLimit", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -354,7 +354,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("energy_consumption_limits", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Household", b => + modelBuilder.Entity("Api.Domain.Entities.Household", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -397,7 +397,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("households", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -483,7 +483,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("recurring_transactions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Transaction", b => + modelBuilder.Entity("Api.Domain.Entities.Transaction", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -559,7 +559,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("transactions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.User", b => + modelBuilder.Entity("Api.Domain.Entities.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -615,7 +615,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("users", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.UserHousehold", b => + modelBuilder.Entity("Api.Domain.Entities.UserHousehold", b => { b.Property("UserId") .HasColumnType("uuid") @@ -664,14 +664,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("CategoryUser", b => { - b.HasOne("Kijk.Api.Domain.Entities.Category", null) + b.HasOne("Api.Domain.Entities.Category", null) .WithMany() .HasForeignKey("CategoriesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_category_user_categories_categories_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany() .HasForeignKey("UsersId") .OnDelete(DeleteBehavior.Cascade) @@ -679,9 +679,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasConstraintName("fk_category_user_users_users_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany("Accounts") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -689,16 +689,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasConstraintName("fk_accounts_users_user_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Budget", b => + modelBuilder.Entity("Api.Domain.Entities.Budget", b => { - b.HasOne("Kijk.Api.Domain.Entities.Category", "Category") + b.HasOne("Api.Domain.Entities.Category", "Category") .WithMany() .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_budgets_categories_category_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany("Budgets") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -708,9 +708,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumption", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumption", b => { - b.HasOne("Kijk.Api.Domain.Entities.Household", null) + b.HasOne("Api.Domain.Entities.Household", null) .WithMany("EnergyConsumptions") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) @@ -718,16 +718,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasConstraintName("fk_energy_consumptions_households_household_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumptionLimit", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumptionLimit", b => { - b.HasOne("Kijk.Api.Domain.Entities.User", "CreatedBy") + b.HasOne("Api.Domain.Entities.User", "CreatedBy") .WithMany() .HasForeignKey("CreatedById") .OnDelete(DeleteBehavior.Restrict) .IsRequired() .HasConstraintName("fk_energy_consumption_limits_users_created_by_id"); - b.HasOne("Kijk.Api.Domain.Entities.Household", null) + b.HasOne("Api.Domain.Entities.Household", null) .WithMany("EnergyConsumptionLimits") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) @@ -737,16 +737,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("CreatedBy"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { - b.HasOne("Kijk.Api.Domain.Entities.Account", "Account") + b.HasOne("Api.Domain.Entities.Account", "Account") .WithMany("RecurringTransactions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_recurring_transactions_accounts_account_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", "User") + b.HasOne("Api.Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -758,25 +758,25 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Transaction", b => + modelBuilder.Entity("Api.Domain.Entities.Transaction", b => { - b.HasOne("Kijk.Api.Domain.Entities.Account", "Account") + b.HasOne("Api.Domain.Entities.Account", "Account") .WithMany("Transactions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_transactions_accounts_account_id"); - b.HasOne("Kijk.Api.Domain.Entities.Category", "Category") + b.HasOne("Api.Domain.Entities.Category", "Category") .WithMany("Transactions") .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Restrict) .IsRequired() .HasConstraintName("fk_transactions_categories_category_id"); - b.HasOne("Kijk.Api.Domain.Entities.RecurringTransactions", null) + b.HasOne("Api.Domain.Entities.RecurringTransactions", null) .WithOne("LastTransaction") - .HasForeignKey("Kijk.Api.Domain.Entities.Transaction", "RecurringTransactionId") + .HasForeignKey("Api.Domain.Entities.Transaction", "RecurringTransactionId") .OnDelete(DeleteBehavior.Restrict) .HasConstraintName("fk_transactions_recurring_transactions_recurring_transaction_id"); @@ -785,16 +785,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.UserHousehold", b => + modelBuilder.Entity("Api.Domain.Entities.UserHousehold", b => { - b.HasOne("Kijk.Api.Domain.Entities.Household", "Household") + b.HasOne("Api.Domain.Entities.Household", "Household") .WithMany("UserHouseholds") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_user_households_households_household_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", "User") + b.HasOne("Api.Domain.Entities.User", "User") .WithMany("UserHouseholds") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -806,19 +806,19 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { b.Navigation("RecurringTransactions"); b.Navigation("Transactions"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Category", b => + modelBuilder.Entity("Api.Domain.Entities.Category", b => { b.Navigation("Transactions"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Household", b => + modelBuilder.Entity("Api.Domain.Entities.Household", b => { b.Navigation("EnergyConsumptionLimits"); @@ -827,12 +827,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("UserHouseholds"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { b.Navigation("LastTransaction"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.User", b => + modelBuilder.Entity("Api.Domain.Entities.User", b => { b.Navigation("Accounts"); diff --git a/apps/api/Kijk.Api/Persistence/Migrations/20240310184603_NewInitial.cs b/apps/api/src/Api/Persistence/Migrations/20240310184603_NewInitial.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/Migrations/20240310184603_NewInitial.cs rename to apps/api/src/Api/Persistence/Migrations/20240310184603_NewInitial.cs index 6354670..c81fe3e 100644 --- a/apps/api/Kijk.Api/Persistence/Migrations/20240310184603_NewInitial.cs +++ b/apps/api/src/Api/Persistence/Migrations/20240310184603_NewInitial.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/api/Kijk.Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.Designer.cs b/apps/api/src/Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.Designer.cs similarity index 91% rename from apps/api/Kijk.Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.Designer.cs rename to apps/api/src/Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.Designer.cs index 89984c5..b2c68e9 100644 --- a/apps/api/Kijk.Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.Designer.cs +++ b/apps/api/src/Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.Designer.cs @@ -44,7 +44,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("category_user", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -103,7 +103,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("accounts", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Budget", b => + modelBuilder.Entity("Api.Domain.Entities.Budget", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -181,7 +181,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("budgets", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Category", b => + modelBuilder.Entity("Api.Domain.Entities.Category", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -229,7 +229,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("categories", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumption", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumption", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -284,7 +284,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("energy_consumptions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumptionLimit", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumptionLimit", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -358,7 +358,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("energy_consumption_limits", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Household", b => + modelBuilder.Entity("Api.Domain.Entities.Household", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -401,7 +401,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("households", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -487,7 +487,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("recurring_transactions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Transaction", b => + modelBuilder.Entity("Api.Domain.Entities.Transaction", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -563,7 +563,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("transactions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.User", b => + modelBuilder.Entity("Api.Domain.Entities.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -619,7 +619,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("users", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.UserHousehold", b => + modelBuilder.Entity("Api.Domain.Entities.UserHousehold", b => { b.Property("UserId") .HasColumnType("uuid") @@ -668,14 +668,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("CategoryUser", b => { - b.HasOne("Kijk.Api.Domain.Entities.Category", null) + b.HasOne("Api.Domain.Entities.Category", null) .WithMany() .HasForeignKey("CategoriesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_category_user_categories_categories_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany() .HasForeignKey("UsersId") .OnDelete(DeleteBehavior.Cascade) @@ -683,9 +683,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasConstraintName("fk_category_user_users_users_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany("Accounts") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -693,16 +693,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasConstraintName("fk_accounts_users_user_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Budget", b => + modelBuilder.Entity("Api.Domain.Entities.Budget", b => { - b.HasOne("Kijk.Api.Domain.Entities.Category", "Category") + b.HasOne("Api.Domain.Entities.Category", "Category") .WithMany() .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_budgets_categories_category_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany("Budgets") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -712,9 +712,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumption", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumption", b => { - b.HasOne("Kijk.Api.Domain.Entities.Household", null) + b.HasOne("Api.Domain.Entities.Household", null) .WithMany("EnergyConsumptions") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) @@ -722,16 +722,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasConstraintName("fk_energy_consumptions_households_household_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumptionLimit", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumptionLimit", b => { - b.HasOne("Kijk.Api.Domain.Entities.User", "CreatedBy") + b.HasOne("Api.Domain.Entities.User", "CreatedBy") .WithMany() .HasForeignKey("CreatedById") .OnDelete(DeleteBehavior.Restrict) .IsRequired() .HasConstraintName("fk_energy_consumption_limits_users_created_by_id"); - b.HasOne("Kijk.Api.Domain.Entities.Household", null) + b.HasOne("Api.Domain.Entities.Household", null) .WithMany("EnergyConsumptionLimits") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) @@ -741,16 +741,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("CreatedBy"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { - b.HasOne("Kijk.Api.Domain.Entities.Account", "Account") + b.HasOne("Api.Domain.Entities.Account", "Account") .WithMany("RecurringTransactions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_recurring_transactions_accounts_account_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", "User") + b.HasOne("Api.Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -762,25 +762,25 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Transaction", b => + modelBuilder.Entity("Api.Domain.Entities.Transaction", b => { - b.HasOne("Kijk.Api.Domain.Entities.Account", "Account") + b.HasOne("Api.Domain.Entities.Account", "Account") .WithMany("Transactions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_transactions_accounts_account_id"); - b.HasOne("Kijk.Api.Domain.Entities.Category", "Category") + b.HasOne("Api.Domain.Entities.Category", "Category") .WithMany("Transactions") .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Restrict) .IsRequired() .HasConstraintName("fk_transactions_categories_category_id"); - b.HasOne("Kijk.Api.Domain.Entities.RecurringTransactions", null) + b.HasOne("Api.Domain.Entities.RecurringTransactions", null) .WithOne("LastTransaction") - .HasForeignKey("Kijk.Api.Domain.Entities.Transaction", "RecurringTransactionId") + .HasForeignKey("Api.Domain.Entities.Transaction", "RecurringTransactionId") .OnDelete(DeleteBehavior.Restrict) .HasConstraintName("fk_transactions_recurring_transactions_recurring_transaction_id"); @@ -789,16 +789,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.UserHousehold", b => + modelBuilder.Entity("Api.Domain.Entities.UserHousehold", b => { - b.HasOne("Kijk.Api.Domain.Entities.Household", "Household") + b.HasOne("Api.Domain.Entities.Household", "Household") .WithMany("UserHouseholds") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_user_households_households_household_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", "User") + b.HasOne("Api.Domain.Entities.User", "User") .WithMany("UserHouseholds") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -810,19 +810,19 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { b.Navigation("RecurringTransactions"); b.Navigation("Transactions"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Category", b => + modelBuilder.Entity("Api.Domain.Entities.Category", b => { b.Navigation("Transactions"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Household", b => + modelBuilder.Entity("Api.Domain.Entities.Household", b => { b.Navigation("EnergyConsumptionLimits"); @@ -831,12 +831,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("UserHouseholds"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { b.Navigation("LastTransaction"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.User", b => + modelBuilder.Entity("Api.Domain.Entities.User", b => { b.Navigation("Accounts"); diff --git a/apps/api/Kijk.Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.cs b/apps/api/src/Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.cs similarity index 82% rename from apps/api/Kijk.Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.cs rename to apps/api/src/Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.cs index 4420620..224104a 100644 --- a/apps/api/Kijk.Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.cs +++ b/apps/api/src/Api/Persistence/Migrations/20240414151635_AddCategoryCreatorType.cs @@ -8,22 +8,16 @@ namespace Kijk.Api.Persistence.Migrations public partial class AddCategoryCreatorType : Migration { /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( + protected override void Up(MigrationBuilder migrationBuilder) => migrationBuilder.AddColumn( name: "creator_type", table: "categories", type: "integer", nullable: false, defaultValue: 0); - } /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( + protected override void Down(MigrationBuilder migrationBuilder) => migrationBuilder.DropColumn( name: "creator_type", table: "categories"); - } } } diff --git a/apps/api/Kijk.Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.Designer.cs b/apps/api/src/Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.Designer.cs similarity index 91% rename from apps/api/Kijk.Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.Designer.cs rename to apps/api/src/Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.Designer.cs index b9dd73a..d4a76fd 100644 --- a/apps/api/Kijk.Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.Designer.cs +++ b/apps/api/src/Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.Designer.cs @@ -44,7 +44,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("category_user", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -103,7 +103,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("accounts", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Budget", b => + modelBuilder.Entity("Api.Domain.Entities.Budget", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -181,7 +181,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("budgets", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Category", b => + modelBuilder.Entity("Api.Domain.Entities.Category", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -230,7 +230,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("categories", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumption", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumption", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -285,7 +285,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("energy_consumptions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumptionLimit", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumptionLimit", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -359,7 +359,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("energy_consumption_limits", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Household", b => + modelBuilder.Entity("Api.Domain.Entities.Household", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -402,7 +402,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("households", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -488,7 +488,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("recurring_transactions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Transaction", b => + modelBuilder.Entity("Api.Domain.Entities.Transaction", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -564,7 +564,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("transactions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.User", b => + modelBuilder.Entity("Api.Domain.Entities.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -620,7 +620,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("users", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.UserHousehold", b => + modelBuilder.Entity("Api.Domain.Entities.UserHousehold", b => { b.Property("UserId") .HasColumnType("uuid") @@ -669,14 +669,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("CategoryUser", b => { - b.HasOne("Kijk.Api.Domain.Entities.Category", null) + b.HasOne("Api.Domain.Entities.Category", null) .WithMany() .HasForeignKey("CategoriesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_category_user_categories_categories_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany() .HasForeignKey("UsersId") .OnDelete(DeleteBehavior.Cascade) @@ -684,9 +684,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasConstraintName("fk_category_user_users_users_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany("Accounts") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -694,16 +694,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasConstraintName("fk_accounts_users_user_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Budget", b => + modelBuilder.Entity("Api.Domain.Entities.Budget", b => { - b.HasOne("Kijk.Api.Domain.Entities.Category", "Category") + b.HasOne("Api.Domain.Entities.Category", "Category") .WithMany() .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_budgets_categories_category_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany("Budgets") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -713,9 +713,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumption", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumption", b => { - b.HasOne("Kijk.Api.Domain.Entities.Household", null) + b.HasOne("Api.Domain.Entities.Household", null) .WithMany("EnergyConsumptions") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) @@ -723,16 +723,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasConstraintName("fk_energy_consumptions_households_household_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumptionLimit", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumptionLimit", b => { - b.HasOne("Kijk.Api.Domain.Entities.User", "CreatedBy") + b.HasOne("Api.Domain.Entities.User", "CreatedBy") .WithMany() .HasForeignKey("CreatedById") .OnDelete(DeleteBehavior.Restrict) .IsRequired() .HasConstraintName("fk_energy_consumption_limits_users_created_by_id"); - b.HasOne("Kijk.Api.Domain.Entities.Household", null) + b.HasOne("Api.Domain.Entities.Household", null) .WithMany("EnergyConsumptionLimits") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) @@ -742,16 +742,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("CreatedBy"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { - b.HasOne("Kijk.Api.Domain.Entities.Account", "Account") + b.HasOne("Api.Domain.Entities.Account", "Account") .WithMany("RecurringTransactions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_recurring_transactions_accounts_account_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", "User") + b.HasOne("Api.Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -763,25 +763,25 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Transaction", b => + modelBuilder.Entity("Api.Domain.Entities.Transaction", b => { - b.HasOne("Kijk.Api.Domain.Entities.Account", "Account") + b.HasOne("Api.Domain.Entities.Account", "Account") .WithMany("Transactions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_transactions_accounts_account_id"); - b.HasOne("Kijk.Api.Domain.Entities.Category", "Category") + b.HasOne("Api.Domain.Entities.Category", "Category") .WithMany("Transactions") .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Restrict) .IsRequired() .HasConstraintName("fk_transactions_categories_category_id"); - b.HasOne("Kijk.Api.Domain.Entities.RecurringTransactions", null) + b.HasOne("Api.Domain.Entities.RecurringTransactions", null) .WithOne("LastTransaction") - .HasForeignKey("Kijk.Api.Domain.Entities.Transaction", "RecurringTransactionId") + .HasForeignKey("Api.Domain.Entities.Transaction", "RecurringTransactionId") .OnDelete(DeleteBehavior.Restrict) .HasConstraintName("fk_transactions_recurring_transactions_recurring_transaction_id"); @@ -790,16 +790,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.UserHousehold", b => + modelBuilder.Entity("Api.Domain.Entities.UserHousehold", b => { - b.HasOne("Kijk.Api.Domain.Entities.Household", "Household") + b.HasOne("Api.Domain.Entities.Household", "Household") .WithMany("UserHouseholds") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_user_households_households_household_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", "User") + b.HasOne("Api.Domain.Entities.User", "User") .WithMany("UserHouseholds") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -811,19 +811,19 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { b.Navigation("RecurringTransactions"); b.Navigation("Transactions"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Category", b => + modelBuilder.Entity("Api.Domain.Entities.Category", b => { b.Navigation("Transactions"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Household", b => + modelBuilder.Entity("Api.Domain.Entities.Household", b => { b.Navigation("EnergyConsumptionLimits"); @@ -832,12 +832,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("UserHouseholds"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { b.Navigation("LastTransaction"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.User", b => + modelBuilder.Entity("Api.Domain.Entities.User", b => { b.Navigation("Accounts"); diff --git a/apps/api/Kijk.Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.cs b/apps/api/src/Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.cs similarity index 84% rename from apps/api/Kijk.Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.cs rename to apps/api/src/Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.cs index 97c59e6..907f0de 100644 --- a/apps/api/Kijk.Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.cs +++ b/apps/api/src/Api/Persistence/Migrations/20240414151932_AddCorrectTypeConversion.cs @@ -8,27 +8,21 @@ namespace Kijk.Api.Persistence.Migrations public partial class AddCorrectTypeConversion : Migration { /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( + protected override void Up(MigrationBuilder migrationBuilder) => migrationBuilder.AlterColumn( name: "creator_type", table: "categories", type: "text", nullable: false, oldClrType: typeof(int), oldType: "integer"); - } /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( + protected override void Down(MigrationBuilder migrationBuilder) => migrationBuilder.AlterColumn( name: "creator_type", table: "categories", type: "integer", nullable: false, oldClrType: typeof(string), oldType: "text"); - } } } diff --git a/apps/api/Kijk.Api/Persistence/Migrations/AppDbContextModelSnapshot.cs b/apps/api/src/Api/Persistence/Migrations/AppDbContextModelSnapshot.cs similarity index 91% rename from apps/api/Kijk.Api/Persistence/Migrations/AppDbContextModelSnapshot.cs rename to apps/api/src/Api/Persistence/Migrations/AppDbContextModelSnapshot.cs index 09e5c74..11cdd56 100644 --- a/apps/api/Kijk.Api/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/apps/api/src/Api/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -41,7 +41,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("category_user", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -100,7 +100,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("accounts", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Budget", b => + modelBuilder.Entity("Api.Domain.Entities.Budget", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -178,7 +178,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("budgets", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Category", b => + modelBuilder.Entity("Api.Domain.Entities.Category", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -227,7 +227,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("categories", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumption", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumption", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -282,7 +282,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("energy_consumptions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumptionLimit", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumptionLimit", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -356,7 +356,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("energy_consumption_limits", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Household", b => + modelBuilder.Entity("Api.Domain.Entities.Household", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -399,7 +399,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("households", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -485,7 +485,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("recurring_transactions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Transaction", b => + modelBuilder.Entity("Api.Domain.Entities.Transaction", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -561,7 +561,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("transactions", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.User", b => + modelBuilder.Entity("Api.Domain.Entities.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -617,7 +617,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("users", (string)null); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.UserHousehold", b => + modelBuilder.Entity("Api.Domain.Entities.UserHousehold", b => { b.Property("UserId") .HasColumnType("uuid") @@ -666,14 +666,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("CategoryUser", b => { - b.HasOne("Kijk.Api.Domain.Entities.Category", null) + b.HasOne("Api.Domain.Entities.Category", null) .WithMany() .HasForeignKey("CategoriesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_category_user_categories_categories_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany() .HasForeignKey("UsersId") .OnDelete(DeleteBehavior.Cascade) @@ -681,9 +681,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasConstraintName("fk_category_user_users_users_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany("Accounts") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -691,16 +691,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasConstraintName("fk_accounts_users_user_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Budget", b => + modelBuilder.Entity("Api.Domain.Entities.Budget", b => { - b.HasOne("Kijk.Api.Domain.Entities.Category", "Category") + b.HasOne("Api.Domain.Entities.Category", "Category") .WithMany() .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_budgets_categories_category_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", null) + b.HasOne("Api.Domain.Entities.User", null) .WithMany("Budgets") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -710,9 +710,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumption", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumption", b => { - b.HasOne("Kijk.Api.Domain.Entities.Household", null) + b.HasOne("Api.Domain.Entities.Household", null) .WithMany("EnergyConsumptions") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) @@ -720,16 +720,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasConstraintName("fk_energy_consumptions_households_household_id"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.EnergyConsumptionLimit", b => + modelBuilder.Entity("Api.Domain.Entities.EnergyConsumptionLimit", b => { - b.HasOne("Kijk.Api.Domain.Entities.User", "CreatedBy") + b.HasOne("Api.Domain.Entities.User", "CreatedBy") .WithMany() .HasForeignKey("CreatedById") .OnDelete(DeleteBehavior.Restrict) .IsRequired() .HasConstraintName("fk_energy_consumption_limits_users_created_by_id"); - b.HasOne("Kijk.Api.Domain.Entities.Household", null) + b.HasOne("Api.Domain.Entities.Household", null) .WithMany("EnergyConsumptionLimits") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) @@ -739,16 +739,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("CreatedBy"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { - b.HasOne("Kijk.Api.Domain.Entities.Account", "Account") + b.HasOne("Api.Domain.Entities.Account", "Account") .WithMany("RecurringTransactions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_recurring_transactions_accounts_account_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", "User") + b.HasOne("Api.Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -760,25 +760,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Transaction", b => + modelBuilder.Entity("Api.Domain.Entities.Transaction", b => { - b.HasOne("Kijk.Api.Domain.Entities.Account", "Account") + b.HasOne("Api.Domain.Entities.Account", "Account") .WithMany("Transactions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_transactions_accounts_account_id"); - b.HasOne("Kijk.Api.Domain.Entities.Category", "Category") + b.HasOne("Api.Domain.Entities.Category", "Category") .WithMany("Transactions") .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Restrict) .IsRequired() .HasConstraintName("fk_transactions_categories_category_id"); - b.HasOne("Kijk.Api.Domain.Entities.RecurringTransactions", null) + b.HasOne("Api.Domain.Entities.RecurringTransactions", null) .WithOne("LastTransaction") - .HasForeignKey("Kijk.Api.Domain.Entities.Transaction", "RecurringTransactionId") + .HasForeignKey("Api.Domain.Entities.Transaction", "RecurringTransactionId") .OnDelete(DeleteBehavior.Restrict) .HasConstraintName("fk_transactions_recurring_transactions_recurring_transaction_id"); @@ -787,16 +787,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.UserHousehold", b => + modelBuilder.Entity("Api.Domain.Entities.UserHousehold", b => { - b.HasOne("Kijk.Api.Domain.Entities.Household", "Household") + b.HasOne("Api.Domain.Entities.Household", "Household") .WithMany("UserHouseholds") .HasForeignKey("HouseholdId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_user_households_households_household_id"); - b.HasOne("Kijk.Api.Domain.Entities.User", "User") + b.HasOne("Api.Domain.Entities.User", "User") .WithMany("UserHouseholds") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -808,19 +808,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Account", b => + modelBuilder.Entity("Api.Domain.Entities.Account", b => { b.Navigation("RecurringTransactions"); b.Navigation("Transactions"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Category", b => + modelBuilder.Entity("Api.Domain.Entities.Category", b => { b.Navigation("Transactions"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.Household", b => + modelBuilder.Entity("Api.Domain.Entities.Household", b => { b.Navigation("EnergyConsumptionLimits"); @@ -829,12 +829,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("UserHouseholds"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.RecurringTransactions", b => + modelBuilder.Entity("Api.Domain.Entities.RecurringTransactions", b => { b.Navigation("LastTransaction"); }); - modelBuilder.Entity("Kijk.Api.Domain.Entities.User", b => + modelBuilder.Entity("Api.Domain.Entities.User", b => { b.Navigation("Accounts"); diff --git a/apps/api/Kijk.Api/Persistence/UtcDateTimeConverter.cs b/apps/api/src/Api/Persistence/UtcDateTimeConverter.cs similarity index 99% rename from apps/api/Kijk.Api/Persistence/UtcDateTimeConverter.cs rename to apps/api/src/Api/Persistence/UtcDateTimeConverter.cs index be88788..98c2180 100644 --- a/apps/api/Kijk.Api/Persistence/UtcDateTimeConverter.cs +++ b/apps/api/src/Api/Persistence/UtcDateTimeConverter.cs @@ -11,4 +11,4 @@ public UtcDateTimeConverter() : base(v => DateTime.SpecifyKind(v, DateTimeKind.Utc), v => v) { } -} +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Program.cs b/apps/api/src/Api/Program.cs similarity index 59% rename from apps/api/Kijk.Api/Program.cs rename to apps/api/src/Api/Program.cs index 52fcf52..dd280ae 100644 --- a/apps/api/Kijk.Api/Program.cs +++ b/apps/api/src/Api/Program.cs @@ -1,15 +1,13 @@ -using Kijk.Api.Common; -using Kijk.Api.Common.Extensions; +using Kijk.Api.Common.Extensions; using Kijk.Api.Common.Middleware; using Kijk.Api.Common.Models; using Kijk.Api.Common.Utils; Log.Logger = LoggerUtils.CreateRootLogger(); +Log.Information("Application is starting ..."); try { - Log.Information("Application is starting ..."); - // ##### Add services to the container. ##### var builder = WebApplication.CreateBuilder(args); @@ -17,10 +15,9 @@ builder.AddLogging(); builder.Services - .AddDatabase() + .AddDatabase(builder.Configuration) .AddAppSettings(builder.Configuration) .AddAuth(builder.Configuration) - .AddExceptionHandler() .AddScoped() .AddHttpClient() .AddOpenApi(builder.Configuration) @@ -36,56 +33,52 @@ // ##### Configure the HTTP request pipeline. ##### var app = builder.Build(); - app.UseExceptionHandler(_ => { }) - .UseAuthExceptionHandler(); - + app.MapOpenApi("api/{documentName}.json"); if (app.Environment.IsDevelopment()) { - app.ApplyMigrations(); - app.UseDeveloperExceptionPage(); + app.Map("/", () => Results.Redirect("api/swagger")); } - if (app.Environment.IsProduction()) - { - app.UseHsts(); - } + app.UseResponseCompression() + .UseSecurityHeaders() + .UseWhen(_ => app.Environment.IsDevelopment(), appBuilder => appBuilder.UseDeveloperExceptionPage()) + .UseWhen(_ => !app.Environment.IsDevelopment(), appBuilder => appBuilder.UseHsts()); app.ApplyInitialData(); - app.UseHttpsRedirection(); - app.UseSecurityHeaders() - .UseCustomOpenApi(); - - app.UseMiddleware() - .UseSerilogRequestLogging() - .UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All }); - - // Authentication, Authorization and Cors - app.UseCors(AppConstants.Policies.Cors) + app.UseGlobalExceptionHandler() + .UseAuthExceptionHandler() + .UseOpenApi() + .UseMiddleware() + .UseRequestLogging() + .UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All }) + .UseCors(AppConstants.Policies.Cors) + .UseHttpsRedirection() .UseAuthentication() .UseAuthorization(); - app.UseResponseCompression(); - // Needs to ba after Auth so we have user data + app.UseMiddleware(); app.UseRateLimiter(); - app.MapApiEndpoints() .MapHealthCheck(); - app.Run(); + await app.RunAsync(); + return 0; } catch (HostAbortedException) { //https://github.com/dotnet/efcore/issues/29809 Log.Information("Application terminated expectedly by e.g. 'dotnet ef'"); + return 1; } catch (Exception ex) { - Log.Fatal(ex, "Application terminated unexpectedly"); + Log.Fatal(ex, "Unhandled exception"); + return 1; } finally { Log.Information("Application is shutting down ..."); - Log.CloseAndFlush(); -} + await Log.CloseAndFlushAsync(); +} \ No newline at end of file diff --git a/apps/api/Kijk.Api/Properties/launchSettings.json b/apps/api/src/Api/Properties/launchSettings.json similarity index 100% rename from apps/api/Kijk.Api/Properties/launchSettings.json rename to apps/api/src/Api/Properties/launchSettings.json diff --git a/apps/api/Kijk.Api/appsettings.Development.json b/apps/api/src/Api/appsettings.Development.json similarity index 82% rename from apps/api/Kijk.Api/appsettings.Development.json rename to apps/api/src/Api/appsettings.Development.json index 7bf5880..897b83a 100644 --- a/apps/api/Kijk.Api/appsettings.Development.json +++ b/apps/api/src/Api/appsettings.Development.json @@ -11,6 +11,8 @@ "Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore.Hosting.Diagnostics": "Warning", "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning", "Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter ": "Warning" } } diff --git a/apps/api/Kijk.Api/appsettings.Production.json b/apps/api/src/Api/appsettings.Production.json similarity index 100% rename from apps/api/Kijk.Api/appsettings.Production.json rename to apps/api/src/Api/appsettings.Production.json diff --git a/apps/api/Kijk.Api/appsettings.json b/apps/api/src/Api/appsettings.json similarity index 100% rename from apps/api/Kijk.Api/appsettings.json rename to apps/api/src/Api/appsettings.json diff --git a/apps/api/src/Api/kijk_openapi.json b/apps/api/src/Api/kijk_openapi.json new file mode 100644 index 0000000..af98eda --- /dev/null +++ b/apps/api/src/Api/kijk_openapi.json @@ -0,0 +1,1171 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Kijk API", + "description": "Kijk API to manage your houses", + "version": "v1" + }, + "paths": { + "/api/transactions": { + "get": { + "tags": [ + "Transactions" + ], + "parameters": [ + { + "name": "year", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "month", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfTransactionDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + }, + "post": { + "tags": [ + "Transactions" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTransactionRequest" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTransactionDto" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/transactions/{id}": { + "get": { + "tags": [ + "Transactions" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTransactionDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + }, + "put": { + "tags": [ + "Transactions" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTransactionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTransactionDto" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + }, + "delete": { + "tags": [ + "Transactions" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfboolean" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/transactions/years": { + "get": { + "tags": [ + "Transactions" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfint" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/users/sign-in": { + "get": { + "tags": [ + "Users" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/users/me": { + "get": { + "tags": [ + "Users" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/users/welcome": { + "put": { + "tags": [ + "Users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WelcomeUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/users/update": { + "put": { + "tags": [ + "Users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/categories": { + "get": { + "tags": [ + "Categories" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfDictionaryOfCategoryTypeAndListOfCategoryDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + }, + "post": { + "tags": [ + "Categories" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCategoryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfCategoryDto" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/categories/{id}": { + "put": { + "tags": [ + "Categories" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCategoryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfCategoryDto" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + }, + "delete": { + "tags": [ + "Categories" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfboolean" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfAppError" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + } + }, + "components": { + "schemas": { + "ApiResponseOfboolean": { + "type": "object", + "properties": { + "data": { + "type": "boolean" + }, + "status": { + "$ref": "#/components/schemas/ResponseStatus" + }, + "message": { + "type": "string", + "nullable": true + } + } + }, + "ApiResponseOfCategoryDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CategoryDto" + }, + "status": { + "$ref": "#/components/schemas/ResponseStatus" + }, + "message": { + "type": "string", + "nullable": true + } + } + }, + "ApiResponseOfDictionaryOfCategoryTypeAndListOfCategoryDto": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CategoryDto2" + } + }, + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ResponseStatus" + }, + "message": { + "type": "string", + "nullable": true + } + } + }, + "ApiResponseOfListOfAppError": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AppError" + }, + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ResponseStatus" + }, + "message": { + "type": "string", + "nullable": true + } + } + }, + "ApiResponseOfListOfint": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ResponseStatus" + }, + "message": { + "type": "string", + "nullable": true + } + } + }, + "ApiResponseOfListOfTransactionDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionDto" + }, + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ResponseStatus" + }, + "message": { + "type": "string", + "nullable": true + } + } + }, + "ApiResponseOfTransactionDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TransactionDto2" + }, + "status": { + "$ref": "#/components/schemas/ResponseStatus" + }, + "message": { + "type": "string", + "nullable": true + } + } + }, + "AppError": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/ErrorType" + }, + "code": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + } + } + }, + "CategoryCreatorType": { + "enum": [ + "Default", + "User" + ] + }, + "CategoryDto": { + "required": [ + "id", + "name", + "color", + "type", + "creatorType" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/CategoryType" + }, + "creatorType": { + "$ref": "#/components/schemas/CategoryCreatorType" + } + }, + "nullable": true + }, + "CategoryDto2": { + "required": [ + "id", + "name", + "color", + "type", + "creatorType" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/CategoryType" + }, + "creatorType": { + "$ref": "#/components/schemas/CategoryCreatorType" + } + } + }, + "CategoryType": { + "enum": [ + "Expense", + "Income", + "Other" + ] + }, + "CreateCategoryRequest": { + "required": [ + "name", + "color", + "type" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "CreateTransactionRequest": { + "required": [ + "name", + "amount", + "type", + "executedAt", + "accountId", + "categoryId" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "amount": { + "type": "number", + "format": "double" + }, + "type": { + "$ref": "#/components/schemas/TransactionType" + }, + "executedAt": { + "type": "string", + "format": "date-time" + }, + "accountId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "categoryId": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + }, + "ErrorType": { + "enum": [ + "Basic", + "Validation", + "Conflict", + "NotFound", + "Authentication", + "Authorization", + "Unexpected" + ] + }, + "NullableOfTransactionType": { + "enum": [ + "Income", + "Expense", + "Transfer", + null + ], + "nullable": true + }, + "ResponseStatus": { + "enum": [ + "Success", + "Error" + ] + }, + "TransactionDto": { + "required": [ + "id", + "name", + "amount", + "type", + "executedAt", + "category" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "amount": { + "type": "number", + "format": "double" + }, + "type": { + "$ref": "#/components/schemas/TransactionType" + }, + "executedAt": { + "type": "string", + "format": "date-time" + }, + "category": { + "$ref": "#/components/schemas/CategoryDto" + } + } + }, + "TransactionDto2": { + "required": [ + "id", + "name", + "amount", + "type", + "executedAt", + "category" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "amount": { + "type": "number", + "format": "double" + }, + "type": { + "$ref": "#/components/schemas/TransactionType" + }, + "executedAt": { + "type": "string", + "format": "date-time" + }, + "category": { + "$ref": "#/components/schemas/CategoryDto" + } + }, + "nullable": true + }, + "TransactionType": { + "enum": [ + "Income", + "Expense", + "Transfer" + ] + }, + "UpdateCategoryRequest": { + "required": [ + "name", + "color", + "type" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "color": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string" + } + } + }, + "UpdateTransactionRequest": { + "required": [ + "name", + "amount", + "type", + "executedAt", + "categoryId" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "amount": { + "type": "number", + "format": "double", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/NullableOfTransactionType" + }, + "executedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "categoryId": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + }, + "UpdateUserRequest": { + "required": [ + "userName", + "useDefaultCategories" + ], + "type": "object", + "properties": { + "userName": { + "type": "string", + "nullable": true + }, + "useDefaultCategories": { + "type": "boolean", + "nullable": true + } + } + }, + "WelcomeUserRequest": { + "required": [ + "userName", + "useDefaultCategories" + ], + "type": "object", + "properties": { + "userName": { + "type": "string", + "nullable": true + }, + "useDefaultCategories": { + "type": "boolean", + "nullable": true + } + } + } + }, + "securitySchemes": { + "Bearer": { + "type": "http", + "description": "JWT Authorization header using the Bearer scheme.", + "scheme": "bearer", + "bearerFormat": "Json Web Token" + } + } + }, + "tags": [ + { + "name": "Transactions" + }, + { + "name": "Users" + }, + { + "name": "Categories" + } + ] +} \ No newline at end of file diff --git a/apps/client/components.json b/apps/client/components.json index 57039a2..68fec4c 100644 --- a/apps/client/components.json +++ b/apps/client/components.json @@ -4,13 +4,18 @@ "rsc": false, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "tailwind.config.ts", "css": "src/index.css", - "baseColor": "slate", - "cssVariables": true + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" }, "aliases": { - "components": "@/components", - "utils": "@/lib/utils" - } + "components": "@/shared/components", + "utils": "@/shared/lib/helpers", + "ui": "@/shared/components/ui", + "lib": "@/shared/lib", + "hooks": "@/shared/hooks" + }, + "iconLibrary": "lucide" } diff --git a/apps/client/eslint.config.js b/apps/client/eslint.config.js index 93e7772..4936bd3 100644 --- a/apps/client/eslint.config.js +++ b/apps/client/eslint.config.js @@ -1,6 +1,7 @@ // @ts-check import eslint from '@eslint/js'; import tanstackQueryPlugin from '@tanstack/eslint-plugin-query'; +import tanstackRouterRouter from '@tanstack/eslint-plugin-router' import prettierConfig from 'eslint-config-prettier'; import reactPlugin from 'eslint-plugin-react'; import reactHooksPlugin from 'eslint-plugin-react-hooks'; @@ -99,8 +100,10 @@ const typescriptConfig = { /** @type {import('typescript-eslint').ConfigWithExtends} */ const reactConfig = { name: 'react', + // @ts-ignore extends: [reactPlugin.configs.flat.recommended], plugins: { + // @ts-ignore 'react-hooks': reactHooksPlugin, 'react-refresh': reactRefreshPlugin, }, @@ -142,6 +145,7 @@ const unicornConfig = { 'unicorn/no-array-reduce': 'warn', 'unicorn/no-null': 'warn', 'unicorn/no-useless-undefined': 'warn', + 'unicorn/no-document-cookie': 'warn', 'unicorn/filename-case': [ 'error', { @@ -202,7 +206,7 @@ export default tseslint.config( prettierConfig, reactConfig, unicornConfig, - // @ts-ignore + ...tanstackRouterRouter.configs['flat/recommended'], ...tanstackQueryPlugin.configs['flat/recommended'], disableTypeChecked, ignoreFiles, diff --git a/apps/client/package.json b/apps/client/package.json index 37c5c5a..21de7fe 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -19,94 +19,101 @@ "clean:build": "rimraf ./dist" }, "dependencies": { - "@clerk/clerk-js": "^5.14.0", - "@clerk/clerk-react": "^5.4.0", - "@hookform/resolvers": "^3.9.0", - "@nivo/calendar": "^0.87.0", - "@nivo/core": "^0.87.0", - "@radix-ui/react-accordion": "^1.2.0", - "@radix-ui/react-alert-dialog": "^1.1.1", + "@clerk/clerk-js": "^5.35.0", + "@clerk/clerk-react": "^5.17.0", + "@hookform/resolvers": "^3.9.1", + "@nivo/calendar": "^0.88.0", + "@nivo/core": "^0.88.0", + "@radix-ui/react-accordion": "^1.2.1", + "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.1.0", - "@radix-ui/react-avatar": "^1.1.0", - "@radix-ui/react-checkbox": "^1.1.1", - "@radix-ui/react-collapsible": "^1.1.0", - "@radix-ui/react-context-menu": "^2.2.1", - "@radix-ui/react-dialog": "^1.1.1", - "@radix-ui/react-dropdown-menu": "^2.1.1", - "@radix-ui/react-hover-card": "^1.1.1", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.2", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-menubar": "^1.1.1", - "@radix-ui/react-navigation-menu": "^1.2.0", - "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-menubar": "^1.1.2", + "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-progress": "^1.1.0", - "@radix-ui/react-radio-group": "^1.2.0", - "@radix-ui/react-scroll-area": "^1.1.0", - "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-radio-group": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-switch": "^1.1.0", - "@radix-ui/react-tabs": "^1.1.0", - "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-toggle": "^1.1.0", - "@radix-ui/react-tooltip": "^1.1.2", - "@sentry/react": "^8.25.0", - "@sentry/vite-plugin": "^2.22.1", - "@tanstack/react-query": "5.51.23", - "@tanstack/react-router": "1.47.1", - "@tanstack/react-table": "^8.20.1", - "@tanstack/router-devtools": "1.47.1", - "axios": "^1.7.3", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "@sentry/react": "^8.40.0", + "@sentry/vite-plugin": "^2.22.6", + "@tanstack/react-query": "5.61.4", + "@tanstack/react-router": "1.82.12", + "@tanstack/react-table": "^8.20.5", + "@tanstack/router-devtools": "1.82.12", + "axios": "^1.7.8", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cmdk": "^1.0.0", - "date-fns": "^3.6.0", - "framer-motion": "^11.3.24", + "cmdk": "^1.0.4", + "date-fns": "^4.1.0", + "framer-motion": "^11.11.17", "immer": "^10.1.1", - "lucide-react": "^0.427.0", - "posthog-js": "^1.155.0", + "input-otp": "^1.4.1", + "lucide-react": "^0.461.0", + "next-themes": "^0.4.3", + "posthog-js": "^1.189.0", "react": "^18.3.1", - "react-day-picker": "^9.0.8", + "react-day-picker": "^9.4.0", "react-dom": "^18.3.1", - "react-error-boundary": "^4.0.13", - "react-hook-form": "^7.52.2", - "tailwind-merge": "^2.5.2", - "tailwind-variants": "^0.2.1", + "react-error-boundary": "^4.1.2", + "react-hook-form": "^7.53.2", + "react-resizable-panels": "^2.1.7", + "sonner": "^1.7.0", + "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.1", "zod": "^3.23.8", - "zustand": "^4.5.4" + "zustand": "^5.0.1" }, "devDependencies": { - "@eslint/compat": "^1.1.1", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.9.0", - "@ianvs/prettier-plugin-sort-imports": "^4.3.1", + "@eslint/compat": "^1.2.3", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.15.0", + "@ianvs/prettier-plugin-sort-imports": "^4.4.0", "@microsoft/eslint-formatter-sarif": "^3.1.0", - "@tanstack/eslint-plugin-query": "^5.51.15", - "@tanstack/react-query-devtools": "5.51.23", - "@tanstack/router-vite-plugin": "^1.47.0", - "@total-typescript/ts-reset": "^0.5.1", - "@types/node": "^22.2.0", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.19", - "eslint": "^9.9.0", + "@tanstack/eslint-plugin-query": "^5.61.4", + "@tanstack/eslint-plugin-router": "^1.82.12", + "@tanstack/react-query-devtools": "5.61.4", + "@tanstack/router-vite-plugin": "^1.82.10", + "@total-typescript/ts-reset": "^0.6.1", + "@types/node": "^22.10.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-react": "^7.35.0", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "eslint-plugin-unicorn": "^55.0.0", - "globals": "^15.9.0", - "knip": "^5.23.2", - "postcss": "^8.4.39", - "prettier": "^3.3.3", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "eslint-plugin-unicorn": "^56.0.1", + "globals": "^15.12.0", + "knip": "^5.38.0", + "postcss": "^8.4.49", + "prettier": "^3.4.1", "prettier-plugin-jsdoc": "^1.3.0", - "prettier-plugin-tailwindcss": "^0.6.6", - "tailwindcss": "^3.4.9", - "typescript": "^5.5.4", - "typescript-eslint": "^8.1.0", - "vite": "^5.4.0", - "vite-plugin-checker": "^0.7.0", - "vite-plugin-pwa": "^0.20.0" + "prettier-plugin-tailwindcss": "^0.6.9", + "tailwindcss": "^3.4.15", + "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0", + "vite": "^6.0.0", + "vite-plugin-checker": "^0.8.0", + "vite-plugin-pwa": "^0.21.0" } } diff --git a/apps/client/src/app/budget/budget-month-nav.tsx b/apps/client/src/app/budget/budget-month-nav.tsx index 17b6579..0311fd7 100644 --- a/apps/client/src/app/budget/budget-month-nav.tsx +++ b/apps/client/src/app/budget/budget-month-nav.tsx @@ -13,12 +13,16 @@ export function BudgetMonthNav({ className, ...props }: Props) { return ( ({ ...previous, month: item })} className={cn( buttonVariants({ variant: 'ghost' }), 'justify-start text-primary/65 data-[status=active]:bg-primary data-[status=active]:text-primary-foreground', )} + search={(previous) => ({ + ...previous, + month: item, + })} > {item} diff --git a/apps/client/src/app/budget/budget-year-switcher.tsx b/apps/client/src/app/budget/budget-year-switcher.tsx index dabab9b..5fc1098 100644 --- a/apps/client/src/app/budget/budget-year-switcher.tsx +++ b/apps/client/src/app/budget/budget-year-switcher.tsx @@ -176,9 +176,9 @@ function AddNewYearDialog({ onClose }: { onClose: () => void }) { }, [form]); return ( - + - Add new Year + Add new year Add a new year to manage.
diff --git a/apps/client/src/app/home/overview.tsx b/apps/client/src/app/home/overview.tsx index da92164..c4a1ad8 100644 --- a/apps/client/src/app/home/overview.tsx +++ b/apps/client/src/app/home/overview.tsx @@ -1,4 +1,4 @@ // TODO add other graph package export function Overview() { - return null; + return <>; } diff --git a/apps/client/src/app/home/team-switcher.tsx b/apps/client/src/app/home/team-switcher.tsx index 55b8b41..8e2c17b 100644 --- a/apps/client/src/app/home/team-switcher.tsx +++ b/apps/client/src/app/home/team-switcher.tsx @@ -134,7 +134,7 @@ export function TeamSwitcher({ className }: TeamSwitcherProps) { - + Create team Add a new team to manage products and customers. diff --git a/apps/client/src/app/root/app-footer.tsx b/apps/client/src/app/root/app-footer.tsx deleted file mode 100644 index ce04fb7..0000000 --- a/apps/client/src/app/root/app-footer.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { AppVersion } from '@/shared/components/app-version'; - -export function AppFooter() { - return ( -
- -
- ); -} diff --git a/apps/client/src/app/root/app-sidebar.tsx b/apps/client/src/app/root/app-sidebar.tsx index a664d5f..f4b012b 100644 --- a/apps/client/src/app/root/app-sidebar.tsx +++ b/apps/client/src/app/root/app-sidebar.tsx @@ -1,93 +1,52 @@ -import { PropsWithChildren } from 'react'; +import * as React from 'react'; import { Link } from '@tanstack/react-router'; -import { HomeIcon, SettingsIcon, WalletIcon, ZapIcon } from 'lucide-react'; -import { AppHelp } from '@/app/root/app-help'; import { CommandMenu } from '@/app/root/command-menu'; -import { UserNav } from '@/app/root/user-nav'; +import { mainNav } from '@/app/root/constants'; +import { NavMain } from '@/app/root/nav-main'; +import { NavSecondary } from '@/app/root/nav-secondary'; +import { NavUser } from '@/app/root/nav-user'; import { Icons } from '@/shared/components/icons'; -import { buttonVariants } from '@/shared/components/ui/button'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from '@/shared/components/ui/sidebar'; import { siteConfig } from '@/shared/lib/constants'; -import { cn } from '@/shared/lib/helpers'; -export const AppSidebar = () => { +export function AppSidebar({ ...props }: React.ComponentProps) { return ( - + + + + + + + +
{siteConfig.name}
+ +
+
+
+
+ + + + + + + + + + + + + + +
); -}; - -interface SidebarItemProps { - leftSlot?: React.ReactNode; - className?: string; - to: string; - exact?: boolean; } - -const SidebarItem = ({ - children, - leftSlot, - to, - exact = false, - className = '', -}: PropsWithChildren) => { - return ( - -
{leftSlot}
-
-
{children}
-
- - ); -}; diff --git a/apps/client/src/app/root/command-menu.tsx b/apps/client/src/app/root/command-menu.tsx index 163a3be..610d57e 100644 --- a/apps/client/src/app/root/command-menu.tsx +++ b/apps/client/src/app/root/command-menu.tsx @@ -1,10 +1,10 @@ import { Suspense, useCallback, useEffect, useState } from 'react'; import { useNavigate } from '@tanstack/react-router'; -import { File, Laptop, Moon, SunMedium } from 'lucide-react'; +import { HousePlug, Laptop, LayoutDashboard, Moon, SunMedium, WalletMinimal } from 'lucide-react'; import { TransactionCreateForm } from '@/app/budget/transaction-create-form'; import { CategoryCreateForm } from '@/app/settings/categories/categories-create-form'; -import { ThemeQuickCustomizer } from '@/shared/components/theme-quick-customizer'; +import { Icons } from '@/shared/components/icons'; import { Button } from '@/shared/components/ui/button'; import { CommandDialog, @@ -15,6 +15,7 @@ import { CommandList, CommandSeparator, } from '@/shared/components/ui/command'; +import { DialogDescription, DialogTitle } from '@/shared/components/ui/dialog'; import { Sheet, SheetContent, @@ -84,7 +85,7 @@ export function CommandMenu({ ...props }: Props) { )} onClick={handleOpen(true)} > - {props.isCollapsed ? null : Search...} + {props.isCollapsed ? undefined : Search...} - - + + Command Menu + + + Command menu + + + No results found. - navigate({ to: '/' }))}> - + Home - + navigate({ to: '/' }))}> + + Energy + @@ -120,7 +129,7 @@ export function CommandMenu({ ...props }: Props) { }), )} > - + Budget @@ -146,17 +155,19 @@ export function CommandMenu({ ...props }: Props) { - - {settingsNav.map((item) => ( - navigate({ to: '/settings/$section', params: { section: item.to } }))} - > - - {item.label} - - ))} + {settingsNav.map((item) => { + const Icon = Icons[item.icon || 'arrowRight']; + return ( + navigate({ to: '/settings/$section', params: { section: item.to } }))} + > + + {item.label} + + ); + })} @@ -187,9 +198,6 @@ export function CommandMenu({ ...props }: Props) { System - - - diff --git a/apps/client/src/app/root/constants.ts b/apps/client/src/app/root/constants.ts new file mode 100644 index 0000000..4f94e75 --- /dev/null +++ b/apps/client/src/app/root/constants.ts @@ -0,0 +1,24 @@ +import { HousePlug, LayoutDashboard, Settings2, WalletMinimal } from 'lucide-react'; + +export const mainNav = [ + { + title: 'Overview', + url: '/home', + icon: LayoutDashboard, + }, + { + title: 'Energy', + url: '/energy', + icon: HousePlug, + }, + { + title: 'Budget', + url: '/budget', + icon: WalletMinimal, + }, + { + title: 'Settings', + url: '/settings', + icon: Settings2, + }, +]; diff --git a/apps/client/src/app/root/helpers.ts b/apps/client/src/app/root/helpers.ts index 811e159..dcba240 100644 --- a/apps/client/src/app/root/helpers.ts +++ b/apps/client/src/app/root/helpers.ts @@ -8,7 +8,7 @@ const initialRegex = new RegExp(/(\p{L}{1})\p{L}+/gu); * @param stringValue The string value to get the initials from. * @returns Returns the initials. */ -export function getInitailChars(stringValue: Optional) { +export function getInitialChars(stringValue: Optional) { if (!stringValue) { return 'KJ'; } diff --git a/apps/client/src/app/root/nav-main.tsx b/apps/client/src/app/root/nav-main.tsx new file mode 100644 index 0000000..d127b08 --- /dev/null +++ b/apps/client/src/app/root/nav-main.tsx @@ -0,0 +1,34 @@ +import { Link } from '@tanstack/react-router'; + +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/shared/components/ui/sidebar'; +import type { LucideIcon } from 'lucide-react'; + +interface Props { + items: Array<{ + title: string; + url: string; + icon: LucideIcon; + isActive?: boolean; + items?: Array<{ + title: string; + url: string; + }>; + }>; +} + +export function NavMain({ items }: Props) { + return ( + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + ); +} diff --git a/apps/client/src/app/root/app-help.tsx b/apps/client/src/app/root/nav-secondary.tsx similarity index 54% rename from apps/client/src/app/root/app-help.tsx rename to apps/client/src/app/root/nav-secondary.tsx index 3d39f3c..d608fe7 100644 --- a/apps/client/src/app/root/app-help.tsx +++ b/apps/client/src/app/root/nav-secondary.tsx @@ -1,11 +1,10 @@ -import { PropsWithChildren, useCallback, useState } from 'react'; -import { ExternalLink, HelpCircle, LucideHeart } from 'lucide-react'; +import { useCallback, useState } from 'react'; +import { ExternalLink, LucideHeart, Send } from 'lucide-react'; import { FeedbackFormValues, FeedbackSchema } from '@/app/root/schemas'; -import { Button, buttonVariants } from '@/shared/components/ui/button'; +import { Button } from '@/shared/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/shared/components/ui/form/form'; import { useZodForm } from '@/shared/components/ui/form/use-zod-form'; -import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover'; import { Sheet, SheetContent, @@ -14,14 +13,22 @@ import { SheetTitle, SheetTrigger, } from '@/shared/components/ui/sheet'; +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from '@/shared/components/ui/sidebar'; import { Textarea } from '@/shared/components/ui/textarea'; import { env } from '@/shared/env'; import { toast } from '@/shared/hooks/use-toast'; import { AnalyticsService } from '@/shared/lib/analytics-tracking'; import { siteConfig } from '@/shared/lib/constants'; -import { cn } from '@/shared/lib/helpers'; -export function AppHelp() { +interface Props extends React.ComponentPropsWithoutRef {} + +export function NavSecondary({ ...props }: Props) { const [showFeedback, setShowFeedback] = useState(false); const handleCloseFeedback = useCallback(() => { @@ -33,33 +40,31 @@ export function AppHelp() { }, []); return ( - - - - - - -
- -
- Support -
-
- - - -
-
+ + + + + + + + + Support + + + + + + + + Feedback + + + + + -
-
+
+ ); } @@ -114,26 +119,3 @@ function FeedbackSheet({ onClose }: { onClose: () => void }) { ); } - -interface ExSidebarItemProps { - to: string; -} - -const ExSidebarItem = ({ children, to }: PropsWithChildren) => { - return ( - -
-
{children}
-
-
- ); -}; diff --git a/apps/client/src/app/root/nav-user.tsx b/apps/client/src/app/root/nav-user.tsx new file mode 100644 index 0000000..e72a1e0 --- /dev/null +++ b/apps/client/src/app/root/nav-user.tsx @@ -0,0 +1,95 @@ +import { useAuth, useUser } from '@clerk/clerk-react'; +import { useNavigate } from '@tanstack/react-router'; +import { Bell, ChevronsUpDown, LogOut, Sparkles } from 'lucide-react'; + +import { getInitialChars } from '@/app/root/helpers'; +import { Avatar, AvatarFallback, AvatarImage } from '@/shared/components/ui/avatar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/shared/components/ui/dropdown-menu'; +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/shared/components/ui/sidebar'; + +export function NavUser() { + const { signOut } = useAuth(); + const { user } = useUser(); + const email = user?.emailAddresses[0]?.emailAddress; + const userInitials = getInitialChars(email); + const navigate = useNavigate({ from: '/' }); + const { isMobile } = useSidebar(); + + const handleSignOut = (event: Event) => { + event.preventDefault(); + signOut() + .then(() => navigate({ to: '/auth', replace: true })) + .catch(console.warn); + }; + + return ( + + + + + + + + {userInitials} + +
+ {user?.firstName} + {email} +
+ +
+
+ + +
+ + + CN + +
+ {user?.firstName} + {email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Notifications + + + + + + Log out + +
+
+
+
+ ); +} diff --git a/apps/client/src/app/root/user-nav.tsx b/apps/client/src/app/root/user-nav.tsx deleted file mode 100644 index 388e101..0000000 --- a/apps/client/src/app/root/user-nav.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useAuth, useUser } from '@clerk/clerk-react'; -import { useNavigate } from '@tanstack/react-router'; -import { LogOutIcon } from 'lucide-react'; - -import { getInitailChars } from '@/app/root/helpers'; -import { Avatar, AvatarFallback, AvatarImage } from '@/shared/components/ui/avatar'; -import { Button } from '@/shared/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/shared/components/ui/dropdown-menu'; -import { useThemeStore } from '@/shared/stores/theme-store'; - -export function UserNav() { - const { signOut } = useAuth(); - const { user } = useUser(); - const email = user?.emailAddresses[0]?.emailAddress; - const userInitials = getInitailChars(email); - const { radius } = useThemeStore(); - const navigate = useNavigate({ from: '/' }); - - const handleSignOut = (event: Event) => { - event.preventDefault(); - signOut() - .then(() => navigate({ to: '/auth', replace: true })) - .catch(console.warn); - }; - - return ( - - - - - - -
-

{user?.firstName}

-

{email}

-
-
- - - -
Log out
-
-
-
- ); -} diff --git a/apps/client/src/app/settings/appearance/appearance-form.tsx b/apps/client/src/app/settings/appearance/appearance-form.tsx index c3c1be7..896b806 100644 --- a/apps/client/src/app/settings/appearance/appearance-form.tsx +++ b/apps/client/src/app/settings/appearance/appearance-form.tsx @@ -1,16 +1,8 @@ import { ThemeCustomizer } from '@/app/settings/appearance/theme-customizer'; -import { ThemeQuickCustomizer } from '@/shared/components/theme-quick-customizer'; export function AppearanceForm() { return (
-
-
-
Quick Customize
-
Pick a style and color from a preset.
-
- -
); diff --git a/apps/client/src/index.css b/apps/client/src/index.css index 7a3385e..17cccdc 100644 --- a/apps/client/src/index.css +++ b/apps/client/src/index.css @@ -13,6 +13,39 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .light .theme-zinc { @@ -304,6 +337,40 @@ --calendar-value-verylow: var(--primary-foreground); --calendar-value-empty: var(--border); } + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } } @layer base { diff --git a/apps/client/src/routeTree.gen.ts b/apps/client/src/routeTree.gen.ts index ca76766..e28aae5 100644 --- a/apps/client/src/routeTree.gen.ts +++ b/apps/client/src/routeTree.gen.ts @@ -1,12 +1,12 @@ -/* prettier-ignore-start */ - /* eslint-disable */ // @ts-nocheck // noinspection JSUnusedGlobalSymbols -// This file is auto-generated by TanStack Router +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Import Routes @@ -24,11 +24,13 @@ import { Route as ProtectedSettingsSectionImport } from './routes/_protected/set // Create/Update Routes const SsoCallbackRoute = SsoCallbackImport.update({ + id: '/sso-callback', path: '/sso-callback', getParentRoute: () => rootRoute, } as any) const AuthRoute = AuthImport.update({ + id: '/auth', path: '/auth', getParentRoute: () => rootRoute, } as any) @@ -39,31 +41,37 @@ const ProtectedRoute = ProtectedImport.update({ } as any) const ProtectedIndexRoute = ProtectedIndexImport.update({ + id: '/', path: '/', getParentRoute: () => ProtectedRoute, } as any) const ProtectedWelcomeRoute = ProtectedWelcomeImport.update({ + id: '/welcome', path: '/welcome', getParentRoute: () => ProtectedRoute, } as any) const ProtectedSettingsRoute = ProtectedSettingsImport.update({ + id: '/settings', path: '/settings', getParentRoute: () => ProtectedRoute, } as any) const ProtectedHomeRoute = ProtectedHomeImport.update({ + id: '/home', path: '/home', getParentRoute: () => ProtectedRoute, } as any) const ProtectedBudgetRoute = ProtectedBudgetImport.update({ + id: '/budget', path: '/budget', getParentRoute: () => ProtectedRoute, } as any) const ProtectedSettingsSectionRoute = ProtectedSettingsSectionImport.update({ + id: '/$section', path: '/$section', getParentRoute: () => ProtectedSettingsRoute, } as any) @@ -140,21 +148,124 @@ declare module '@tanstack/react-router' { // Create and export the route tree -export const routeTree = rootRoute.addChildren({ - ProtectedRoute: ProtectedRoute.addChildren({ - ProtectedBudgetRoute, - ProtectedHomeRoute, - ProtectedSettingsRoute: ProtectedSettingsRoute.addChildren({ - ProtectedSettingsSectionRoute, - }), - ProtectedWelcomeRoute, - ProtectedIndexRoute, - }), - AuthRoute, - SsoCallbackRoute, -}) - -/* prettier-ignore-end */ +interface ProtectedSettingsRouteChildren { + ProtectedSettingsSectionRoute: typeof ProtectedSettingsSectionRoute +} + +const ProtectedSettingsRouteChildren: ProtectedSettingsRouteChildren = { + ProtectedSettingsSectionRoute: ProtectedSettingsSectionRoute, +} + +const ProtectedSettingsRouteWithChildren = + ProtectedSettingsRoute._addFileChildren(ProtectedSettingsRouteChildren) + +interface ProtectedRouteChildren { + ProtectedBudgetRoute: typeof ProtectedBudgetRoute + ProtectedHomeRoute: typeof ProtectedHomeRoute + ProtectedSettingsRoute: typeof ProtectedSettingsRouteWithChildren + ProtectedWelcomeRoute: typeof ProtectedWelcomeRoute + ProtectedIndexRoute: typeof ProtectedIndexRoute +} + +const ProtectedRouteChildren: ProtectedRouteChildren = { + ProtectedBudgetRoute: ProtectedBudgetRoute, + ProtectedHomeRoute: ProtectedHomeRoute, + ProtectedSettingsRoute: ProtectedSettingsRouteWithChildren, + ProtectedWelcomeRoute: ProtectedWelcomeRoute, + ProtectedIndexRoute: ProtectedIndexRoute, +} + +const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren( + ProtectedRouteChildren, +) + +export interface FileRoutesByFullPath { + '': typeof ProtectedRouteWithChildren + '/auth': typeof AuthRoute + '/sso-callback': typeof SsoCallbackRoute + '/budget': typeof ProtectedBudgetRoute + '/home': typeof ProtectedHomeRoute + '/settings': typeof ProtectedSettingsRouteWithChildren + '/welcome': typeof ProtectedWelcomeRoute + '/': typeof ProtectedIndexRoute + '/settings/$section': typeof ProtectedSettingsSectionRoute +} + +export interface FileRoutesByTo { + '/auth': typeof AuthRoute + '/sso-callback': typeof SsoCallbackRoute + '/budget': typeof ProtectedBudgetRoute + '/home': typeof ProtectedHomeRoute + '/settings': typeof ProtectedSettingsRouteWithChildren + '/welcome': typeof ProtectedWelcomeRoute + '/': typeof ProtectedIndexRoute + '/settings/$section': typeof ProtectedSettingsSectionRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/_protected': typeof ProtectedRouteWithChildren + '/auth': typeof AuthRoute + '/sso-callback': typeof SsoCallbackRoute + '/_protected/budget': typeof ProtectedBudgetRoute + '/_protected/home': typeof ProtectedHomeRoute + '/_protected/settings': typeof ProtectedSettingsRouteWithChildren + '/_protected/welcome': typeof ProtectedWelcomeRoute + '/_protected/': typeof ProtectedIndexRoute + '/_protected/settings/$section': typeof ProtectedSettingsSectionRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '' + | '/auth' + | '/sso-callback' + | '/budget' + | '/home' + | '/settings' + | '/welcome' + | '/' + | '/settings/$section' + fileRoutesByTo: FileRoutesByTo + to: + | '/auth' + | '/sso-callback' + | '/budget' + | '/home' + | '/settings' + | '/welcome' + | '/' + | '/settings/$section' + id: + | '__root__' + | '/_protected' + | '/auth' + | '/sso-callback' + | '/_protected/budget' + | '/_protected/home' + | '/_protected/settings' + | '/_protected/welcome' + | '/_protected/' + | '/_protected/settings/$section' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + ProtectedRoute: typeof ProtectedRouteWithChildren + AuthRoute: typeof AuthRoute + SsoCallbackRoute: typeof SsoCallbackRoute +} + +const rootRouteChildren: RootRouteChildren = { + ProtectedRoute: ProtectedRouteWithChildren, + AuthRoute: AuthRoute, + SsoCallbackRoute: SsoCallbackRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() /* ROUTE_MANIFEST_START { diff --git a/apps/client/src/routes/_protected.tsx b/apps/client/src/routes/_protected.tsx index 908fa4e..bc6f0d8 100644 --- a/apps/client/src/routes/_protected.tsx +++ b/apps/client/src/routes/_protected.tsx @@ -4,7 +4,8 @@ import { createFileRoute, Outlet, redirect, useNavigate } from '@tanstack/react- import { AppSidebar } from '@/app/root/app-sidebar'; import { useSignInUser } from '@/app/root/use-signin-user'; import { InitLoader } from '@/shared/components/ui/loaders/init-loader'; -import { cn } from '@/shared/lib/helpers'; +import { Separator } from '@/shared/components/ui/separator'; +import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/shared/components/ui/sidebar'; import { stringIsNotEmptyOrWhitespace } from '@/shared/utils/string'; export const Route = createFileRoute('/_protected')({ @@ -34,19 +35,38 @@ function Protected() { }, [isFirstTime, navigate, query.data.data, query.isSuccess, query.status]); return ( -
- {/* Sidebar */} - {isFirstTime ? undefined : ( -
- -
- )} - {/* Content */} -
-
+ + {isFirstTime ? undefined : } + + {isFirstTime ? undefined : ( +
+
+ + + {/* TODO add breadcrumbs */} + {/* + + + Building Your Application + + + + Data Fetching + + + */} +
+
+ )} + {/* Content */} +
+ {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/}
-
-
+ + ); } diff --git a/apps/client/src/routes/_protected/budget.tsx b/apps/client/src/routes/_protected/budget.tsx index 085c24d..69c4e49 100644 --- a/apps/client/src/routes/_protected/budget.tsx +++ b/apps/client/src/routes/_protected/budget.tsx @@ -38,16 +38,16 @@ const searchSchema = z.object({ }); export const Route = createFileRoute('/_protected/budget')({ + validateSearch: searchSchema, loaderDeps: ({ search: { month, year } }) => ({ month, year }), - loader: ({ deps: { month, year }, context: { queryClient } }) => { - return queryClient.ensureQueryData(getTransactionsQuery(year, month)); - }, preSearchFilters: [ (search) => ({ ...search, }), ], - validateSearch: searchSchema, + loader: ({ deps: { month, year }, context: { queryClient } }) => { + return queryClient.ensureQueryData(getTransactionsQuery(year, month)); + }, component: BudgetPage, notFoundComponent: NotFound, pendingComponent: () => , diff --git a/apps/client/src/shared/components/errors/app-error.tsx b/apps/client/src/shared/components/errors/app-error.tsx index a0af1b1..bcc60fd 100644 --- a/apps/client/src/shared/components/errors/app-error.tsx +++ b/apps/client/src/shared/components/errors/app-error.tsx @@ -11,7 +11,7 @@ import { cn } from '@/shared/lib/helpers'; type Props = { resetErrorBoundary?: () => void } & Partial; const handleGotToRoot = () => { - window.location.href = '/'; + globalThis.location.href = '/'; }; export function AppError({ error, info, resetErrorBoundary }: Props) { diff --git a/apps/client/src/shared/components/theme-mode-switcher.tsx b/apps/client/src/shared/components/theme-mode-switcher.tsx index 46e97c9..992caa8 100644 --- a/apps/client/src/shared/components/theme-mode-switcher.tsx +++ b/apps/client/src/shared/components/theme-mode-switcher.tsx @@ -6,12 +6,12 @@ export function ThemeModeSwitcher() { const { mode } = useThemeStore(); useEffect(() => { - const root = window.document.documentElement; + const root = globalThis.document.documentElement; root.classList.remove('light', 'dark'); if (mode === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + const systemTheme = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; root.classList.add(systemTheme); return; @@ -21,14 +21,14 @@ export function ThemeModeSwitcher() { }, [mode]); useEffect(() => { - const root = window.document.documentElement; + const root = globalThis.document.documentElement; const listener = (event: MediaQueryListEvent) => { root.classList.remove('light', 'dark'); root.classList.add(event.matches ? 'dark' : 'light'); }; - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', listener); + globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', listener); }, []); // eslint-disable-next-line unicorn/no-useless-undefined diff --git a/apps/client/src/shared/components/ui/accordion.tsx b/apps/client/src/shared/components/ui/accordion.tsx index 84be93b..4046f75 100644 --- a/apps/client/src/shared/components/ui/accordion.tsx +++ b/apps/client/src/shared/components/ui/accordion.tsx @@ -1,4 +1,4 @@ -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'; +import * as React from 'react'; import * as AccordionPrimitive from '@radix-ui/react-accordion'; import { ChevronDown } from 'lucide-react'; @@ -6,17 +6,17 @@ import { cn } from '@/shared/lib/helpers'; const Accordion = AccordionPrimitive.Root; -const AccordionItem = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AccordionItem.displayName = 'AccordionItem'; -const AccordionTrigger = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( , - ComponentPropsWithoutRef +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( -
{children}
+
{children}
)); + AccordionContent.displayName = AccordionPrimitive.Content.displayName; export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/apps/client/src/shared/components/ui/alert-dialog.tsx b/apps/client/src/shared/components/ui/alert-dialog.tsx index 0f3ac59..8f98823 100644 --- a/apps/client/src/shared/components/ui/alert-dialog.tsx +++ b/apps/client/src/shared/components/ui/alert-dialog.tsx @@ -1,4 +1,4 @@ -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'; +import * as React from 'react'; import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; import { buttonVariants } from '@/shared/components/ui/button'; @@ -8,18 +8,15 @@ const AlertDialog = AlertDialogPrimitive.Root; const AlertDialogTrigger = AlertDialogPrimitive.Trigger; -const AlertDialogPortal = ({ ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => ( - -); -AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName; +const AlertDialogPortal = AlertDialogPrimitive.Portal; -const AlertDialogOverlay = forwardRef< - ElementRef, - Omit, 'children'> +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( , - ComponentPropsWithoutRef +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( , - ComponentPropsWithoutRef +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; -const AlertDialogDescription = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; -const AlertDialogAction = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; -const AlertDialogCancel = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', - variants: { - variant: { - default: 'bg-background text-foreground', - destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', +const alertVariants = cva( + 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', + }, + }, + defaultVariants: { + variant: 'default', }, }, - defaultVariants: { - variant: 'default', - }, -}); +); const Alert = React.forwardRef< HTMLDivElement, diff --git a/apps/client/src/shared/components/ui/badge.tsx b/apps/client/src/shared/components/ui/badge.tsx index 04f50e7..21d6720 100644 --- a/apps/client/src/shared/components/ui/badge.tsx +++ b/apps/client/src/shared/components/ui/badge.tsx @@ -1,23 +1,25 @@ import * as React from 'react'; -import { tv } from 'tailwind-variants'; +import { cva } from 'class-variance-authority'; import { cn } from '@/shared/lib/helpers'; -import type { VariantProps } from 'tailwind-variants'; +import type { VariantProps } from 'class-variance-authority'; -const badgeVariants = tv({ - base: 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', - variants: { - variant: { - default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', - secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', - destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', - outline: 'text-foreground', +const badgeVariants = cva( + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', + secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', }, }, - defaultVariants: { - variant: 'default', - }, -}); +); export interface BadgeProps extends React.HTMLAttributes, VariantProps {} @@ -25,4 +27,4 @@ function Badge({ className, variant, ...props }: BadgeProps) { return
; } -export { Badge }; +export { Badge, badgeVariants }; diff --git a/apps/client/src/shared/components/ui/breadcrumb.tsx b/apps/client/src/shared/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..b76577f --- /dev/null +++ b/apps/client/src/shared/components/ui/breadcrumb.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { ChevronRight, MoreHorizontal } from 'lucide-react'; + +import { cn } from '@/shared/lib/helpers'; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<'nav'> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>