diff --git a/user-management/.gitignore b/user-management/.gitignore index 2af8d5e5..ee7b9c53 100644 --- a/user-management/.gitignore +++ b/user-management/.gitignore @@ -202,6 +202,9 @@ orleans.codegen.cs # RIA/Silverlight projects Generated_Code/ +**/net8.0/* +**/tmp/* + # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) diff --git a/user-management/Stickerlandia.UserManagement.sln b/user-management/Stickerlandia.UserManagement.sln index b267d331..ee063039 100644 --- a/user-management/Stickerlandia.UserManagement.sln +++ b/user-management/Stickerlandia.UserManagement.sln @@ -27,7 +27,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{FC9DDF76-8 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagement.Agnostic", "src\Stickerlandia.UserManagement.Agnostic\Stickerlandia.UserManagement.Agnostic.csproj", "{295E8315-2CD2-4CD8-B7F4-21EF587B74A9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagement.Lambda", "src\Stickerlandia.UserManagement.Lambda\src\Stickerlandia.UserManagement.Lambda\Stickerlandia.UserManagement.Lambda.csproj", "{5C770F92-B6E7-4270-A041-A32F385DA6C4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagement.Lambda", "src\Stickerlandia.UserManagement.Lambda\Stickerlandia.UserManagement.Lambda.csproj", "{5C770F92-B6E7-4270-A041-A32F385DA6C4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Driving", "Driving", "{924749FC-98D5-40A6-8F4B-BA9BFDEDDEC3}" EndProject @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagemen EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagement.MigrationService", "src\Stickerlandia.UserManagement.MigrationService\Stickerlandia.UserManagement.MigrationService.csproj", "{91536DB3-CBA5-44DE-943C-76F8577793FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManagementService", "infra\aws\src\UserManagementService.csproj", "{57FBF8EA-3559-45A5-ACE1-823B2225E370}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -106,6 +108,10 @@ Global {91536DB3-CBA5-44DE-943C-76F8577793FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {91536DB3-CBA5-44DE-943C-76F8577793FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {91536DB3-CBA5-44DE-943C-76F8577793FE}.Release|Any CPU.Build.0 = Release|Any CPU + {57FBF8EA-3559-45A5-ACE1-823B2225E370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57FBF8EA-3559-45A5-ACE1-823B2225E370}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57FBF8EA-3559-45A5-ACE1-823B2225E370}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57FBF8EA-3559-45A5-ACE1-823B2225E370}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {4DDC795B-2EA4-4123-B631-F0D8CAA99BA6} = {FE220876-2B87-4095-8BD2-039049503C72} diff --git a/user-management/infra/aws/README.md b/user-management/infra/aws/README.md new file mode 100644 index 00000000..f28e4d55 --- /dev/null +++ b/user-management/infra/aws/README.md @@ -0,0 +1,14 @@ +# Welcome to your CDK C# project! + +This is a blank project for CDK development with C#. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +It uses the [.NET CLI](https://docs.microsoft.com/dotnet/articles/core/) to compile and execute your project. + +## Useful commands + +* `dotnet build src` compile this app +* `cdk deploy` deploy this stack to your default AWS account/region +* `cdk diff` compare deployed stack with current state +* `cdk synth` emits the synthesized CloudFormation template \ No newline at end of file diff --git a/user-management/infra/aws/cdk.json b/user-management/infra/aws/cdk.json new file mode 100644 index 00000000..5e22f387 --- /dev/null +++ b/user-management/infra/aws/cdk.json @@ -0,0 +1,94 @@ +{ + "app": "dotnet run --project src/UserManagementService.csproj", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/GlobalSuppressions.cs", + "src/*/*.csproj" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true + } +} diff --git a/user-management/infra/aws/src/GlobalSuppressions.cs b/user-management/infra/aws/src/GlobalSuppressions.cs new file mode 100644 index 00000000..26233fcb --- /dev/null +++ b/user-management/infra/aws/src/GlobalSuppressions.cs @@ -0,0 +1 @@ +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "RECS0026:Possible unassigned object created by 'new'", Justification = "Constructs add themselves to the scope in which they are created")] diff --git a/user-management/infra/aws/src/InstrumentedFunction.cs b/user-management/infra/aws/src/InstrumentedFunction.cs new file mode 100644 index 00000000..c27f6e10 --- /dev/null +++ b/user-management/infra/aws/src/InstrumentedFunction.cs @@ -0,0 +1,96 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +using Amazon.CDK; +using Amazon.CDK.AWS.IAM; +using Amazon.CDK.AWS.Lambda; +using Amazon.CDK.AWS.Lambda.DotNet; +using Amazon.CDK.AWS.Logs; +using Amazon.CDK.AWS.SecretsManager; +using Constructs; +using Environment = System.Environment; + +namespace UserManagementService; + +public record FunctionProps( + SharedProps Shared, + string FunctionName, + string ProjectPath, + string Handler, + Dictionary EnvironmentVariables); + +public class InstrumentedFunction : Construct +{ + public IFunction Function { get; private set; } + + public InstrumentedFunction(Construct scope, string id, FunctionProps props) : base(scope, id) + { + if (props.Handler.Length > 128) + throw new Exception( + "Function handler cannot be greater than 128 chars. https://docs.aws.amazon.com/lambda/latest/api/API_CreateFunction.html#lambda-CreateFunction-request-Handler"); + var functionName = $"{props.Shared.ServiceName}-{props.FunctionName}-{props.Shared.Env}"; + + if (functionName.Length > 64) + { + var extraCharacters = functionName.Length - 64; + + functionName = + $"{props.Shared.ServiceName}-{props.FunctionName.Substring(0, props.FunctionName.Length - extraCharacters)}-{props.Shared.Env}"; + } + + var defaultEnvironmentVariables = new Dictionary() + { + { "POWERTOOLS_SERVICE_NAME", props.Shared.ServiceName }, + { "POWERTOOLS_LOG_LEVEL", "DEBUG" }, + { "AWS_LAMBDA_EXEC_WRAPPER", "/opt/datadog_wrapper" }, + { "DD_SITE", Environment.GetEnvironmentVariable("DD_SITE") }, + { "DD_ENV", props.Shared.Env }, + { "ENV", props.Shared.Env }, + { "DD_VERSION", props.Shared.Version }, + { "DD_SERVICE", props.Shared.ServiceName }, + { "DD_API_KEY_SECRET_ARN", props.Shared.DDApiKeySecret.SecretArn }, + { "DD_CAPTURE_LAMBDA_PAYLOAD", "true" }, + { "DOMAIN", props.Shared.Domain }, + { "TEAM", props.Shared.Team } + }; + + Function = new DotNetFunction(this, id, + new DotNetFunctionProps + { + ProjectDir = props.ProjectPath, + Handler = props.Handler, + MemorySize = 1024, + Timeout = Duration.Seconds(29), + Runtime = Runtime.DOTNET_8, + Environment = defaultEnvironmentVariables.Union(props.EnvironmentVariables) + .ToDictionary(x => x.Key, x => x.Value), + Architecture = Architecture.ARM_64, + FunctionName = functionName, + LogRetention = RetentionDays.ONE_DAY, + Layers = + [ + LayerVersion.FromLayerVersionArn(this, "DDExtension", + $"arn:aws:lambda:{Environment.GetEnvironmentVariable("AWS_REGION") ?? "us-east-1"}:464622532012:layer:Datadog-Extension-ARM:80"), + LayerVersion.FromLayerVersionArn(this, "DDTrace", + $"arn:aws:lambda:{Environment.GetEnvironmentVariable("AWS_REGION") ?? "us-east-1"}:464622532012:layer:dd-trace-dotnet-ARM:20") + ] + }); + + // The Datadog extension sends log data to Datadog using the telemetry API, disabling CloudWatch prevents 'double paying' for logs + if (Environment.GetEnvironmentVariable("ENABLE_CLOUDWATCH_LOGS") != "Y") + Function.AddToRolePolicy(new PolicyStatement(new PolicyStatementProps() + { + Resources = new[] { "arn:aws:logs:*:*:*" }, + Actions = new[] + { + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + }, + Effect = Effect.DENY + })); + + props.Shared.DDApiKeySecret.GrantRead(Function); + } +} \ No newline at end of file diff --git a/user-management/infra/aws/src/Program.cs b/user-management/infra/aws/src/Program.cs new file mode 100644 index 00000000..77b16805 --- /dev/null +++ b/user-management/infra/aws/src/Program.cs @@ -0,0 +1,16 @@ +using Amazon.CDK; + +namespace UserManagementService +{ + sealed class Program + { + public static void Main(string[] args) + { + var app = new App(); + var userManagementServiceStack = new UserManagementServiceStack(app, "UserManagementServiceStack", new StackProps()); + userManagementServiceStack.Tags.SetTag("team", "users"); + userManagementServiceStack.Tags.SetTag("domain", "users"); + app.Synth(); + } + } +} diff --git a/user-management/infra/aws/src/SharedProps.cs b/user-management/infra/aws/src/SharedProps.cs new file mode 100644 index 00000000..8322cd7c --- /dev/null +++ b/user-management/infra/aws/src/SharedProps.cs @@ -0,0 +1,9 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +using Amazon.CDK.AWS.SecretsManager; + +namespace UserManagementService; + +public record SharedProps(string ServiceName, string Env, string Version, string Team, string Domain, ISecret DDApiKeySecret, string DDSite); \ No newline at end of file diff --git a/user-management/infra/aws/src/UserManagementService.csproj b/user-management/infra/aws/src/UserManagementService.csproj new file mode 100644 index 00000000..8cde52ce --- /dev/null +++ b/user-management/infra/aws/src/UserManagementService.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + + Major + enable + + + + + + + + + + + diff --git a/user-management/infra/aws/src/UserManagementServiceStack.cs b/user-management/infra/aws/src/UserManagementServiceStack.cs new file mode 100644 index 00000000..33577b3c --- /dev/null +++ b/user-management/infra/aws/src/UserManagementServiceStack.cs @@ -0,0 +1,138 @@ +using Amazon.CDK; +using Amazon.CDK.AWS.APIGateway; +using Amazon.CDK.AWS.Events; +using Amazon.CDK.AWS.Events.Targets; +using Amazon.CDK.AWS.Lambda.EventSources; +using Amazon.CDK.AWS.SecretsManager; +using Amazon.CDK.AWS.SNS; +using Amazon.CDK.AWS.SQS; +using Constructs; + +namespace UserManagementService; + +public class UserManagementServiceStack : Stack +{ + internal UserManagementServiceStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + var serviceName = "UserManagementService"; + var env = System.Environment.GetEnvironmentVariable("ENV") ?? "dev"; + var version = System.Environment.GetEnvironmentVariable("VERSION") ?? "latest"; + var ddSite = System.Environment.GetEnvironmentVariable("DD_SITE") ?? "datadoghq.com"; + + var secret = new Secret(this, "DDApiKeySecret", new SecretProps + { + SecretName = $"/{env}/{serviceName}/dd-api-key", + SecretStringValue = new SecretValue(System.Environment.GetEnvironmentVariable("DD_API_KEY") ?? + throw new Exception("DD_API_KEY environment variable is not set")) + }); + + var team = "users"; + var domain = "users"; + var sharedProps = new SharedProps(serviceName, env, version, team, domain, secret, ddSite); + + var userRegisteredTopic = new Topic(this, "UserRegisteredTopic", new TopicProps + { + TopicName = $"{sharedProps.ServiceName}-user-registered-{sharedProps.Env}" + }); + var stickerClaimedDLQ = new Queue(this, "StickerClaimedDLQ", new QueueProps + { + QueueName = $"{sharedProps.ServiceName}-sticker-claimed-dlq-{sharedProps.Env}" + }); + var stickerClaimedQueue = new Queue(this, "StickerClaimedQueue", new QueueProps + { + QueueName = $"{sharedProps.ServiceName}-sticker-claimed-{sharedProps.Env}", + DeadLetterQueue = new DeadLetterQueue + { + MaxReceiveCount = 3, + Queue = stickerClaimedDLQ + } + }); + + var defaultEnvironmentVariables = new Dictionary + { + { + "ConnectionStrings__database", + "Host=ep-weathered-wave-ab469hjs-pooler.eu-west-2.aws.neon.tech;Port=5432;Username=stickerlandia-users_owner;Password=npg_buwe2PoK1NgV;Database=stickerlandia-users" + }, + { "Aws__UserRegisteredTopicArn", userRegisteredTopic.TopicArn }, + { "Aws__StickerClaimedQueueUrl", stickerClaimedQueue.QueueUrl }, + { "Aws__StickerClaimedDLQUrl", stickerClaimedDLQ.QueueUrl }, + { "DRIVING", "AWS" }, + { "DRIVEN", "AWS" } + }; + + var dbMigrationFunction = new InstrumentedFunction( + this, + "UserManagementDbMigrationFunction", + new FunctionProps( + sharedProps, + "UserManagementDbMigration", + "../../src/Stickerlandia.UserManagement.Lambda/", + "Stickerlandia.UserManagement.Lambda::Stickerlandia.UserManagement.Lambda.MigrationFunction_Migrate_Generated::Migrate", + defaultEnvironmentVariables + ) + ); + + var stickerClaimedFunction = new InstrumentedFunction( + this, + "UserManagementStickerClaimedFunction", + new FunctionProps( + sharedProps, + "UserManagementStickerClaimedFunction", + "../../src/Stickerlandia.UserManagement.Lambda/", + "Stickerlandia.UserManagement.Lambda::Stickerlandia.UserManagement.Lambda.Sqs_StickerClaimed_Generated::StickerClaimed", + defaultEnvironmentVariables + ) + ); + stickerClaimedFunction.Function.AddEventSource(new SqsEventSource(stickerClaimedQueue, + new SqsEventSourceProps + { + ReportBatchItemFailures = true + })); + + var outboxWorker = new InstrumentedFunction( + this, + "UserManagementOutboxWorker", + new FunctionProps( + sharedProps, + "UserManagementOutboxWorker", + "../../src/Stickerlandia.UserManagement.Lambda/", + "Stickerlandia.UserManagement.Lambda::Stickerlandia.UserManagement.Lambda.OutboxFunctions_Worker_Generated::Worker", + defaultEnvironmentVariables + ) + ); + // To run the outbox worker once per minute + var scheduleRule = new Rule(this, "OutboxWorkerSchedule", new RuleProps + { + Schedule = Schedule.Rate(Duration.Minutes(1)), + Targets = new IRuleTarget[] + { + new LambdaFunction(outboxWorker.Function, new LambdaFunctionProps + { + // Optional: Retry policy + RetryAttempts = 2 + }) + } + }); + userRegisteredTopic.GrantPublish(outboxWorker.Function); + + var apiFunction = new InstrumentedFunction( + this, + "UserManagementApiFunction", + new FunctionProps( + sharedProps, + "UserManagementApi", + "../../src/Stickerlandia.UserManagement.Api/", + "Stickerlandia.UserManagement.Api", + defaultEnvironmentVariables + ) + ); + + var api = new RestApi(this, "UserManagementApi", new RestApiProps + { + RestApiName = $"${serviceName}-api-${env}" + }); + var proxyResource = api.Root.AddResource("{proxy+}"); + proxyResource.AddMethod("ANY", new LambdaIntegration(apiFunction.Function)); + } +} \ No newline at end of file diff --git a/user-management/infra/database.tf b/user-management/infra/azure/database.tf similarity index 100% rename from user-management/infra/database.tf rename to user-management/infra/azure/database.tf diff --git a/user-management/infra/main.tf b/user-management/infra/azure/main.tf similarity index 100% rename from user-management/infra/main.tf rename to user-management/infra/azure/main.tf diff --git a/user-management/infra/messaging.tf b/user-management/infra/azure/messaging.tf similarity index 100% rename from user-management/infra/messaging.tf rename to user-management/infra/azure/messaging.tf diff --git a/user-management/infra/providers.tf b/user-management/infra/azure/providers.tf similarity index 100% rename from user-management/infra/providers.tf rename to user-management/infra/azure/providers.tf diff --git a/user-management/infra/variables.tf b/user-management/infra/azure/variables.tf similarity index 100% rename from user-management/infra/variables.tf rename to user-management/infra/azure/variables.tf diff --git a/user-management/src/Stickerlandia.UserManagement.AWS/AwsConfiguration.cs b/user-management/src/Stickerlandia.UserManagement.AWS/AwsConfiguration.cs new file mode 100644 index 00000000..b10c2e6c --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.AWS/AwsConfiguration.cs @@ -0,0 +1,14 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +namespace Stickerlandia.UserManagement.AWS; + +public class AwsConfiguration +{ + public string UserRegisteredTopicArn { get; set; } = string.Empty; + + public string StickerClaimedQueueUrl { get; set; } = string.Empty; + + public string StickerClaimedDLQUrl { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.AWS/Class1.cs b/user-management/src/Stickerlandia.UserManagement.AWS/Class1.cs deleted file mode 100644 index e375d8be..00000000 --- a/user-management/src/Stickerlandia.UserManagement.AWS/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Stickerlandia.UserManagement.AWS; - -public class Class1 -{ -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.AWS/ServiceExtensions.cs b/user-management/src/Stickerlandia.UserManagement.AWS/ServiceExtensions.cs new file mode 100644 index 00000000..17001b54 --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.AWS/ServiceExtensions.cs @@ -0,0 +1,32 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +using Amazon.SimpleNotificationService; +using Amazon.SQS; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Stickerlandia.UserManagement.Agnostic; +using Stickerlandia.UserManagement.Core; + +namespace Stickerlandia.UserManagement.AWS; + +public static class ServiceExtensions +{ + public static IServiceCollection AddAwsAdapters(this IServiceCollection services, IConfiguration configuration) + { + services.AddAWSLambdaHosting(LambdaEventSource.RestApi); + services.AddPostgresAuthServices(configuration); + + services.Configure( + configuration.GetSection("Aws")); + + services.AddSingleton(); + services.AddSingleton(new AmazonSQSClient()); + services.AddSingleton(new AmazonSimpleNotificationServiceClient()); + + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.AWS/SnsEventPublisher.cs b/user-management/src/Stickerlandia.UserManagement.AWS/SnsEventPublisher.cs new file mode 100644 index 00000000..6a14fe2f --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.AWS/SnsEventPublisher.cs @@ -0,0 +1,73 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using CloudNative.CloudEvents; +using CloudNative.CloudEvents.SystemTextJson; +using Datadog.Trace; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Saunter.Attributes; +using Stickerlandia.UserManagement.Core; +using Stickerlandia.UserManagement.Core.RegisterUser; + +namespace Stickerlandia.UserManagement.AWS; + +[AsyncApi] +public class SnsEventPublisher( + ILogger logger, + AmazonSimpleNotificationServiceClient client, + IOptions awsConfiguration) : IUserEventPublisher +{ + [Channel("users.userRegistered.v1")] + [PublishOperation(typeof(UserRegisteredEvent))] + public async Task PublishUserRegisteredEventV1(UserRegisteredEvent userRegisteredEvent) + { + var cloudEvent = new CloudEvent(CloudEventsSpecVersion.V1_0) + { + Id = Guid.NewGuid().ToString(), + Source = new Uri("https://stickerlandia.com"), + Type = userRegisteredEvent.EventName, + Time = DateTime.UtcNow, + Data = userRegisteredEvent + }; + + await Publish(cloudEvent); + } + + private async Task Publish(CloudEvent cloudEvent) + { + var activeSpan = Tracer.Instance.ActiveScope?.Span; + IScope? processScope = null; + + try + { + if (activeSpan != null) + { + processScope = Tracer.Instance.StartActive($"publish {cloudEvent.Type}", new SpanCreationSettings + { + Parent = activeSpan.Context + }); + + cloudEvent.SetAttributeFromString("traceparent", $"00-{activeSpan.TraceId}-{activeSpan.SpanId}[01"); + } + + var formatter = new JsonEventFormatter(); + var data = formatter.EncodeStructuredModeMessage(cloudEvent, out _); + var jsonString = System.Text.Encoding.UTF8.GetString(data.Span); + + await client.PublishAsync(new PublishRequest(awsConfiguration.Value.UserRegisteredTopicArn, jsonString)); + } + catch (Exception ex) + { + logger.LogError(ex, "Failure publishing message"); + processScope?.Span.SetException(ex); + } + finally + { + processScope?.Close(); + } + } +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.AWS/SqsStickerClaimedWorker.cs b/user-management/src/Stickerlandia.UserManagement.AWS/SqsStickerClaimedWorker.cs new file mode 100644 index 00000000..2d99b1df --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.AWS/SqsStickerClaimedWorker.cs @@ -0,0 +1,88 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +using System.Text.Json; +using Amazon.SQS; +using Amazon.SQS.Model; +using Datadog.Trace; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Saunter.Attributes; +using Stickerlandia.UserManagement.Core; +using Stickerlandia.UserManagement.Core.StickerClaimedEvent; + +namespace Stickerlandia.UserManagement.AWS; + +[AsyncApi] +public class SqsStickerClaimedWorker : IMessagingWorker +{ + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger _logger; + private readonly AmazonSQSClient _sqsClient; + private readonly IOptions _awsConfiguration; + + public SqsStickerClaimedWorker(ILogger logger, + AmazonSQSClient sqsClient, IServiceScopeFactory serviceScopeFactory, + IOptions awsConfiguration) + { + _logger = logger; + _sqsClient = sqsClient; + _serviceScopeFactory = serviceScopeFactory; + _awsConfiguration = awsConfiguration; + } + + [Channel("users.stickerClaimed.v1")] + [SubscribeOperation(typeof(StickerClaimedEventV1))] + private async Task ProcessMessageAsync(Message message) + { + using var processSpan = Tracer.Instance.StartActive($"process users.stickerClaimed.v1"); + + using var scope = _serviceScopeFactory.CreateScope(); + var handler = scope.ServiceProvider.GetRequiredService(); + + var evtData = JsonSerializer.Deserialize(message.Body); + + if (evtData == null) + { + await _sqsClient.SendMessageAsync(_awsConfiguration.Value.StickerClaimedDLQUrl, message.Body); + return; + } + + // Process your message here + try + { + await handler.Handle(evtData!); + } + catch (InvalidUserException ex) + { + _logger.LogWarning(ex, "User with account in this event not found"); + await _sqsClient.SendMessageAsync(_awsConfiguration.Value.StickerClaimedDLQUrl, message.Body); + } + + await _sqsClient.DeleteMessageAsync(_awsConfiguration.Value.StickerClaimedQueueUrl, message.ReceiptHandle); + } + + public Task StartAsync() + { + _logger.LogInformation("Starting SQS processor"); + + return Task.CompletedTask; + } + + public async Task PollAsync(CancellationToken cancellationToken) + { + var messages = + await _sqsClient.ReceiveMessageAsync(_awsConfiguration.Value.StickerClaimedQueueUrl, cancellationToken); + + foreach (var message in messages.Messages) await ProcessMessageAsync(message); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stopping SQS processor"); + // No specific stop logic for SQS, as it is a pull-based system. + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.AWS/Stickerlandia.UserManagement.AWS.csproj b/user-management/src/Stickerlandia.UserManagement.AWS/Stickerlandia.UserManagement.AWS.csproj index a688b61a..9598379f 100644 --- a/user-management/src/Stickerlandia.UserManagement.AWS/Stickerlandia.UserManagement.AWS.csproj +++ b/user-management/src/Stickerlandia.UserManagement.AWS/Stickerlandia.UserManagement.AWS.csproj @@ -7,15 +7,23 @@ + - + - - - + + + + + + + + + + diff --git a/user-management/src/Stickerlandia.UserManagement.Agnostic/DbContextFactory.cs b/user-management/src/Stickerlandia.UserManagement.Agnostic/DbContextFactory.cs index 4cf04f8b..6ed45cbf 100644 --- a/user-management/src/Stickerlandia.UserManagement.Agnostic/DbContextFactory.cs +++ b/user-management/src/Stickerlandia.UserManagement.Agnostic/DbContextFactory.cs @@ -1,8 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Stickerlandia.UserManagement.Core; -using Stickerlandia.UserManagement.Core.Outbox; namespace Stickerlandia.UserManagement.Agnostic; diff --git a/user-management/src/Stickerlandia.UserManagement.Agnostic/KafakStickerClaimedWorker.cs b/user-management/src/Stickerlandia.UserManagement.Agnostic/KafakStickerClaimedWorker.cs index 33b318ca..8c70787d 100644 --- a/user-management/src/Stickerlandia.UserManagement.Agnostic/KafakStickerClaimedWorker.cs +++ b/user-management/src/Stickerlandia.UserManagement.Agnostic/KafakStickerClaimedWorker.cs @@ -2,7 +2,6 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -using System.Text; using System.Text.Json; using Confluent.Kafka; using Datadog.Trace; diff --git a/user-management/src/Stickerlandia.UserManagement.Agnostic/MicrsoftIdentityAuthService.cs b/user-management/src/Stickerlandia.UserManagement.Agnostic/MicrsoftIdentityAuthService.cs index a32f9068..bfec5455 100644 --- a/user-management/src/Stickerlandia.UserManagement.Agnostic/MicrsoftIdentityAuthService.cs +++ b/user-management/src/Stickerlandia.UserManagement.Agnostic/MicrsoftIdentityAuthService.cs @@ -6,8 +6,6 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; @@ -53,17 +51,26 @@ OpenIddictConstants.Claims.Name when claim.Subject.HasScope(OpenIddictConstants. public async Task VerifyPassword(string username, string password, ImmutableArray scopes) { + logger.LogInformation("Attempting to verify password"); + var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Role); var user = await userManager.FindByEmailAsync(username); AuthenticationProperties properties = new(); if (user == null) + { + logger.LogWarning("User not found"); return null; + } // Validate the username/password parameters and ensure the account is not locked out. var result = await userManager.CheckPasswordAsync(user, password); - if (!result) return null; + if (!result) + { + logger.LogInformation("Password invalid"); + return null; + }; // The user is now validated, so reset lockout counts, if necessary if (userManager.SupportsUserLockout) await userManager.ResetAccessFailedCountAsync(user); @@ -83,6 +90,8 @@ OpenIddictConstants.Claims.Name when claim.Subject.HasScope(OpenIddictConstants. // Setting destinations of claims i.e. identity token or access token identity.SetDestinations(GetDestinations); + + logger.LogInformation("Password verified, returning identity"); return identity; } diff --git a/user-management/src/Stickerlandia.UserManagement.Agnostic/PostgresUserRepository.cs b/user-management/src/Stickerlandia.UserManagement.Agnostic/PostgresUserRepository.cs index e23cb279..50e7ed16 100644 --- a/user-management/src/Stickerlandia.UserManagement.Agnostic/PostgresUserRepository.cs +++ b/user-management/src/Stickerlandia.UserManagement.Agnostic/PostgresUserRepository.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Stickerlandia.UserManagement.Core; using Stickerlandia.UserManagement.Core.Outbox; diff --git a/user-management/src/Stickerlandia.UserManagement.Agnostic/ServiceExtensions.cs b/user-management/src/Stickerlandia.UserManagement.Agnostic/ServiceExtensions.cs index 590f128a..33d93aa2 100644 --- a/user-management/src/Stickerlandia.UserManagement.Agnostic/ServiceExtensions.cs +++ b/user-management/src/Stickerlandia.UserManagement.Agnostic/ServiceExtensions.cs @@ -2,15 +2,11 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -using System.Globalization; -using System.Text.Json; using Confluent.Kafka; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Stickerlandia.UserManagement.Auth; using Stickerlandia.UserManagement.Core; using Stickerlandia.UserManagement.Core.Outbox; diff --git a/user-management/src/Stickerlandia.UserManagement.Agnostic/UserManagementDbContext.cs b/user-management/src/Stickerlandia.UserManagement.Agnostic/UserManagementDbContext.cs index cee94a8e..c179a7ab 100644 --- a/user-management/src/Stickerlandia.UserManagement.Agnostic/UserManagementDbContext.cs +++ b/user-management/src/Stickerlandia.UserManagement.Agnostic/UserManagementDbContext.cs @@ -2,7 +2,6 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; diff --git a/user-management/src/Stickerlandia.UserManagement.Api/LoginEndpoint.cs b/user-management/src/Stickerlandia.UserManagement.Api/LoginEndpoint.cs index 3a20ac23..7b123ed1 100644 --- a/user-management/src/Stickerlandia.UserManagement.Api/LoginEndpoint.cs +++ b/user-management/src/Stickerlandia.UserManagement.Api/LoginEndpoint.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; +using Serilog; using Stickerlandia.UserManagement.Core; using Stickerlandia.UserManagement.Core.Login; @@ -14,11 +15,15 @@ public static async Task HandleAsync( LoginCommandHandler loginCommandHandler, HttpContext httpContext) { + Log.Logger.Information("Executing login command"); + var request = httpContext.GetOpenIddictServerRequest(); if (request.IsPasswordGrantType()) { - var identity = await authService.VerifyPassword(request.Username, request.Password, request.GetScopes()); + Log.Logger.Information("Is password grant, verifying user credentials"); + + var identity = await authService.VerifyPassword(request.Username!, request.Password!, request.GetScopes()); var signInResult = TypedResults.SignIn(new ClaimsPrincipal(identity), null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); return signInResult; diff --git a/user-management/src/Stickerlandia.UserManagement.Api/Program.cs b/user-management/src/Stickerlandia.UserManagement.Api/Program.cs index c9de475c..244c4ad4 100644 --- a/user-management/src/Stickerlandia.UserManagement.Api/Program.cs +++ b/user-management/src/Stickerlandia.UserManagement.Api/Program.cs @@ -5,17 +5,13 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Saunter; using Serilog; -using Serilog.Events; using Serilog.Formatting.Json; -using Stickerlandia.UserManagement.Agnostic; using Stickerlandia.UserManagement.Api; using Stickerlandia.UserManagement.Api.Configurations; using Stickerlandia.UserManagement.Api.Middlewares; using Stickerlandia.UserManagement.Core; using Stickerlandia.UserManagement.Core.Login; using Stickerlandia.UserManagement.Core.RegisterUser; -using Stickerlandia.UserManagement.Auth; -using Stickerlandia.UserManagement.Azure; using Stickerlandia.UserManagement.ServiceDefaults; var builder = WebApplication.CreateBuilder(args); diff --git a/user-management/src/Stickerlandia.UserManagement.Api/Stickerlandia.UserManagement.Api.csproj b/user-management/src/Stickerlandia.UserManagement.Api/Stickerlandia.UserManagement.Api.csproj index 55b4eb4f..66d9584c 100644 --- a/user-management/src/Stickerlandia.UserManagement.Api/Stickerlandia.UserManagement.Api.csproj +++ b/user-management/src/Stickerlandia.UserManagement.Api/Stickerlandia.UserManagement.Api.csproj @@ -10,11 +10,11 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/user-management/src/Stickerlandia.UserManagement.Aspire/AppBuilderExtensions.cs b/user-management/src/Stickerlandia.UserManagement.Aspire/AppBuilderExtensions.cs index 1cd69d93..2ba42f73 100644 --- a/user-management/src/Stickerlandia.UserManagement.Aspire/AppBuilderExtensions.cs +++ b/user-management/src/Stickerlandia.UserManagement.Aspire/AppBuilderExtensions.cs @@ -3,6 +3,7 @@ // Copyright 2025 Datadog, Inc. using System.Text.Json; +using Aspire.Hosting.AWS.Lambda; using Aspire.Hosting.Azure; using Azure.Messaging.ServiceBus; using Confluent.Kafka; @@ -48,8 +49,9 @@ public static InfrastructureResources WithAgnosticServices( .AddPostgres("database") .WithLifetime(ContainerLifetime.Persistent) .AddDatabase("users"); - - var migrationService = builder.AddProject("migration-service") + + var migrationService = builder + .AddProject("migration-service") .WithEnvironment("ConnectionStrings__database", agnosticDb) .WithEnvironment("ConnectionStrings__messaging", kafka) .WithEnvironment("DRIVING", builder.Configuration["DRIVING"]) @@ -60,7 +62,7 @@ public static InfrastructureResources WithAgnosticServices( return new InfrastructureResources(agnosticDb, kafka, migrationService); } - + public static IDistributedApplicationBuilder CreateKafkaTopicsOnReady( this IDistributedApplicationBuilder builder, IResourceBuilder kafka) @@ -161,8 +163,9 @@ public static InfrastructureResources WithAzureNativeServices(this IDistributedA .AddPostgres("database") .WithLifetime(ContainerLifetime.Persistent) .AddDatabase("users"); - - var migrationService = builder.AddProject("migration-service") + + var migrationService = builder + .AddProject("migration-service") .WithEnvironment("ConnectionStrings__database", azurePostgresDb) .WithEnvironment("ConnectionStrings__messaging", serviceBus) .WithEnvironment("DRIVING", builder.Configuration["DRIVING"]) @@ -223,7 +226,7 @@ public static IDistributedApplicationBuilder WithAzureFunctions( { var storage = builder.AddAzureStorage("storage") .RunAsEmulator(); - + var functions = builder.AddAzureFunctionsProject("worker") .WithHostStorage(storage) .WithEnvironment("ConnectionStrings__messaging", resources.MessagingResource) @@ -238,12 +241,23 @@ public static IDistributedApplicationBuilder WithAzureFunctions( return builder; } +#pragma warning disable CA2252 + public static IDistributedApplicationBuilder WithAwsLambda( this IDistributedApplicationBuilder builder, - IResourceBuilder databaseResource, - IResourceBuilder messagingResource) + InfrastructureResources resources) { - //TODO: Implement AWS Lambda support + var apiLambdaFunction = builder.AddAWSLambdaFunction("UsersApi", + "Stickerlandia.UserManagement.Api") + .WithEnvironment("ConnectionStrings__messaging", resources.MessagingResource) + .WithEnvironment("ConnectionStrings__database", resources.DatabaseResource) + .WithEnvironment("DRIVING", builder.Configuration["DRIVING"]) + .WithEnvironment("DRIVEN", builder.Configuration["DRIVEN"]); + + builder.AddAWSAPIGatewayEmulator("api", APIGatewayType.Rest) + .WithReference(apiLambdaFunction, Method.Any, "{proxy+}") + .WithHttpsEndpoint(51660); + ; return builder; } diff --git a/user-management/src/Stickerlandia.UserManagement.Aspire/Program.cs b/user-management/src/Stickerlandia.UserManagement.Aspire/Program.cs index 18863d52..74a41ef6 100644 --- a/user-management/src/Stickerlandia.UserManagement.Aspire/Program.cs +++ b/user-management/src/Stickerlandia.UserManagement.Aspire/Program.cs @@ -23,6 +23,7 @@ resources = builder.WithAgnosticServices(); break; case DrivenAdapters.AWS: + resources = builder.WithAgnosticServices(); break; } @@ -37,7 +38,7 @@ .WithBackgroundWorker(resources); break; case DrivingAdapter.AWS: - builder.WithContainerizedApi(resources); + builder.WithAwsLambda(resources); break; } diff --git a/user-management/src/Stickerlandia.UserManagement.Aspire/Stickerlandia.UserManagement.Aspire.csproj b/user-management/src/Stickerlandia.UserManagement.Aspire/Stickerlandia.UserManagement.Aspire.csproj index 6d677e25..3784cc72 100644 --- a/user-management/src/Stickerlandia.UserManagement.Aspire/Stickerlandia.UserManagement.Aspire.csproj +++ b/user-management/src/Stickerlandia.UserManagement.Aspire/Stickerlandia.UserManagement.Aspire.csproj @@ -25,6 +25,7 @@ + diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/AuthServiceExtensions.cs b/user-management/src/Stickerlandia.UserManagement.Auth/AuthServiceExtensions.cs index 54819b85..61128d93 100644 --- a/user-management/src/Stickerlandia.UserManagement.Auth/AuthServiceExtensions.cs +++ b/user-management/src/Stickerlandia.UserManagement.Auth/AuthServiceExtensions.cs @@ -3,8 +3,6 @@ // Copyright 2025 Datadog, Inc. using System.Globalization; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenIddict.Abstractions; using OpenIddict.Server; diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbApplicationStore.cs b/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbApplicationStore.cs deleted file mode 100644 index f0646f59..00000000 --- a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbApplicationStore.cs +++ /dev/null @@ -1,437 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2025 Datadog, Inc. - -using System.Collections.Immutable; -using System.Globalization; -using System.Text.Json; -using Amazon.DynamoDBv2.DataModel; -using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; - -namespace Stickerlandia.UserManagement.Auth.DynamoDB; - -/// -/// DynamoDB implementation of IOpenIddictApplicationStore -/// -public class DynamoDbApplicationStore : IOpenIddictApplicationStore -{ - private readonly DynamoDBContext _context; - - public DynamoDbApplicationStore(DynamoDBContext context) - { - _context = context; - } - - public ValueTask CountAsync(CancellationToken cancellationToken) - { - // Note: This is not efficient for DynamoDB, but required by the interface - return new ValueTask(0L); - } - - public ValueTask CountAsync(Func, IQueryable> query, CancellationToken cancellationToken) - { - return new ValueTask(0L); - } - - public async ValueTask CreateAsync(OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) - { - var application = new DynamoDbApplication - { - Id = Guid.NewGuid().ToString(), - ClientId = descriptor.ClientId, - ClientSecret = descriptor.ClientSecret, - ConsentType = descriptor.ConsentType, - DisplayName = descriptor.DisplayName, - ConcurrencyToken = Guid.NewGuid().ToString() - }; - - if (descriptor.Permissions.Count > 0) - { - application.Permissions = JsonSerializer.Serialize(descriptor.Permissions.ToArray()); - } - - await _context.SaveAsync(application, cancellationToken); - return application; - } - - public async ValueTask CreateAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(application.Id)) - { - application.Id = Guid.NewGuid().ToString(); - } - - if (string.IsNullOrEmpty(application.ConcurrencyToken)) - { - application.ConcurrencyToken = Guid.NewGuid().ToString(); - } - - await _context.SaveAsync(application, cancellationToken); - } - - public async ValueTask DeleteAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - await _context.DeleteAsync(application, cancellationToken); - } - - public async ValueTask FindByClientIdAsync(string identifier, CancellationToken cancellationToken) - { - var allApplications = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - return allApplications.FirstOrDefault(a => a.ClientId == identifier); - } - - public async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) - { - return await _context.LoadAsync(identifier, cancellationToken); - } - - public async IAsyncEnumerable FindByPostLogoutRedirectUriAsync(string uri, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - var allApplications = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var matchingApps = allApplications.Where(a => - !string.IsNullOrEmpty(a.PostLogoutRedirectUris) && - a.PostLogoutRedirectUris.Contains(uri)); - - foreach (var app in matchingApps) - { - yield return app; - } - } - - public async IAsyncEnumerable FindByRedirectUriAsync(string uri, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - var allApplications = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var matchingApps = allApplications.Where(a => - !string.IsNullOrEmpty(a.RedirectUris) && - a.RedirectUris.Contains(uri)); - - foreach (var app in matchingApps) - { - yield return app; - } - } - - public ValueTask GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) - { - throw new NotSupportedException("LINQ queries are not supported with DynamoDB store"); - } - - public ValueTask GetApplicationTypeAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - return new ValueTask(application.Type); - } - - public ValueTask GetClientIdAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - return new ValueTask(application.ClientId); - } - - public ValueTask GetClientSecretAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - return new ValueTask(application.ClientSecret); - } - - public ValueTask GetClientTypeAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - return new ValueTask(application.Type); - } - - public ValueTask GetConsentTypeAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - return new ValueTask(application.ConsentType); - } - - public ValueTask GetDisplayNameAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - return new ValueTask(application.DisplayName); - } - - public ValueTask> GetDisplayNamesAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(application.DisplayNames)) - { - return new ValueTask>(ImmutableDictionary.Empty); - } - - try - { - var names = JsonSerializer.Deserialize>(application.DisplayNames); - if (names != null) - { - var cultureNames = ImmutableDictionary.CreateBuilder(); - foreach (var kvp in names) - { - if (CultureInfo.GetCultureInfo(kvp.Key) is var culture) - { - cultureNames[culture] = kvp.Value; - } - } - return new ValueTask>(cultureNames.ToImmutable()); - } - } - catch (JsonException) - { - // Ignore deserialization errors - } - catch (CultureNotFoundException) - { - // Ignore invalid culture codes - } - - return new ValueTask>(ImmutableDictionary.Empty); - } - - public ValueTask GetIdAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - return new ValueTask(application.Id); - } - - public ValueTask GetJsonWebKeySetAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - // DynamoDB implementation doesn't store JsonWebKeySet directly - return new ValueTask(default(JsonWebKeySet)); - } - - public ValueTask> GetPermissionsAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(application.Permissions)) - { - return new ValueTask>(ImmutableArray.Empty); - } - - try - { - var permissions = JsonSerializer.Deserialize(application.Permissions); - return new ValueTask>(permissions?.ToImmutableArray() ?? ImmutableArray.Empty); - } - catch (JsonException) - { - return new ValueTask>(ImmutableArray.Empty); - } - } - - public ValueTask> GetPostLogoutRedirectUrisAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(application.PostLogoutRedirectUris)) - { - return new ValueTask>(ImmutableArray.Empty); - } - - try - { - var uris = JsonSerializer.Deserialize(application.PostLogoutRedirectUris); - return new ValueTask>(uris?.ToImmutableArray() ?? ImmutableArray.Empty); - } - catch (JsonException) - { - return new ValueTask>(ImmutableArray.Empty); - } - } - - public ValueTask> GetPropertiesAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(application.Properties)) - { - return new ValueTask>(ImmutableDictionary.Empty); - } - - try - { - var properties = JsonSerializer.Deserialize>(application.Properties); - return new ValueTask>( - properties?.ToImmutableDictionary() ?? ImmutableDictionary.Empty); - } - catch (JsonException) - { - return new ValueTask>(ImmutableDictionary.Empty); - } - } - - public ValueTask> GetRedirectUrisAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(application.RedirectUris)) - { - return new ValueTask>(ImmutableArray.Empty); - } - - try - { - var uris = JsonSerializer.Deserialize(application.RedirectUris); - return new ValueTask>(uris?.ToImmutableArray() ?? ImmutableArray.Empty); - } - catch (JsonException) - { - return new ValueTask>(ImmutableArray.Empty); - } - } - - public ValueTask> GetRequirementsAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(application.Requirements)) - { - return new ValueTask>(ImmutableArray.Empty); - } - - try - { - var requirements = JsonSerializer.Deserialize(application.Requirements); - return new ValueTask>(requirements?.ToImmutableArray() ?? ImmutableArray.Empty); - } - catch (JsonException) - { - return new ValueTask>(ImmutableArray.Empty); - } - } - - public ValueTask> GetSettingsAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(application.Properties)) - { - return new ValueTask>(ImmutableDictionary.Empty); - } - - try - { - var settings = JsonSerializer.Deserialize>(application.Properties); - return new ValueTask>( - settings?.ToImmutableDictionary() ?? ImmutableDictionary.Empty); - } - catch (JsonException) - { - return new ValueTask>(ImmutableDictionary.Empty); - } - } - - public ValueTask InstantiateAsync(CancellationToken cancellationToken) - { - return new ValueTask(new DynamoDbApplication()); - } - - public async IAsyncEnumerable ListAsync(int? count, int? offset, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - var allApplications = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var applications = allApplications.AsEnumerable(); - - if (offset.HasValue) - { - applications = applications.Skip(offset.Value); - } - - if (count.HasValue) - { - applications = applications.Take(count.Value); - } - - foreach (var application in applications) - { - yield return application; - } - } - - public async IAsyncEnumerable ListAsync(Func, TState, IQueryable> query, TState state, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - throw new NotSupportedException("LINQ queries are not supported with DynamoDB store"); - yield break; // Required for async enumerable - } - - public async ValueTask UpdateAsync(DynamoDbApplication application, CancellationToken cancellationToken) - { - application.ConcurrencyToken = Guid.NewGuid().ToString(); - await _context.SaveAsync(application, cancellationToken); - } - - public ValueTask SetApplicationTypeAsync(DynamoDbApplication application, string? type, CancellationToken cancellationToken) - { - application.Type = type; - return ValueTask.CompletedTask; - } - - public ValueTask SetClientIdAsync(DynamoDbApplication application, string? identifier, CancellationToken cancellationToken) - { - application.ClientId = identifier; - return ValueTask.CompletedTask; - } - - public ValueTask SetClientSecretAsync(DynamoDbApplication application, string? secret, CancellationToken cancellationToken) - { - application.ClientSecret = secret; - return ValueTask.CompletedTask; - } - - public ValueTask SetClientTypeAsync(DynamoDbApplication application, string? type, CancellationToken cancellationToken) - { - application.Type = type; - return ValueTask.CompletedTask; - } - - public ValueTask SetConsentTypeAsync(DynamoDbApplication application, string? type, CancellationToken cancellationToken) - { - application.ConsentType = type; - return ValueTask.CompletedTask; - } - - public ValueTask SetDisplayNameAsync(DynamoDbApplication application, string? name, CancellationToken cancellationToken) - { - application.DisplayName = name; - return ValueTask.CompletedTask; - } - - public ValueTask SetDisplayNamesAsync(DynamoDbApplication application, ImmutableDictionary names, CancellationToken cancellationToken) - { - var nameDict = names.ToDictionary(kvp => kvp.Key.Name, kvp => kvp.Value); - application.DisplayNames = JsonSerializer.Serialize(nameDict); - return ValueTask.CompletedTask; - } - - public ValueTask SetJsonWebKeySetAsync(DynamoDbApplication application, JsonWebKeySet? jwks, CancellationToken cancellationToken) - { - // DynamoDB implementation doesn't store JsonWebKeySet directly - // Could be serialized to Properties if needed - return ValueTask.CompletedTask; - } - - public ValueTask SetPermissionsAsync(DynamoDbApplication application, ImmutableArray permissions, CancellationToken cancellationToken) - { - application.Permissions = JsonSerializer.Serialize(permissions.ToArray()); - return ValueTask.CompletedTask; - } - - public ValueTask SetPostLogoutRedirectUrisAsync(DynamoDbApplication application, ImmutableArray uris, CancellationToken cancellationToken) - { - application.PostLogoutRedirectUris = JsonSerializer.Serialize(uris.ToArray()); - return ValueTask.CompletedTask; - } - - public ValueTask SetPropertiesAsync(DynamoDbApplication application, ImmutableDictionary properties, CancellationToken cancellationToken) - { - application.Properties = JsonSerializer.Serialize(properties); - return ValueTask.CompletedTask; - } - - public ValueTask SetRedirectUrisAsync(DynamoDbApplication application, ImmutableArray uris, CancellationToken cancellationToken) - { - application.RedirectUris = JsonSerializer.Serialize(uris.ToArray()); - return ValueTask.CompletedTask; - } - - public ValueTask SetRequirementsAsync(DynamoDbApplication application, ImmutableArray requirements, CancellationToken cancellationToken) - { - application.Requirements = JsonSerializer.Serialize(requirements.ToArray()); - return ValueTask.CompletedTask; - } - - public ValueTask SetSettingsAsync(DynamoDbApplication application, ImmutableDictionary settings, CancellationToken cancellationToken) - { - application.Properties = JsonSerializer.Serialize(settings); - return ValueTask.CompletedTask; - } -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbModels.cs b/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbModels.cs deleted file mode 100644 index 1c403d10..00000000 --- a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbModels.cs +++ /dev/null @@ -1,304 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2025 Datadog, Inc. - -using Amazon.DynamoDBv2.DataModel; -using Microsoft.AspNetCore.Identity; - -namespace Stickerlandia.UserManagement.Auth.DynamoDB; - -/// -/// DynamoDB representation of IdentityUser -/// -[DynamoDBTable("Users")] -public class DynamoDbUser -{ - [DynamoDBHashKey] - public string Id { get; set; } = string.Empty; - - public string? UserName { get; set; } - public string? NormalizedUserName { get; set; } - public string? Email { get; set; } - public string? NormalizedEmail { get; set; } - public bool EmailConfirmed { get; set; } - public string? PasswordHash { get; set; } - public string? SecurityStamp { get; set; } - public string? ConcurrencyStamp { get; set; } - public string? PhoneNumber { get; set; } - public bool PhoneNumberConfirmed { get; set; } - public bool TwoFactorEnabled { get; set; } - public DateTimeOffset? LockoutEnd { get; set; } - public bool LockoutEnabled { get; set; } - public int AccessFailedCount { get; set; } - - public IdentityUser ToIdentityUser() - { - return new IdentityUser - { - Id = Id, - UserName = UserName, - NormalizedUserName = NormalizedUserName, - Email = Email, - NormalizedEmail = NormalizedEmail, - EmailConfirmed = EmailConfirmed, - PasswordHash = PasswordHash, - SecurityStamp = SecurityStamp, - ConcurrencyStamp = ConcurrencyStamp, - PhoneNumber = PhoneNumber, - PhoneNumberConfirmed = PhoneNumberConfirmed, - TwoFactorEnabled = TwoFactorEnabled, - LockoutEnd = LockoutEnd, - LockoutEnabled = LockoutEnabled, - AccessFailedCount = AccessFailedCount - }; - } - - public static DynamoDbUser FromIdentityUser(IdentityUser user) - { - return new DynamoDbUser - { - Id = user.Id, - UserName = user.UserName, - NormalizedUserName = user.NormalizedUserName, - Email = user.Email, - NormalizedEmail = user.NormalizedEmail, - EmailConfirmed = user.EmailConfirmed, - PasswordHash = user.PasswordHash, - SecurityStamp = user.SecurityStamp, - ConcurrencyStamp = user.ConcurrencyStamp, - PhoneNumber = user.PhoneNumber, - PhoneNumberConfirmed = user.PhoneNumberConfirmed, - TwoFactorEnabled = user.TwoFactorEnabled, - LockoutEnd = user.LockoutEnd, - LockoutEnabled = user.LockoutEnabled, - AccessFailedCount = user.AccessFailedCount - }; - } -} - -/// -/// DynamoDB representation of IdentityRole -/// -[DynamoDBTable("Roles")] -public class DynamoDbRole -{ - [DynamoDBHashKey] - public string Id { get; set; } = string.Empty; - - public string? Name { get; set; } - public string? NormalizedName { get; set; } - public string? ConcurrencyStamp { get; set; } - - public IdentityRole ToIdentityRole() - { - return new IdentityRole - { - Id = Id, - Name = Name, - NormalizedName = NormalizedName, - ConcurrencyStamp = ConcurrencyStamp - }; - } - - public static DynamoDbRole FromIdentityRole(IdentityRole role) - { - return new DynamoDbRole - { - Id = role.Id, - Name = role.Name, - NormalizedName = role.NormalizedName, - ConcurrencyStamp = role.ConcurrencyStamp - }; - } -} - -/// -/// DynamoDB representation of IdentityUserClaim -/// -[DynamoDBTable("UserClaims")] -public class DynamoDbUserClaim -{ - [DynamoDBHashKey] - public string UserId { get; set; } = string.Empty; - - [DynamoDBRangeKey] - public string ClaimKey { get; set; } = string.Empty; // Combination of ClaimType and ClaimValue for uniqueness - - public int Id { get; set; } - public string? ClaimType { get; set; } - public string? ClaimValue { get; set; } - - public IdentityUserClaim ToIdentityUserClaim() - { - return new IdentityUserClaim - { - Id = Id, - UserId = UserId, - ClaimType = ClaimType, - ClaimValue = ClaimValue - }; - } - - public static DynamoDbUserClaim FromIdentityUserClaim(IdentityUserClaim claim) - { - return new DynamoDbUserClaim - { - Id = claim.Id, - UserId = claim.UserId, - ClaimType = claim.ClaimType, - ClaimValue = claim.ClaimValue, - ClaimKey = $"{claim.ClaimType}#{claim.ClaimValue}#{claim.Id}" - }; - } -} - -/// -/// DynamoDB representation of IdentityUserLogin -/// -[DynamoDBTable("UserLogins")] -public class DynamoDbUserLogin -{ - [DynamoDBHashKey] - public string UserId { get; set; } = string.Empty; - - [DynamoDBRangeKey] - public string LoginKey { get; set; } = string.Empty; // LoginProvider#ProviderKey - - public string LoginProvider { get; set; } = string.Empty; - public string ProviderKey { get; set; } = string.Empty; - public string? ProviderDisplayName { get; set; } - - public IdentityUserLogin ToIdentityUserLogin() - { - return new IdentityUserLogin - { - UserId = UserId, - LoginProvider = LoginProvider, - ProviderKey = ProviderKey, - ProviderDisplayName = ProviderDisplayName - }; - } - - public static DynamoDbUserLogin FromIdentityUserLogin(IdentityUserLogin login) - { - return new DynamoDbUserLogin - { - UserId = login.UserId, - LoginProvider = login.LoginProvider, - ProviderKey = login.ProviderKey, - ProviderDisplayName = login.ProviderDisplayName, - LoginKey = $"{login.LoginProvider}#{login.ProviderKey}" - }; - } -} - -/// -/// DynamoDB representation of IdentityUserToken -/// -[DynamoDBTable("UserTokens")] -public class DynamoDbUserToken -{ - [DynamoDBHashKey] - public string UserId { get; set; } = string.Empty; - - [DynamoDBRangeKey] - public string TokenKey { get; set; } = string.Empty; // LoginProvider#Name - - public string LoginProvider { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public string? Value { get; set; } - - public IdentityUserToken ToIdentityUserToken() - { - return new IdentityUserToken - { - UserId = UserId, - LoginProvider = LoginProvider, - Name = Name, - Value = Value - }; - } - - public static DynamoDbUserToken FromIdentityUserToken(IdentityUserToken token) - { - return new DynamoDbUserToken - { - UserId = token.UserId, - LoginProvider = token.LoginProvider, - Name = token.Name, - Value = token.Value, - TokenKey = $"{token.LoginProvider}#{token.Name}" - }; - } -} - -/// -/// DynamoDB representation of IdentityUserRole -/// -[DynamoDBTable("UserRoles")] -public class DynamoDbUserRole -{ - [DynamoDBHashKey] - public string UserId { get; set; } = string.Empty; - - [DynamoDBRangeKey] - public string RoleId { get; set; } = string.Empty; - - public IdentityUserRole ToIdentityUserRole() - { - return new IdentityUserRole - { - UserId = UserId, - RoleId = RoleId - }; - } - - public static DynamoDbUserRole FromIdentityUserRole(IdentityUserRole userRole) - { - return new DynamoDbUserRole - { - UserId = userRole.UserId, - RoleId = userRole.RoleId - }; - } -} - -/// -/// DynamoDB representation of IdentityRoleClaim -/// -[DynamoDBTable("RoleClaims")] -public class DynamoDbRoleClaim -{ - [DynamoDBHashKey] - public string RoleId { get; set; } = string.Empty; - - [DynamoDBRangeKey] - public string ClaimKey { get; set; } = string.Empty; // Combination of ClaimType and ClaimValue for uniqueness - - public int Id { get; set; } - public string? ClaimType { get; set; } - public string? ClaimValue { get; set; } - - public IdentityRoleClaim ToIdentityRoleClaim() - { - return new IdentityRoleClaim - { - Id = Id, - RoleId = RoleId, - ClaimType = ClaimType, - ClaimValue = ClaimValue - }; - } - - public static DynamoDbRoleClaim FromIdentityRoleClaim(IdentityRoleClaim claim) - { - return new DynamoDbRoleClaim - { - Id = claim.Id, - RoleId = claim.RoleId, - ClaimType = claim.ClaimType, - ClaimValue = claim.ClaimValue, - ClaimKey = $"{claim.ClaimType}#{claim.ClaimValue}#{claim.Id}" - }; - } -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbRoleStore.cs b/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbRoleStore.cs deleted file mode 100644 index 821771a5..00000000 --- a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbRoleStore.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2025 Datadog, Inc. - -using System.Security.Claims; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.DataModel; -using Microsoft.AspNetCore.Identity; - -namespace Stickerlandia.UserManagement.Auth.DynamoDB; - -/// -/// DynamoDB implementation of IRoleStore for ASP.NET Core Identity -/// -public class DynamoDbRoleStore(DynamoDBContext context, IAmazonDynamoDB dynamoDb) : - IRoleStore, - IRoleClaimStore, - IDisposable -{ - private readonly IAmazonDynamoDB _dynamoDb = dynamoDb; - - public void Dispose() - { - context?.Dispose(); - } - - #region IRoleStore - - public async Task CreateAsync(IdentityRole role, CancellationToken cancellationToken) - { - var dynamoDbRole = DynamoDbRole.FromIdentityRole(role); - await context.SaveAsync(dynamoDbRole, cancellationToken); - return IdentityResult.Success; - } - - public async Task DeleteAsync(IdentityRole role, CancellationToken cancellationToken) - { - await context.DeleteAsync(role.Id, cancellationToken); - return IdentityResult.Success; - } - - public async Task FindByIdAsync(string roleId, CancellationToken cancellationToken) - { - var dynamoDbRole = await context.LoadAsync(roleId, cancellationToken); - return dynamoDbRole?.ToIdentityRole(); - } - - public async Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) - { - // Use scan to find role by normalized name - not optimal but functional - var allRoles = await context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var role = allRoles.FirstOrDefault(r => r.NormalizedName == normalizedRoleName); - return role?.ToIdentityRole(); - } - - public Task GetNormalizedRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) - { - return Task.FromResult(role.NormalizedName); - } - - public Task GetRoleIdAsync(IdentityRole role, CancellationToken cancellationToken) - { - return Task.FromResult(role.Id); - } - - public Task GetRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) - { - return Task.FromResult(role.Name); - } - - public Task SetNormalizedRoleNameAsync(IdentityRole role, string? normalizedName, CancellationToken cancellationToken) - { - role.NormalizedName = normalizedName; - return Task.CompletedTask; - } - - public Task SetRoleNameAsync(IdentityRole role, string? roleName, CancellationToken cancellationToken) - { - role.Name = roleName; - return Task.CompletedTask; - } - - public async Task UpdateAsync(IdentityRole role, CancellationToken cancellationToken) - { - var dynamoDbRole = DynamoDbRole.FromIdentityRole(role); - await context.SaveAsync(dynamoDbRole, cancellationToken); - return IdentityResult.Success; - } - - #endregion - - #region IRoleClaimStore - - public async Task AddClaimAsync(IdentityRole role, Claim claim, CancellationToken cancellationToken = default) - { - var roleClaim = new IdentityRoleClaim - { - RoleId = role.Id, - ClaimType = claim.Type, - ClaimValue = claim.Value - }; - - var dynamoDbRoleClaim = DynamoDbRoleClaim.FromIdentityRoleClaim(roleClaim); - await context.SaveAsync(dynamoDbRoleClaim, cancellationToken); - } - - public async Task> GetClaimsAsync(IdentityRole role, CancellationToken cancellationToken = default) - { - var roleClaims = await context.QueryAsync(role.Id) - .GetRemainingAsync(cancellationToken); - - return roleClaims.Select(c => new Claim(c.ClaimType!, c.ClaimValue!)).ToList(); - } - - public async Task RemoveClaimAsync(IdentityRole role, Claim claim, CancellationToken cancellationToken = default) - { - var existingClaims = await context.QueryAsync(role.Id) - .GetRemainingAsync(cancellationToken); - - var claimsToRemove = existingClaims.Where(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value); - - foreach (var claimToRemove in claimsToRemove) - { - await context.DeleteAsync(claimToRemove, cancellationToken); - } - } - - #endregion -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbScopeStore.cs b/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbScopeStore.cs deleted file mode 100644 index db901d76..00000000 --- a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbScopeStore.cs +++ /dev/null @@ -1,330 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2025 Datadog, Inc. - -using System.Collections.Immutable; -using System.Globalization; -using System.Text.Json; -using Amazon.DynamoDBv2.DataModel; -using OpenIddict.Abstractions; - -namespace Stickerlandia.UserManagement.Auth.DynamoDB; - -/// -/// DynamoDB implementation of IOpenIddictScopeStore -/// -public class DynamoDbScopeStore : IOpenIddictScopeStore -{ - private readonly DynamoDBContext _context; - - public DynamoDbScopeStore(DynamoDBContext context) - { - _context = context; - } - - public ValueTask CountAsync(CancellationToken cancellationToken) - { - return new ValueTask(0L); - } - - public ValueTask CountAsync(Func, IQueryable> query, CancellationToken cancellationToken) - { - return new ValueTask(0L); - } - - public async ValueTask CreateAsync(OpenIddictScopeDescriptor descriptor, CancellationToken cancellationToken) - { - var scope = new DynamoDbScope - { - Id = Guid.NewGuid().ToString(), - Name = descriptor.Name, - DisplayName = descriptor.DisplayName, - Description = descriptor.Description, - ConcurrencyToken = Guid.NewGuid().ToString() - }; - - if (descriptor.Resources.Count > 0) - { - scope.Resources = JsonSerializer.Serialize(descriptor.Resources.ToArray()); - } - - await _context.SaveAsync(scope, cancellationToken); - return scope; - } - - public async ValueTask CreateAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(scope.Id)) - { - scope.Id = Guid.NewGuid().ToString(); - } - - if (string.IsNullOrEmpty(scope.ConcurrencyToken)) - { - scope.ConcurrencyToken = Guid.NewGuid().ToString(); - } - - await _context.SaveAsync(scope, cancellationToken); - } - - public async ValueTask DeleteAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - await _context.DeleteAsync(scope, cancellationToken); - } - - public async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) - { - return await _context.LoadAsync(identifier, cancellationToken); - } - - public async ValueTask FindByNameAsync(string name, CancellationToken cancellationToken) - { - var allScopes = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - return allScopes.FirstOrDefault(s => s.Name == name); - } - - public async IAsyncEnumerable FindByNamesAsync(ImmutableArray names, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - var allScopes = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var matchingScopes = allScopes.Where(s => names.Contains(s.Name)); - foreach (var scope in matchingScopes) - { - yield return scope; - } - } - - public async IAsyncEnumerable FindByResourceAsync(string resource, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - var allScopes = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var matchingScopes = allScopes.Where(s => - !string.IsNullOrEmpty(s.Resources) && - s.Resources.Contains(resource)); - - foreach (var scope in matchingScopes) - { - yield return scope; - } - } - - public ValueTask GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) - { - throw new NotSupportedException("LINQ queries are not supported with DynamoDB store"); - } - - public ValueTask GetDescriptionAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - return new ValueTask(scope.Description); - } - - public ValueTask> GetDescriptionsAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(scope.Descriptions)) - { - return new ValueTask>(ImmutableDictionary.Empty); - } - - try - { - var descriptions = JsonSerializer.Deserialize>(scope.Descriptions); - if (descriptions != null) - { - var cultureDescriptions = ImmutableDictionary.CreateBuilder(); - foreach (var kvp in descriptions) - { - if (CultureInfo.GetCultureInfo(kvp.Key) is var culture) - { - cultureDescriptions[culture] = kvp.Value; - } - } - return new ValueTask>(cultureDescriptions.ToImmutable()); - } - } - catch (JsonException) - { - // Ignore deserialization errors - } - catch (CultureNotFoundException) - { - // Ignore invalid culture codes - } - - return new ValueTask>(ImmutableDictionary.Empty); - } - - public ValueTask GetDisplayNameAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - return new ValueTask(scope.DisplayName); - } - - public ValueTask> GetDisplayNamesAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(scope.DisplayNames)) - { - return new ValueTask>(ImmutableDictionary.Empty); - } - - try - { - var displayNames = JsonSerializer.Deserialize>(scope.DisplayNames); - if (displayNames != null) - { - var cultureDisplayNames = ImmutableDictionary.CreateBuilder(); - foreach (var kvp in displayNames) - { - if (CultureInfo.GetCultureInfo(kvp.Key) is var culture) - { - cultureDisplayNames[culture] = kvp.Value; - } - } - return new ValueTask>(cultureDisplayNames.ToImmutable()); - } - } - catch (JsonException) - { - // Ignore deserialization errors - } - catch (CultureNotFoundException) - { - // Ignore invalid culture codes - } - - return new ValueTask>(ImmutableDictionary.Empty); - } - - public ValueTask GetIdAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - return new ValueTask(scope.Id); - } - - public ValueTask GetNameAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - return new ValueTask(scope.Name); - } - - public ValueTask> GetPropertiesAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(scope.Properties)) - { - return new ValueTask>(ImmutableDictionary.Empty); - } - - try - { - var properties = JsonSerializer.Deserialize>(scope.Properties); - return new ValueTask>( - properties?.ToImmutableDictionary() ?? ImmutableDictionary.Empty); - } - catch (JsonException) - { - return new ValueTask>(ImmutableDictionary.Empty); - } - } - - public ValueTask> GetResourcesAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(scope.Resources)) - { - return new ValueTask>(ImmutableArray.Empty); - } - - try - { - var resources = JsonSerializer.Deserialize(scope.Resources); - return new ValueTask>(resources?.ToImmutableArray() ?? ImmutableArray.Empty); - } - catch (JsonException) - { - return new ValueTask>(ImmutableArray.Empty); - } - } - - public ValueTask InstantiateAsync(CancellationToken cancellationToken) - { - return new ValueTask(new DynamoDbScope()); - } - - public async IAsyncEnumerable ListAsync(int? count, int? offset, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - var allScopes = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var scopes = allScopes.AsEnumerable(); - - if (offset.HasValue) - { - scopes = scopes.Skip(offset.Value); - } - - if (count.HasValue) - { - scopes = scopes.Take(count.Value); - } - - foreach (var scope in scopes) - { - yield return scope; - } - } - - public async IAsyncEnumerable ListAsync(Func, TState, IQueryable> query, TState state, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - throw new NotSupportedException("LINQ queries are not supported with DynamoDB store"); - yield break; // Required for async enumerable - } - - public async ValueTask UpdateAsync(DynamoDbScope scope, CancellationToken cancellationToken) - { - scope.ConcurrencyToken = Guid.NewGuid().ToString(); - await _context.SaveAsync(scope, cancellationToken); - } - - public ValueTask SetDescriptionAsync(DynamoDbScope scope, string? description, CancellationToken cancellationToken) - { - scope.Description = description; - return ValueTask.CompletedTask; - } - - public ValueTask SetDescriptionsAsync(DynamoDbScope scope, ImmutableDictionary descriptions, CancellationToken cancellationToken) - { - var descriptionDict = descriptions.ToDictionary(kvp => kvp.Key.Name, kvp => kvp.Value); - scope.Descriptions = JsonSerializer.Serialize(descriptionDict); - return ValueTask.CompletedTask; - } - - public ValueTask SetDisplayNameAsync(DynamoDbScope scope, string? name, CancellationToken cancellationToken) - { - scope.DisplayName = name; - return ValueTask.CompletedTask; - } - - public ValueTask SetDisplayNamesAsync(DynamoDbScope scope, ImmutableDictionary names, CancellationToken cancellationToken) - { - var nameDict = names.ToDictionary(kvp => kvp.Key.Name, kvp => kvp.Value); - scope.DisplayNames = JsonSerializer.Serialize(nameDict); - return ValueTask.CompletedTask; - } - - public ValueTask SetNameAsync(DynamoDbScope scope, string? name, CancellationToken cancellationToken) - { - scope.Name = name; - return ValueTask.CompletedTask; - } - - public ValueTask SetPropertiesAsync(DynamoDbScope scope, ImmutableDictionary properties, CancellationToken cancellationToken) - { - scope.Properties = JsonSerializer.Serialize(properties); - return ValueTask.CompletedTask; - } - - public ValueTask SetResourcesAsync(DynamoDbScope scope, ImmutableArray resources, CancellationToken cancellationToken) - { - scope.Resources = JsonSerializer.Serialize(resources.ToArray()); - return ValueTask.CompletedTask; - } -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbUserStore.cs b/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbUserStore.cs deleted file mode 100644 index 1ce32b95..00000000 --- a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/DynamoDbUserStore.cs +++ /dev/null @@ -1,553 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2025 Datadog, Inc. - -using System.Security.Claims; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.DataModel; -using Amazon.DynamoDBv2.DocumentModel; -using Amazon.DynamoDBv2.Model; -using Microsoft.AspNetCore.Identity; - -namespace Stickerlandia.UserManagement.Auth.DynamoDB; - -/// -/// DynamoDB implementation of IUserStore for ASP.NET Core Identity -/// -public class DynamoDbUserStore : - IUserStore, - IUserEmailStore, - IUserPasswordStore, - IUserSecurityStampStore, - IUserClaimStore, - IUserLoginStore, - IUserRoleStore, - IUserLockoutStore, - IUserPhoneNumberStore, - IUserTwoFactorStore, - IUserAuthenticatorKeyStore, - IUserTwoFactorRecoveryCodeStore, - IDisposable -{ - private readonly DynamoDBContext _context; - private readonly IAmazonDynamoDB _dynamoDb; - - public DynamoDbUserStore(DynamoDBContext context, IAmazonDynamoDB dynamoDb) - { - _context = context; - _dynamoDb = dynamoDb; - } - - public void Dispose() - { - _context?.Dispose(); - } - - #region IUserStore - - public async Task CreateAsync(IdentityUser user, CancellationToken cancellationToken) - { - var dynamoDbUser = DynamoDbUser.FromIdentityUser(user); - await _context.SaveAsync(dynamoDbUser, cancellationToken); - return IdentityResult.Success; - } - - public async Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken) - { - await _context.DeleteAsync(user.Id, cancellationToken); - return IdentityResult.Success; - } - - public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) - { - var dynamoDbUser = await _context.LoadAsync(userId, cancellationToken); - return dynamoDbUser?.ToIdentityUser(); - } - - public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) - { - // Use scan to find user by normalized username - not optimal but functional - var allUsers = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var user = allUsers.FirstOrDefault(u => u.NormalizedUserName == normalizedUserName); - return user?.ToIdentityUser(); - } - - public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.NormalizedUserName); - } - - public Task GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.Id); - } - - public Task GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.UserName); - } - - public Task SetNormalizedUserNameAsync(IdentityUser user, string? normalizedName, CancellationToken cancellationToken) - { - user.NormalizedUserName = normalizedName; - return Task.CompletedTask; - } - - public Task SetUserNameAsync(IdentityUser user, string? userName, CancellationToken cancellationToken) - { - user.UserName = userName; - return Task.CompletedTask; - } - - public async Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken) - { - var dynamoDbUser = DynamoDbUser.FromIdentityUser(user); - await _context.SaveAsync(dynamoDbUser, cancellationToken); - return IdentityResult.Success; - } - - #endregion - - #region IUserEmailStore - - public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) - { - // Use scan to find user by normalized email - not optimal but functional - var allUsers = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var user = allUsers.FirstOrDefault(u => u.NormalizedEmail == normalizedEmail); - return user?.ToIdentityUser(); - } - - public Task GetEmailAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.Email); - } - - public Task GetEmailConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.EmailConfirmed); - } - - public Task GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.NormalizedEmail); - } - - public Task SetEmailAsync(IdentityUser user, string? email, CancellationToken cancellationToken) - { - user.Email = email; - return Task.CompletedTask; - } - - public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) - { - user.EmailConfirmed = confirmed; - return Task.CompletedTask; - } - - public Task SetNormalizedEmailAsync(IdentityUser user, string? normalizedEmail, CancellationToken cancellationToken) - { - user.NormalizedEmail = normalizedEmail; - return Task.CompletedTask; - } - - #endregion - - #region IUserPasswordStore - - public Task GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.PasswordHash); - } - - public Task HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(!string.IsNullOrEmpty(user.PasswordHash)); - } - - public Task SetPasswordHashAsync(IdentityUser user, string? passwordHash, CancellationToken cancellationToken) - { - user.PasswordHash = passwordHash; - return Task.CompletedTask; - } - - #endregion - - #region IUserSecurityStampStore - - public Task GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.SecurityStamp); - } - - public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken) - { - user.SecurityStamp = stamp; - return Task.CompletedTask; - } - - #endregion - - #region IUserClaimStore - - public async Task AddClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) - { - foreach (var claim in claims) - { - var userClaim = new IdentityUserClaim - { - UserId = user.Id, - ClaimType = claim.Type, - ClaimValue = claim.Value - }; - - var dynamoDbUserClaim = DynamoDbUserClaim.FromIdentityUserClaim(userClaim); - await _context.SaveAsync(dynamoDbUserClaim, cancellationToken); - } - } - - public async Task> GetClaimsAsync(IdentityUser user, CancellationToken cancellationToken) - { - var userClaims = await _context.QueryAsync(user.Id) - .GetRemainingAsync(cancellationToken); - - return userClaims.Select(c => new Claim(c.ClaimType!, c.ClaimValue!)).ToList(); - } - - public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) - { - // Scan all user claims - not optimal but functional - var allUserClaims = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var matchingClaims = allUserClaims.Where(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value); - var userIds = matchingClaims.Select(c => c.UserId).Distinct(); - - var users = new List(); - foreach (var userId in userIds) - { - var user = await FindByIdAsync(userId, cancellationToken); - if (user != null) - users.Add(user); - } - - return users; - } - - public async Task RemoveClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) - { - foreach (var claim in claims) - { - var existingClaims = await _context.QueryAsync(user.Id) - .GetRemainingAsync(cancellationToken); - - var claimsToRemove = existingClaims.Where(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value); - - foreach (var claimToRemove in claimsToRemove) - { - await _context.DeleteAsync(claimToRemove, cancellationToken); - } - } - } - - public async Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) - { - await RemoveClaimsAsync(user, new[] { claim }, cancellationToken); - await AddClaimsAsync(user, new[] { newClaim }, cancellationToken); - } - - #endregion - - #region IUserLoginStore - - public async Task AddLoginAsync(IdentityUser user, UserLoginInfo login, CancellationToken cancellationToken) - { - var userLogin = new IdentityUserLogin - { - UserId = user.Id, - LoginProvider = login.LoginProvider, - ProviderKey = login.ProviderKey, - ProviderDisplayName = login.ProviderDisplayName - }; - - var dynamoDbUserLogin = DynamoDbUserLogin.FromIdentityUserLogin(userLogin); - await _context.SaveAsync(dynamoDbUserLogin, cancellationToken); - } - - public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) - { - // Scan all user logins - not optimal but functional - var allUserLogins = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var userLogin = allUserLogins.FirstOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); - - if (userLogin != null) - { - return await FindByIdAsync(userLogin.UserId, cancellationToken); - } - - return null; - } - - public async Task> GetLoginsAsync(IdentityUser user, CancellationToken cancellationToken) - { - var userLogins = await _context.QueryAsync(user.Id) - .GetRemainingAsync(cancellationToken); - - return userLogins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)) - .ToList(); - } - - public async Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) - { - var loginKey = $"{loginProvider}#{providerKey}"; - await _context.DeleteAsync(user.Id, loginKey, cancellationToken); - } - - #endregion - - #region IUserRoleStore - - public async Task AddToRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) - { - // First find the role by name to get its ID - var roleStore = new DynamoDbRoleStore(_context, _dynamoDb); - var role = await roleStore.FindByNameAsync(roleName, cancellationToken); - - if (role != null) - { - var userRole = new IdentityUserRole - { - UserId = user.Id, - RoleId = role.Id - }; - - var dynamoDbUserRole = DynamoDbUserRole.FromIdentityUserRole(userRole); - await _context.SaveAsync(dynamoDbUserRole, cancellationToken); - } - } - - public async Task> GetRolesAsync(IdentityUser user, CancellationToken cancellationToken) - { - var userRoles = await _context.QueryAsync(user.Id) - .GetRemainingAsync(cancellationToken); - - var roleStore = new DynamoDbRoleStore(_context, _dynamoDb); - var roleNames = new List(); - - foreach (var userRole in userRoles) - { - var role = await roleStore.FindByIdAsync(userRole.RoleId, cancellationToken); - if (role?.Name != null) - { - roleNames.Add(role.Name); - } - } - - return roleNames; - } - - public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) - { - var roleStore = new DynamoDbRoleStore(_context, _dynamoDb); - var role = await roleStore.FindByNameAsync(roleName, cancellationToken); - - if (role == null) - return new List(); - - // Scan all user roles to find users in this role - var allUserRoles = await _context.ScanAsync(new List()) - .GetRemainingAsync(cancellationToken); - - var userRolesForRole = allUserRoles.Where(ur => ur.RoleId == role.Id); - - var users = new List(); - foreach (var userRole in userRolesForRole) - { - var user = await FindByIdAsync(userRole.UserId, cancellationToken); - if (user != null) - users.Add(user); - } - - return users; - } - - public async Task IsInRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) - { - var roles = await GetRolesAsync(user, cancellationToken); - return roles.Contains(roleName); - } - - public async Task RemoveFromRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) - { - var roleStore = new DynamoDbRoleStore(_context, _dynamoDb); - var role = await roleStore.FindByNameAsync(roleName, cancellationToken); - - if (role != null) - { - await _context.DeleteAsync(user.Id, role.Id, cancellationToken); - } - } - - #endregion - - #region IUserLockoutStore - - public Task GetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.AccessFailedCount); - } - - public Task GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.LockoutEnabled); - } - - public Task GetLockoutEndDateAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.LockoutEnd); - } - - public Task IncrementAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) - { - user.AccessFailedCount++; - return Task.FromResult(user.AccessFailedCount); - } - - public Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) - { - user.AccessFailedCount = 0; - return Task.CompletedTask; - } - - public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) - { - user.LockoutEnabled = enabled; - return Task.CompletedTask; - } - - public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) - { - user.LockoutEnd = lockoutEnd; - return Task.CompletedTask; - } - - #endregion - - #region IUserPhoneNumberStore - - public Task GetPhoneNumberAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.PhoneNumber); - } - - public Task GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.PhoneNumberConfirmed); - } - - public Task SetPhoneNumberAsync(IdentityUser user, string? phoneNumber, CancellationToken cancellationToken) - { - user.PhoneNumber = phoneNumber; - return Task.CompletedTask; - } - - public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) - { - user.PhoneNumberConfirmed = confirmed; - return Task.CompletedTask; - } - - #endregion - - #region IUserTwoFactorStore - - public Task GetTwoFactorEnabledAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.TwoFactorEnabled); - } - - public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) - { - user.TwoFactorEnabled = enabled; - return Task.CompletedTask; - } - - #endregion - - #region IUserAuthenticatorKeyStore - - public async Task GetAuthenticatorKeyAsync(IdentityUser user, CancellationToken cancellationToken) - { - var tokenKey = $"[AspNetUserStore]#AuthenticatorKey"; - var token = await _context.LoadAsync(user.Id, tokenKey, cancellationToken); - return token?.Value; - } - - public async Task SetAuthenticatorKeyAsync(IdentityUser user, string key, CancellationToken cancellationToken) - { - var token = new DynamoDbUserToken - { - UserId = user.Id, - LoginProvider = "[AspNetUserStore]", - Name = "AuthenticatorKey", - Value = key, - TokenKey = $"[AspNetUserStore]#AuthenticatorKey" - }; - - await _context.SaveAsync(token, cancellationToken); - } - - #endregion - - #region IUserTwoFactorRecoveryCodeStore - - public async Task CountCodesAsync(IdentityUser user, CancellationToken cancellationToken) - { - var tokenKey = $"[AspNetUserStore]#RecoveryCodes"; - var token = await _context.LoadAsync(user.Id, tokenKey, cancellationToken); - if (token?.Value != null) - { - return token.Value.Split(';').Length; - } - return 0; - } - - public async Task RedeemCodeAsync(IdentityUser user, string code, CancellationToken cancellationToken) - { - var tokenKey = $"[AspNetUserStore]#RecoveryCodes"; - var token = await _context.LoadAsync(user.Id, tokenKey, cancellationToken); - if (token?.Value != null) - { - var codes = token.Value.Split(';').ToList(); - if (codes.Remove(code)) - { - token.Value = string.Join(";", codes); - await _context.SaveAsync(token, cancellationToken); - return true; - } - } - return false; - } - - public async Task ReplaceCodesAsync(IdentityUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) - { - var token = new DynamoDbUserToken - { - UserId = user.Id, - LoginProvider = "[AspNetUserStore]", - Name = "RecoveryCodes", - Value = string.Join(";", recoveryCodes), - TokenKey = $"[AspNetUserStore]#RecoveryCodes" - }; - - await _context.SaveAsync(token, cancellationToken); - } - - #endregion -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/OpenIddictDynamoDbModels.cs b/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/OpenIddictDynamoDbModels.cs deleted file mode 100644 index 3eb15ecf..00000000 --- a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDB/OpenIddictDynamoDbModels.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2025 Datadog, Inc. - -using System.Collections.Immutable; -using System.Globalization; -using System.Text.Json; -using Amazon.DynamoDBv2.DataModel; -using OpenIddict.Abstractions; - -namespace Stickerlandia.UserManagement.Auth.DynamoDB; - -/// -/// DynamoDB representation of OpenIddict Application -/// -[DynamoDBTable("OpenIddictApplications")] -public class DynamoDbApplication -{ - [DynamoDBHashKey] - public string Id { get; set; } = string.Empty; - - public string? ClientId { get; set; } - public string? ClientSecret { get; set; } - public string? ConcurrencyToken { get; set; } - public string? ConsentType { get; set; } - public string? DisplayName { get; set; } - public string? DisplayNames { get; set; } // JSON serialized - public string? Endpoints { get; set; } // JSON serialized - public string? GrantTypes { get; set; } // JSON serialized - public string? Permissions { get; set; } // JSON serialized - public string? PostLogoutRedirectUris { get; set; } // JSON serialized - public string? Properties { get; set; } // JSON serialized - public string? RedirectUris { get; set; } // JSON serialized - public string? Requirements { get; set; } // JSON serialized - public string? ResponseTypes { get; set; } // JSON serialized - public string? Type { get; set; } -} - -/// -/// DynamoDB representation of OpenIddict Authorization -/// -[DynamoDBTable("OpenIddictAuthorizations")] -public class DynamoDbAuthorization -{ - [DynamoDBHashKey] - public string Id { get; set; } = string.Empty; - - public string? ApplicationId { get; set; } - public string? ConcurrencyToken { get; set; } - public DateTime? CreationDate { get; set; } - public string? Properties { get; set; } // JSON serialized - public string? Scopes { get; set; } // JSON serialized - public string? Status { get; set; } - public string? Subject { get; set; } - public string? Type { get; set; } -} - -/// -/// DynamoDB representation of OpenIddict Scope -/// -[DynamoDBTable("OpenIddictScopes")] -public class DynamoDbScope -{ - [DynamoDBHashKey] - public string Id { get; set; } = string.Empty; - - public string? ConcurrencyToken { get; set; } - public string? Description { get; set; } - public string? Descriptions { get; set; } // JSON serialized - public string? DisplayName { get; set; } - public string? DisplayNames { get; set; } // JSON serialized - public string? Name { get; set; } - public string? Properties { get; set; } // JSON serialized - public string? Resources { get; set; } // JSON serialized -} - -/// -/// DynamoDB representation of OpenIddict Token -/// -[DynamoDBTable("OpenIddictTokens")] -public class DynamoDbToken -{ - [DynamoDBHashKey] - public string Id { get; set; } = string.Empty; - - public string? ApplicationId { get; set; } - public string? AuthorizationId { get; set; } - public string? ConcurrencyToken { get; set; } - public DateTime? CreationDate { get; set; } - public DateTime? ExpirationDate { get; set; } - public string? Payload { get; set; } - public string? Properties { get; set; } // JSON serialized - public DateTime? RedemptionDate { get; set; } - public string? ReferenceId { get; set; } - public string? Status { get; set; } - public string? Subject { get; set; } - public string? Type { get; set; } -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDbAuthenticationWorker.cs b/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDbAuthenticationWorker.cs deleted file mode 100644 index 2a423ff1..00000000 --- a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDbAuthenticationWorker.cs +++ /dev/null @@ -1,207 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2025 Datadog, Inc. - -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using OpenIddict.Abstractions; -using Stickerlandia.UserManagement.Auth.DynamoDB; - -namespace Stickerlandia.UserManagement.Auth; - -/// -/// Background service that initializes DynamoDB tables and creates default OpenIddict applications -/// -public class DynamoDbAuthenticationWorker : IHostedService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public DynamoDbAuthenticationWorker(IServiceProvider serviceProvider, ILogger logger) - { - _serviceProvider = serviceProvider; - _logger = logger; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - using var scope = _serviceProvider.CreateScope(); - - try - { - // Initialize DynamoDB tables - await InitializeDynamoDbTablesAsync(scope, cancellationToken); - - // Create default OpenIddict applications - await CreateDefaultApplicationsAsync(scope, cancellationToken); - - _logger.LogInformation("DynamoDB authentication infrastructure initialized successfully"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to initialize DynamoDB authentication infrastructure"); - throw; - } - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - private async Task InitializeDynamoDbTablesAsync(IServiceScope scope, CancellationToken cancellationToken) - { - var dynamoDb = scope.ServiceProvider.GetRequiredService(); - - var tables = new[] - { - "Users", "Roles", "UserClaims", "UserLogins", "UserTokens", "UserRoles", "RoleClaims", - "OpenIddictApplications", "OpenIddictAuthorizations", "OpenIddictScopes", "OpenIddictTokens" - }; - - foreach (var tableName in tables) - { - try - { - await dynamoDb.DescribeTableAsync(tableName, cancellationToken); - _logger.LogDebug("Table {TableName} already exists", tableName); - } - catch (ResourceNotFoundException) - { - _logger.LogInformation("Creating DynamoDB table: {TableName}", tableName); - await CreateTableAsync(dynamoDb, tableName, cancellationToken); - } - } - } - - private async Task CreateTableAsync(IAmazonDynamoDB dynamoDb, string tableName, CancellationToken cancellationToken) - { - var request = new CreateTableRequest - { - TableName = tableName, - BillingMode = BillingMode.PAY_PER_REQUEST - }; - - // Define key schema based on table type - switch (tableName) - { - case "Users": - case "Roles": - case "OpenIddictApplications": - case "OpenIddictAuthorizations": - case "OpenIddictScopes": - case "OpenIddictTokens": - // Hash key only tables - request.KeySchema = new List - { - new() { AttributeName = "Id", KeyType = KeyType.HASH } - }; - request.AttributeDefinitions = new List - { - new() { AttributeName = "Id", AttributeType = ScalarAttributeType.S } - }; - break; - - case "UserClaims": - case "UserLogins": - case "UserTokens": - case "UserRoles": - case "RoleClaims": - // Hash and range key tables - var hashKey = tableName.StartsWith("User") ? "UserId" : "RoleId"; - var rangeKey = tableName switch - { - "UserClaims" => "ClaimKey", - "UserLogins" => "LoginKey", - "UserTokens" => "TokenKey", - "UserRoles" => "RoleId", - "RoleClaims" => "ClaimKey", - _ => "Id" - }; - - request.KeySchema = new List - { - new() { AttributeName = hashKey, KeyType = KeyType.HASH }, - new() { AttributeName = rangeKey, KeyType = KeyType.RANGE } - }; - request.AttributeDefinitions = new List - { - new() { AttributeName = hashKey, AttributeType = ScalarAttributeType.S }, - new() { AttributeName = rangeKey, AttributeType = ScalarAttributeType.S } - }; - break; - } - - await dynamoDb.CreateTableAsync(request, cancellationToken); - - // Wait for table to become active - var tableActive = false; - var attempts = 0; - const int maxAttempts = 30; - - while (!tableActive && attempts < maxAttempts) - { - await Task.Delay(2000, cancellationToken); - attempts++; - - var describeResponse = await dynamoDb.DescribeTableAsync(tableName, cancellationToken); - tableActive = describeResponse.Table.TableStatus == TableStatus.ACTIVE; - - if (!tableActive) - { - _logger.LogDebug("Waiting for table {TableName} to become active... (attempt {Attempt}/{MaxAttempts})", - tableName, attempts, maxAttempts); - } - } - - if (tableActive) - { - _logger.LogInformation("Successfully created table: {TableName}", tableName); - } - else - { - _logger.LogWarning("Table {TableName} creation may not be complete after {MaxAttempts} attempts", - tableName, maxAttempts); - } - } - - private async Task CreateDefaultApplicationsAsync(IServiceScope scope, CancellationToken cancellationToken) - { - var manager = scope.ServiceProvider.GetRequiredService(); - - // Create user authentication application - if (await manager.FindByClientIdAsync("user-authentication", cancellationToken) is null) - { - _logger.LogInformation("Creating default application: user-authentication"); - await manager.CreateAsync(new OpenIddictApplicationDescriptor - { - ClientId = "user-authentication", - ClientSecret = "388D45FA-B36B-4988-BA59-B187D329C207", - Permissions = - { - OpenIddictConstants.Permissions.Endpoints.Token, - OpenIddictConstants.Permissions.GrantTypes.Password, - } - }, cancellationToken); - } - - // Create internal service application - if (await manager.FindByClientIdAsync("internal-service", cancellationToken) is null) - { - _logger.LogInformation("Creating default application: internal-service"); - await manager.CreateAsync(new OpenIddictApplicationDescriptor - { - ClientId = "internal-service", - ClientSecret = "8E1167EF-5C44-4209-A803-3A109155FDD3", - Permissions = - { - OpenIddictConstants.Permissions.Endpoints.Token, - OpenIddictConstants.Permissions.GrantTypes.Password, - OpenIddictConstants.Permissions.GrantTypes.ClientCredentials, - } - }, cancellationToken); - } - - _logger.LogInformation("Default OpenIddict applications configured successfully"); - } -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDbIdentityAuthService.cs b/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDbIdentityAuthService.cs deleted file mode 100644 index 6b682bf5..00000000 --- a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDbIdentityAuthService.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2025 Datadog, Inc. - -using System.Collections.Immutable; -using System.Security.Claims; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.DataModel; -using Amazon.DynamoDBv2.Model; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; -using OpenIddict.Server.AspNetCore; -using Stickerlandia.UserManagement.Auth.DynamoDB; -using Stickerlandia.UserManagement.Core; - -namespace Stickerlandia.UserManagement.Auth; - -/// -/// DynamoDB-specific implementation of IAuthService using AWS DynamoDB for persistence -/// -public class DynamoDbIdentityAuthService( - UserManager userManager, - IOpenIddictApplicationManager applicationManager, - ILogger logger, - IOpenIddictScopeManager scopeManager) - : IAuthService -{ - public async Task VerifyClient(string clientId) - { - var application = await applicationManager.FindByClientIdAsync(clientId); - if (application == null) - { - logger.LogError("Client application {ClientId} not found", clientId); - return null; - } - - // Create a new ClaimsIdentity containing the claims that - // will be used to create an id_token, a token or a code. - var identity = new ClaimsIdentity( - authenticationType: TokenValidationParameters.DefaultAuthenticationType, - nameType: OpenIddictConstants.Claims.Name, - roleType: OpenIddictConstants.Claims.Role); - - // Use the client_id as the subject identifier. - identity.SetClaim(OpenIddictConstants.Claims.Subject, await applicationManager.GetClientIdAsync(application)); - identity.SetClaim(OpenIddictConstants.Claims.Name, await applicationManager.GetDisplayNameAsync(application)); - - identity.SetDestinations(static claim => claim.Type switch - { - // Allow the "name" claim to be stored in both the access and identity tokens - // when the "profile" scope was granted (by calling principal.SetScopes(...)). - OpenIddictConstants.Claims.Name when claim.Subject.HasScope(OpenIddictConstants.Permissions.Scopes.Profile) - => new[] { OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken }, - - // Otherwise, only store the claim in the access tokens. - _ => new[] { OpenIddictConstants.Destinations.AccessToken } - }); - - return identity; - } - - public async Task VerifyPassword(string username, string password, ImmutableArray scopes) - { - var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Role); - IdentityUser? user = await userManager.FindByEmailAsync(username); - AuthenticationProperties properties = new(); - - if (user == null) - return null; - - // Validate the username/password parameters and ensure the account is not locked out. - var result = await userManager.CheckPasswordAsync(user, password); - if (!result) return null; - - // The user is now validated, so reset lockout counts, if necessary - if (userManager.SupportsUserLockout) await userManager.ResetAccessFailedCountAsync(user); - - //// Getting scopes from user parameters (TokenViewModel) and adding in Identity - identity.SetScopes(scopes); - - var resources = await scopeManager.ListResourcesAsync(scopes).ToListAsync(); - identity.SetResources(resources); - - var applicationUser = await userManager.FindByEmailAsync(user.Email); - - // Add Custom claims - identity.AddClaim(new Claim(OpenIddictConstants.Claims.Subject, applicationUser.Id.ToString())); - identity.AddClaim(new Claim(OpenIddictConstants.Claims.Audience, "Resource")); - identity.AddClaim(new Claim(OpenIddictConstants.Claims.Email, user.Email)); - identity.AddClaim(new Claim(OpenIddictConstants.Claims.Username, applicationUser.Id.ToString())); - - // Setting destinations of claims i.e. identity token or access token - identity.SetDestinations(GetDestinations); - - return identity; - } - - public Task EnsureStoreCreatedAsync() - { - return Task.CompletedTask; - } - - private static IEnumerable GetDestinations(Claim claim) - { - // Note: by default, claims are NOT automatically included in the access and identity tokens. - // To allow OpenIddict to serialize them, you must attach them a destination, that specifies - // whether they should be included in access tokens, in identity tokens or in both. - - return claim.Type switch - { - OpenIddictConstants.Claims.Name or - OpenIddictConstants.Claims.Subject - => new[] { OpenIddictConstants.Destinations.AccessToken }, - - OpenIddictConstants.Claims.Email or - OpenIddictConstants.Claims.Role or - OpenIddictConstants.Claims.PreferredUsername - => new[] { OpenIddictConstants.Destinations.IdentityToken }, - - _ => Array.Empty() - }; - } -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDbServiceExtensions.cs b/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDbServiceExtensions.cs deleted file mode 100644 index 8184c420..00000000 --- a/user-management/src/Stickerlandia.UserManagement.Auth/DynamoDbServiceExtensions.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2025 Datadog, Inc. - -using System.Globalization; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.DataModel; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using OpenIddict.Abstractions; -using OpenIddict.Server; -using OpenIddict.Validation.AspNetCore; -using Stickerlandia.UserManagement.Auth.DynamoDB; -using Stickerlandia.UserManagement.Core; - -namespace Stickerlandia.UserManagement.Auth; - -/// -/// Service extensions for configuring DynamoDB as the authentication persistence layer -/// -public static class DynamoDbServiceExtensions -{ - /// - /// Configures authentication services using DynamoDB as the persistence provider - /// - /// The service collection - /// Application configuration - /// The service collection for method chaining - public static IServiceCollection AddDynamoDbAuthServices(this IServiceCollection services, IConfiguration configuration) - { - // Configure AWS DynamoDB - services.AddAWSService(); - services.AddSingleton(); - - // Configure DynamoDB-specific Identity stores - services.AddTransient, DynamoDbUserStore>(); - services.AddTransient, DynamoDbRoleStore>(); - - // Configure DynamoDB-specific OpenIddict stores - services.AddSingleton, DynamoDbApplicationStore>(); - services.AddSingleton, DynamoDbScopeStore>(); - - // Configure Identity with DynamoDB stores - services.AddIdentityCore(options => - { - options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name; - options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject; - options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role; - options.ClaimsIdentity.EmailClaimType = OpenIddictConstants.Claims.Email; - - // Configure password requirements - options.Password.RequireDigit = true; - options.Password.RequireLowercase = true; - options.Password.RequireNonAlphanumeric = true; - options.Password.RequireUppercase = true; - options.Password.RequiredLength = 8; - options.Password.RequiredUniqueChars = 1; - - // Configure lockout settings - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); - options.Lockout.MaxFailedAccessAttempts = 5; - options.Lockout.AllowedForNewUsers = true; - - // Configure user settings - options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; - options.User.RequireUniqueEmail = true; - }) - .AddDefaultTokenProviders(); - - services.AddOpenIddict() - .AddCore(options => - { - // Configure OpenIddict to use our custom DynamoDB stores - options.SetDefaultApplicationEntity() - .SetDefaultScopeEntity(); - }) - .AddServer(options => - { - // Enable the token endpoint. - options.SetAuthorizationEndpointUris("/authorize") - .SetTokenEndpointUris("/api/users/v1/login") - .SetIntrospectionEndpointUris("/introspection") - .SetUserInfoEndpointUris("/userinfo") - .SetEndSessionEndpointUris("/logout"); - - // Enable the client credentials flow. - options.AllowClientCredentialsFlow() - .AllowAuthorizationCodeFlow() - .AllowPasswordFlow() - .AllowImplicitFlow() - .AllowHybridFlow() - .AllowRefreshTokenFlow(); - - // Expose all the supported claims in the discovery document. - options.RegisterClaims("email", "issuer", "preferred_username", "profile", "updated_at"); - - // Expose all the supported scopes in the discovery document. - options.RegisterScopes("email", "profile"); - - // Register the signing and encryption credentials. - options.AddEphemeralEncryptionKey() - .AddEphemeralSigningKey(); - - // Register the ASP.NET Core host and configure the ASP.NET Core options. - options.UseAspNetCore() - .EnableAuthorizationEndpointPassthrough() - .EnableTokenEndpointPassthrough() - .EnableEndSessionEndpointPassthrough(); - - options.AddEventHandler(options => options.UseInlineHandler(context => - { - if (context.Principal.HasScope(OpenIddictConstants.Permissions.Scopes.Profile)) - { - context.Profile = context.Principal.GetClaim(OpenIddictConstants.Claims.Profile); - context.PreferredUsername = context.Principal.GetClaim(OpenIddictConstants.Claims.PreferredUsername); - context.Claims[OpenIddictConstants.Claims.UpdatedAt] = long.Parse( - context.Principal.GetClaim(OpenIddictConstants.Claims.UpdatedAt)!, - NumberStyles.Number, CultureInfo.InvariantCulture); - } - - if (context.Principal.HasScope(OpenIddictConstants.Scopes.Email)) - { - context.Email = context.Principal.GetClaim(OpenIddictConstants.Claims.Email); - context.EmailVerified = false; - } - - return default; - })); - }) - .AddValidation(options => - { - // Import the configuration from the local OpenIddict server instance. - options.UseLocalServer(); - - // Register the ASP.NET Core host. - options.UseAspNetCore(); - - // Enable authorization entry validation, which is required to be able - // to reject access tokens retrieved from a revoked authorization code. - options.EnableAuthorizationEntryValidation(); - }); - - // Register the DynamoDB-specific authentication service - services.AddScoped(); - - // Add the hosted service for DynamoDB initialization - services.AddHostedService(); - - return services; - } -} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Auth/Stickerlandia.UserManagement.Auth.csproj b/user-management/src/Stickerlandia.UserManagement.Auth/Stickerlandia.UserManagement.Auth.csproj index d34de9a6..a055610c 100644 --- a/user-management/src/Stickerlandia.UserManagement.Auth/Stickerlandia.UserManagement.Auth.csproj +++ b/user-management/src/Stickerlandia.UserManagement.Auth/Stickerlandia.UserManagement.Auth.csproj @@ -22,7 +22,7 @@ - + diff --git a/user-management/src/Stickerlandia.UserManagement.Azure/ServiceExtensions.cs b/user-management/src/Stickerlandia.UserManagement.Azure/ServiceExtensions.cs index b0ca281c..a78d487c 100644 --- a/user-management/src/Stickerlandia.UserManagement.Azure/ServiceExtensions.cs +++ b/user-management/src/Stickerlandia.UserManagement.Azure/ServiceExtensions.cs @@ -2,21 +2,11 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -using System.Globalization; using Azure.Messaging.ServiceBus; -using Microsoft.AspNetCore.Identity; -using Microsoft.Azure.Cosmos; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using OpenIddict.Abstractions; -using OpenIddict.Server; -using OpenIddict.Validation.AspNetCore; using Stickerlandia.UserManagement.Agnostic; -using Stickerlandia.UserManagement.Auth; using Stickerlandia.UserManagement.Core; -using Stickerlandia.UserManagement.Core.Outbox; namespace Stickerlandia.UserManagement.Azure; diff --git a/user-management/src/Stickerlandia.UserManagement.Core/IAuthService.cs b/user-management/src/Stickerlandia.UserManagement.Core/IAuthService.cs index 1215dff6..dd9a1e69 100644 --- a/user-management/src/Stickerlandia.UserManagement.Core/IAuthService.cs +++ b/user-management/src/Stickerlandia.UserManagement.Core/IAuthService.cs @@ -4,7 +4,6 @@ using System.Collections.Immutable; using System.Security.Claims; -using Microsoft.AspNetCore.Http; namespace Stickerlandia.UserManagement.Core; diff --git a/user-management/src/Stickerlandia.UserManagement.Core/Login/LoginCommandHandler.cs b/user-management/src/Stickerlandia.UserManagement.Core/Login/LoginCommandHandler.cs index 14e78955..a1b2deed 100644 --- a/user-management/src/Stickerlandia.UserManagement.Core/Login/LoginCommandHandler.cs +++ b/user-management/src/Stickerlandia.UserManagement.Core/Login/LoginCommandHandler.cs @@ -2,7 +2,6 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -using System.Collections.Immutable; using System.Diagnostics; namespace Stickerlandia.UserManagement.Core.Login; diff --git a/user-management/src/Stickerlandia.UserManagement.Core/Login/LoginResponse.cs b/user-management/src/Stickerlandia.UserManagement.Core/Login/LoginResponse.cs index 1cef0a4c..bb8e272a 100644 --- a/user-management/src/Stickerlandia.UserManagement.Core/Login/LoginResponse.cs +++ b/user-management/src/Stickerlandia.UserManagement.Core/Login/LoginResponse.cs @@ -10,5 +10,5 @@ namespace Stickerlandia.UserManagement.Core.Login; public class LoginResponse { [JsonPropertyName("identity")] - public ClaimsIdentity Identity { get; set; } + public ClaimsIdentity? Identity { get; set; } } diff --git a/user-management/src/Stickerlandia.UserManagement.Core/RegisterUser/RegisterCommandHandler.cs b/user-management/src/Stickerlandia.UserManagement.Core/RegisterUser/RegisterCommandHandler.cs index b9a83e80..702b3d64 100644 --- a/user-management/src/Stickerlandia.UserManagement.Core/RegisterUser/RegisterCommandHandler.cs +++ b/user-management/src/Stickerlandia.UserManagement.Core/RegisterUser/RegisterCommandHandler.cs @@ -3,31 +3,35 @@ // Copyright 2025 Datadog, Inc. using System.Diagnostics; -using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; namespace Stickerlandia.UserManagement.Core.RegisterUser; -public class RegisterCommandHandler(IUsers users) +public class RegisterCommandHandler(IUsers users, ILogger logger) { public async Task Handle(RegisterUserCommand command, AccountType accountType) { try { - if (command == null || !command.IsValid()) throw new ArgumentException("Invalid LoginCommand"); + if (command == null || !command.IsValid()) + { + logger.LogWarning("Input command is null or invalid"); + throw new ArgumentException("Invalid LoginCommand"); + }; // Check if email exists before creating account var emailExists = await users.DoesEmailExistAsync(command.EmailAddress); if (emailExists) throw new UserExistsException(); // Use async version for better performance - var userAccount = await UserAccount.Register(command.EmailAddress, command.Password, command.FirstName, + var userAccount = UserAccount.Register(command.EmailAddress, command.Password, command.FirstName, command.LastName, accountType); await users.Add(userAccount); return new RegisterResponse { - AccountId = userAccount.Id.Value + AccountId = userAccount.Id!.Value }; } catch (UserExistsException ex) diff --git a/user-management/src/Stickerlandia.UserManagement.Core/RegisterUser/UserRegisteredEvent.cs b/user-management/src/Stickerlandia.UserManagement.Core/RegisterUser/UserRegisteredEvent.cs index 3b76ea14..2be1d5b1 100644 --- a/user-management/src/Stickerlandia.UserManagement.Core/RegisterUser/UserRegisteredEvent.cs +++ b/user-management/src/Stickerlandia.UserManagement.Core/RegisterUser/UserRegisteredEvent.cs @@ -13,7 +13,7 @@ public UserRegisteredEvent(){} public UserRegisteredEvent(UserAccount account) { - AccountId = account.Id.Value; + AccountId = account.Id!.Value; } [JsonPropertyName("eventName")] diff --git a/user-management/src/Stickerlandia.UserManagement.Core/Stickerlandia.UserManagement.Core.csproj b/user-management/src/Stickerlandia.UserManagement.Core/Stickerlandia.UserManagement.Core.csproj index 23559853..ae9eb8ce 100644 --- a/user-management/src/Stickerlandia.UserManagement.Core/Stickerlandia.UserManagement.Core.csproj +++ b/user-management/src/Stickerlandia.UserManagement.Core/Stickerlandia.UserManagement.Core.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/user-management/src/Stickerlandia.UserManagement.Core/UpdateUserDetails/UserDetailsUpdatedEvent.cs b/user-management/src/Stickerlandia.UserManagement.Core/UpdateUserDetails/UserDetailsUpdatedEvent.cs index a9bd1005..79268af5 100644 --- a/user-management/src/Stickerlandia.UserManagement.Core/UpdateUserDetails/UserDetailsUpdatedEvent.cs +++ b/user-management/src/Stickerlandia.UserManagement.Core/UpdateUserDetails/UserDetailsUpdatedEvent.cs @@ -13,7 +13,7 @@ public UserDetailsUpdatedEvent(){} public UserDetailsUpdatedEvent(UserAccount account) { - AccountId = account.Id.Value; + AccountId = account.Id!.Value; } [JsonPropertyName("eventName")] diff --git a/user-management/src/Stickerlandia.UserManagement.Core/UserAccount.cs b/user-management/src/Stickerlandia.UserManagement.Core/UserAccount.cs index 2b727a61..d0e348a6 100644 --- a/user-management/src/Stickerlandia.UserManagement.Core/UserAccount.cs +++ b/user-management/src/Stickerlandia.UserManagement.Core/UserAccount.cs @@ -56,7 +56,7 @@ public UserAccount() } // Async version for better performance in web contexts - public static async Task Register(string emailAddress, string password, string firstName, + public static UserAccount Register(string emailAddress, string password, string firstName, string lastName, AccountType accountType) { if (!IsValidEmail(emailAddress)) throw new InvalidUserException("Invalid email address"); @@ -101,7 +101,7 @@ public static UserAccount From( }; } - public AccountId Id { get; private set; } + public AccountId? Id { get; private set; } public string EmailAddress { get; private set; } = string.Empty; diff --git a/user-management/src/Stickerlandia.UserManagement.Core/UserAccountDTO.cs b/user-management/src/Stickerlandia.UserManagement.Core/UserAccountDTO.cs index 675fc7da..dfacde7a 100644 --- a/user-management/src/Stickerlandia.UserManagement.Core/UserAccountDTO.cs +++ b/user-management/src/Stickerlandia.UserManagement.Core/UserAccountDTO.cs @@ -10,7 +10,7 @@ public record UserAccountDTO { public UserAccountDTO(UserAccount userAccount) { - AccountId = userAccount.Id.Value; + AccountId = userAccount.Id!.Value; EmailAddress = userAccount.EmailAddress; FirstName = userAccount.FirstName; LastName = userAccount.LastName; diff --git a/user-management/src/Stickerlandia.UserManagement.FunctionApp/Middlewares/ExceptionHandlingMiddleware.cs b/user-management/src/Stickerlandia.UserManagement.FunctionApp/Middlewares/ExceptionHandlingMiddleware.cs index b4590ea3..4ac4ba5d 100644 --- a/user-management/src/Stickerlandia.UserManagement.FunctionApp/Middlewares/ExceptionHandlingMiddleware.cs +++ b/user-management/src/Stickerlandia.UserManagement.FunctionApp/Middlewares/ExceptionHandlingMiddleware.cs @@ -44,10 +44,6 @@ await ProcessError(ex, context, } catch (Exception ex) { - var activeSpan = Tracer.Instance.ActiveScope?.Span; - activeSpan?.SetException(ex); - activeSpan?.SetTag("error", "true"); - activeSpan?.SetTag("error.message", ex.Message); logger.LogError(ex, $"Unknown error: {ex.Message} : {ex.StackTrace}"); await ProcessError(ex, context, new ApiResponse(false, "Error", "Unknown error")); } @@ -55,6 +51,11 @@ await ProcessError(ex, context, private async Task ProcessError(Exception ex, FunctionContext context, ApiResponse apiResponse) { + var activeSpan = Tracer.Instance.ActiveScope?.Span; + activeSpan?.SetException(ex); + activeSpan?.SetTag("error", "true"); + activeSpan?.SetTag("error.message", ex.Message); + logger.LogError(ex, "Error processing invocation"); var httpReqData = await context.GetHttpRequestDataAsync(); diff --git a/user-management/src/Stickerlandia.UserManagement.FunctionApp/Program.cs b/user-management/src/Stickerlandia.UserManagement.FunctionApp/Program.cs index c8e29248..336bff2d 100644 --- a/user-management/src/Stickerlandia.UserManagement.FunctionApp/Program.cs +++ b/user-management/src/Stickerlandia.UserManagement.FunctionApp/Program.cs @@ -45,9 +45,7 @@ var appLogger = new SerilogLoggerFactory(logger) .CreateLogger(); - appLogger.LogInformation("Application started"); -; var app = hostBuilder.Build(); diff --git a/user-management/src/Stickerlandia.UserManagement.FunctionApp/Stickerlandia.UserManagement.FunctionApp.csproj b/user-management/src/Stickerlandia.UserManagement.FunctionApp/Stickerlandia.UserManagement.FunctionApp.csproj index 9ad1c3af..b55c6382 100644 --- a/user-management/src/Stickerlandia.UserManagement.FunctionApp/Stickerlandia.UserManagement.FunctionApp.csproj +++ b/user-management/src/Stickerlandia.UserManagement.FunctionApp/Stickerlandia.UserManagement.FunctionApp.csproj @@ -16,14 +16,14 @@ - + - + diff --git a/user-management/src/Stickerlandia.UserManagement.Lambda/AssemblyInfo.cs b/user-management/src/Stickerlandia.UserManagement.Lambda/AssemblyInfo.cs new file mode 100644 index 00000000..30e8574c --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.Lambda/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Amazon.Lambda.Core; + +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Lambda/Kafka.cs b/user-management/src/Stickerlandia.UserManagement.Lambda/Kafka.cs new file mode 100644 index 00000000..11982820 --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.Lambda/Kafka.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using Amazon.Lambda.Annotations; +using Amazon.Lambda.KafkaEvents; +using Datadog.Trace; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Stickerlandia.UserManagement.Core; +using Stickerlandia.UserManagement.Core.Outbox; +using Stickerlandia.UserManagement.Core.StickerClaimedEvent; + +namespace Stickerlandia.UserManagement.Lambda; + +public class Kafka(ILogger logger, IServiceScopeFactory serviceScopeFactory, OutboxProcessor outboxProcessor) +{ + [LambdaFunction] + public async Task StickerClaimed(KafkaEvent kafkaEvent) + { + using var processSpan = Tracer.Instance.StartActive($"process users.stickerClaimed.v1"); + + using var scope = serviceScopeFactory.CreateScope(); + var handler = scope.ServiceProvider.GetRequiredService(); + + foreach (var message in kafkaEvent.Records) + foreach (var record in message.Value) + { + var evtData = JsonSerializer.Deserialize(record.Value); + + if (evtData == null) continue; + + try + { + await handler.Handle(evtData!); + } + catch (InvalidUserException ex) + { + logger.LogWarning(ex, "User with account in this event not found"); + } + } + } +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Lambda/MigrationFunction.cs b/user-management/src/Stickerlandia.UserManagement.Lambda/MigrationFunction.cs new file mode 100644 index 00000000..c673c8b7 --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.Lambda/MigrationFunction.cs @@ -0,0 +1,67 @@ +using Amazon.Lambda.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenIddict.Abstractions; +using Stickerlandia.UserManagement.Agnostic; +using Stickerlandia.UserManagement.Core.Outbox; + +namespace Stickerlandia.UserManagement.Lambda; + +public class MigrationFunction( + ILogger logger, + IServiceScopeFactory serviceScopeFactory, + OutboxProcessor outboxProcessor) +{ + [LambdaFunction] + public async Task Migrate(object evtData) + { + using var scope = serviceScopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var manager = scope.ServiceProvider.GetRequiredService(); + + await RunMigrationAsync(dbContext, default); + await SeedDataAsync(dbContext, manager, default); + } + + private static async Task RunMigrationAsync(UserManagementDbContext dbContext, CancellationToken cancellationToken) + { + var strategy = dbContext.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + // Run migration in a transaction to avoid partial migration if it fails. + await dbContext.Database.MigrateAsync(cancellationToken); + }); + } + + private static async Task SeedDataAsync(UserManagementDbContext dbContext, IOpenIddictApplicationManager manager, + CancellationToken cancellationToken) + { + // Add seeding logic here if needed. + if (await manager.FindByClientIdAsync("user-authentication") is null) + await manager.CreateAsync(new OpenIddictApplicationDescriptor + { + ClientId = "user-authentication", + ClientSecret = "388D45FA-B36B-4988-BA59-B187D329C207", + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.Password, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken + } + }); + + if (await manager.FindByClientIdAsync("internal-service") is null) + await manager.CreateAsync(new OpenIddictApplicationDescriptor + { + ClientId = "internal-service", + ClientSecret = "8E1167EF-5C44-4209-A803-3A109155FDD3", + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.Password, + OpenIddictConstants.Permissions.GrantTypes.ClientCredentials + } + }); + } +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Lambda/OutboxFunctions.cs b/user-management/src/Stickerlandia.UserManagement.Lambda/OutboxFunctions.cs new file mode 100644 index 00000000..1671cbe0 --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.Lambda/OutboxFunctions.cs @@ -0,0 +1,17 @@ +using Amazon.Lambda.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Stickerlandia.UserManagement.Core.Outbox; + +namespace Stickerlandia.UserManagement.Lambda; + +public class OutboxFunctions(ILogger logger, IServiceScopeFactory serviceScopeFactory, OutboxProcessor outboxProcessor) +{ + [LambdaFunction] + public async Task Worker(object evtData) + { + logger.LogInformation("Running outbox timer"); + + await outboxProcessor.ProcessAsync(); + } +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Lambda/Properties/launchSettings.json b/user-management/src/Stickerlandia.UserManagement.Lambda/Properties/launchSettings.json new file mode 100644 index 00000000..457ad9f8 --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.Lambda/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Aspire_StickerClaimedProcessorFunction": { + "commandName": "Executable", + "executablePath": "dotnet", + "commandLineArgs": "exec --depsfile ./Stickerlandia.UserManagement.Lambda.deps.json --runtimeconfig ./Stickerlandia.UserManagement.Lambda.runtimeconfig.json $(HOME)/.dotnet/tools/.store/amazon.lambda.testtool/0.10.3/amazon.lambda.testtool/0.10.3/content/Amazon.Lambda.RuntimeSupport/net8.0/Amazon.Lambda.RuntimeSupport.dll Stickerlandia.UserManagement.Lambda::Stickerlandia.UserManagement.Lambda.Sqs_StickerClaimed_Generated::StickerClaimed", + "workingDirectory": "./bin/$(Configuration)/net8.0" + } + } +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Lambda/src/Stickerlandia.UserManagement.Lambda/Readme.md b/user-management/src/Stickerlandia.UserManagement.Lambda/Readme.md similarity index 100% rename from user-management/src/Stickerlandia.UserManagement.Lambda/src/Stickerlandia.UserManagement.Lambda/Readme.md rename to user-management/src/Stickerlandia.UserManagement.Lambda/Readme.md diff --git a/user-management/src/Stickerlandia.UserManagement.Lambda/Sqs.cs b/user-management/src/Stickerlandia.UserManagement.Lambda/Sqs.cs new file mode 100644 index 00000000..a6cf3374 --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.Lambda/Sqs.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using Amazon.Lambda.Annotations; +using Amazon.Lambda.SQSEvents; +using Datadog.Trace; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Stickerlandia.UserManagement.Core; +using Stickerlandia.UserManagement.Core.Outbox; +using Stickerlandia.UserManagement.Core.StickerClaimedEvent; + +namespace Stickerlandia.UserManagement.Lambda; + +public class Sqs(ILogger logger, IServiceScopeFactory serviceScopeFactory, OutboxProcessor outboxProcessor) +{ + [LambdaFunction] + public async Task StickerClaimed(SQSEvent sqsEvent) + { + using var processSpan = Tracer.Instance.StartActive($"process users.stickerClaimed.v1"); + + using var scope = serviceScopeFactory.CreateScope(); + var handler = scope.ServiceProvider.GetRequiredService(); + + var failedMessages = new List(); + + foreach (var message in sqsEvent.Records) await ProcessMessage(message, failedMessages, handler); + + return new SQSBatchResponse(failedMessages); + } + + private async Task ProcessMessage(SQSEvent.SQSMessage message, + List failedMessages, + StickerClaimedEventHandler handler) + { + var evtData = JsonSerializer.Deserialize(message.Body); + + if (evtData == null) + { + failedMessages.Add(new SQSBatchResponse.BatchItemFailure { ItemIdentifier = message.MessageId }); + return; + } + + try + { + await handler.Handle(evtData!); + } + catch (InvalidUserException ex) + { + logger.LogWarning(ex, "User with account in this event not found"); + failedMessages.Add(new SQSBatchResponse.BatchItemFailure { ItemIdentifier = message.MessageId }); + } + } +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Lambda/Startup.cs b/user-management/src/Stickerlandia.UserManagement.Lambda/Startup.cs new file mode 100644 index 00000000..7d8eb262 --- /dev/null +++ b/user-management/src/Stickerlandia.UserManagement.Lambda/Startup.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using Serilog.Extensions.Logging; +using Serilog.Formatting.Json; +using Stickerlandia.UserManagement.ServiceDefaults; + +namespace Stickerlandia.UserManagement.Lambda; + +[Amazon.Lambda.Annotations.LambdaStartup] +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + var configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + + var logger = Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + .Enrich.FromLogContext() + .WriteTo.Console(new JsonFormatter()) + .CreateLogger(); + + services.ConfigureDefaultUserManagementServices(configuration); + + var appLogger = new SerilogLoggerFactory(logger) + .CreateLogger(); + + appLogger.LogInformation("Application configured"); + } +} \ No newline at end of file diff --git a/user-management/src/Stickerlandia.UserManagement.Lambda/src/Stickerlandia.UserManagement.Lambda/Stickerlandia.UserManagement.Lambda.csproj b/user-management/src/Stickerlandia.UserManagement.Lambda/Stickerlandia.UserManagement.Lambda.csproj similarity index 65% rename from user-management/src/Stickerlandia.UserManagement.Lambda/src/Stickerlandia.UserManagement.Lambda/Stickerlandia.UserManagement.Lambda.csproj rename to user-management/src/Stickerlandia.UserManagement.Lambda/Stickerlandia.UserManagement.Lambda.csproj index cfeccdc7..9e5a8df5 100644 --- a/user-management/src/Stickerlandia.UserManagement.Lambda/src/Stickerlandia.UserManagement.Lambda/Stickerlandia.UserManagement.Lambda.csproj +++ b/user-management/src/Stickerlandia.UserManagement.Lambda/Stickerlandia.UserManagement.Lambda.csproj @@ -12,10 +12,14 @@ true - - - - + + + + + + + +