diff options
Diffstat (limited to 'server/src')
| -rw-r--r-- | server/src/Data/AppDbContext.cs | 7 | ||||
| -rw-r--r-- | server/src/Data/Database/GithubUserMapping.cs | 9 | ||||
| -rw-r--r-- | server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs | 18 | ||||
| -rw-r--r-- | server/src/IOL.GreatOffice.Api.csproj | 1 | ||||
| -rw-r--r-- | server/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs | 496 | ||||
| -rw-r--r-- | server/src/Migrations/20220819203816_RemoveGithubUsers.cs | 43 | ||||
| -rw-r--r-- | server/src/Migrations/AppDbContextModelSnapshot.cs | 39 | ||||
| -rw-r--r-- | server/src/Program.cs | 12 |
8 files changed, 540 insertions, 85 deletions
diff --git a/server/src/Data/AppDbContext.cs b/server/src/Data/AppDbContext.cs index a5ef36e..c970429 100644 --- a/server/src/Data/AppDbContext.cs +++ b/server/src/Data/AppDbContext.cs @@ -10,7 +10,6 @@ public class AppDbContext : DbContext, IDataProtectionKeyContext public DbSet<TimeLabel> TimeLabels { get; set; } public DbSet<TimeEntry> TimeEntries { get; set; } public DbSet<TimeCategory> TimeCategories { get; set; } - public DbSet<GithubUserMapping> GithubUserMappings { get; set; } public DbSet<ApiAccessToken> AccessTokens { get; set; } public DbSet<Tenant> Tenants { get; set; } public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } @@ -39,12 +38,6 @@ public class AppDbContext : DbContext, IDataProtectionKeyContext e.ToTable("time_entries"); }); - modelBuilder.Entity<GithubUserMapping>(e => { - e.HasOne(c => c.User); - e.HasKey(c => c.GithubId); - e.ToTable("github_user_mappings"); - }); - modelBuilder.Entity<ApiAccessToken>(e => { e.ToTable("api_access_tokens"); }); diff --git a/server/src/Data/Database/GithubUserMapping.cs b/server/src/Data/Database/GithubUserMapping.cs deleted file mode 100644 index dbdb2b7..0000000 --- a/server/src/Data/Database/GithubUserMapping.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace IOL.GreatOffice.Api.Data.Database; - -public class GithubUserMapping -{ - public User User { get; set; } - public string GithubId { get; set; } - public string Email { get; set; } - public string RefreshToken { get; set; } -} diff --git a/server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs b/server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs deleted file mode 100644 index 055820c..0000000 --- a/server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs +++ /dev/null @@ -1,18 +0,0 @@ -//using AspNet.Security.OAuth.GitHub; - -namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; - -public class CreateGithubSessionRoute : RouteBaseSync.WithRequest<string>.WithActionResult -{ - public CreateGithubSessionRoute(IConfiguration configuration) { } - - [AllowAnonymous] - [HttpGet("~/_/account/create-github-session")] - public override ActionResult Handle(string next) { - return BadRequest("This action is deprecated"); - // return Challenge(new AuthenticationProperties { - // RedirectUri = next - // }, - // GitHubAuthenticationDefaults.AuthenticationScheme); - } -} diff --git a/server/src/IOL.GreatOffice.Api.csproj b/server/src/IOL.GreatOffice.Api.csproj index a560f6a..0b3d37e 100644 --- a/server/src/IOL.GreatOffice.Api.csproj +++ b/server/src/IOL.GreatOffice.Api.csproj @@ -10,7 +10,6 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="AspNet.Security.OAuth.GitHub" Version="6.0.6" /> <PackageReference Include="Duende.IdentityServer" Version="6.1.2" /> <PackageReference Include="Duende.IdentityServer.EntityFramework.Storage" Version="6.1.2" /> <PackageReference Include="EFCore.NamingConventions" Version="6.0.0" /> diff --git a/server/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs b/server/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs new file mode 100644 index 0000000..33b5cfd --- /dev/null +++ b/server/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs @@ -0,0 +1,496 @@ +// <auto-generated /> +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220819203816_RemoveGithubUsers")] + partial class RemoveGithubUsers + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property<bool>("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property<bool>("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property<bool>("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime>("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_api_access_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_api_access_tokens_user_id"); + + b.ToTable("api_access_tokens", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<Guid>("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property<string>("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid?>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid?>("TimeEntryId") + .HasColumnType("uuid") + .HasColumnName("time_entry_id"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.HasIndex("TimeEntryId") + .HasDatabaseName("ix_time_labels_time_entry_id"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("FirstName") + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property<string>("LastName") + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property<string>("Xml") + .HasColumnType("text") + .HasColumnName("xml"); + + b.HasKey("Id") + .HasName("pk_data_protection_keys"); + + b.ToTable("data_protection_keys", (string)null); + }); + + modelBuilder.Entity("TenantUser", b => + { + b.Property<Guid>("TenantsId") + .HasColumnType("uuid") + .HasColumnName("tenants_id"); + + b.Property<Guid>("UsersId") + .HasColumnType("uuid") + .HasColumnName("users_id"); + + b.HasKey("TenantsId", "UsersId") + .HasName("pk_tenant_user"); + + b.HasIndex("UsersId") + .HasDatabaseName("ix_tenant_user_users_id"); + + b.ToTable("tenant_user", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_api_access_tokens_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null) + .WithMany("Labels") + .HasForeignKey("TimeEntryId") + .HasConstraintName("fk_time_labels_time_entries_time_entry_id"); + }); + + modelBuilder.Entity("TenantUser", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null) + .WithMany() + .HasForeignKey("TenantsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_user_tenants_tenants_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_user_users_users_id"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.Navigation("Labels"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220819203816_RemoveGithubUsers.cs b/server/src/Migrations/20220819203816_RemoveGithubUsers.cs new file mode 100644 index 0000000..d301f67 --- /dev/null +++ b/server/src/Migrations/20220819203816_RemoveGithubUsers.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class RemoveGithubUsers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "github_user_mappings"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "github_user_mappings", + columns: table => new + { + github_id = table.Column<string>(type: "text", nullable: false), + user_id = table.Column<Guid>(type: "uuid", nullable: true), + email = table.Column<string>(type: "text", nullable: true), + refresh_token = table.Column<string>(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_github_user_mappings", x => x.github_id); + table.ForeignKey( + name: "fk_github_user_mappings_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id"); + }); + + migrationBuilder.CreateIndex( + name: "ix_github_user_mappings_user_id", + table: "github_user_mappings", + column: "user_id"); + } + } +} diff --git a/server/src/Migrations/AppDbContextModelSnapshot.cs b/server/src/Migrations/AppDbContextModelSnapshot.cs index 128d3b6..cc4bf72 100644 --- a/server/src/Migrations/AppDbContextModelSnapshot.cs +++ b/server/src/Migrations/AppDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace IOL.GreatOffice.Api.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("ProductVersion", "6.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -98,33 +98,6 @@ namespace IOL.GreatOffice.Api.Migrations b.ToTable("forgot_password_requests", (string)null); }); - modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => - { - b.Property<string>("GithubId") - .HasColumnType("text") - .HasColumnName("github_id"); - - b.Property<string>("Email") - .HasColumnType("text") - .HasColumnName("email"); - - b.Property<string>("RefreshToken") - .HasColumnType("text") - .HasColumnName("refresh_token"); - - b.Property<Guid?>("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("GithubId") - .HasName("pk_github_user_mappings"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_github_user_mappings_user_id"); - - b.ToTable("github_user_mappings", (string)null); - }); - modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b => { b.Property<Guid>("Id") @@ -476,16 +449,6 @@ namespace IOL.GreatOffice.Api.Migrations b.Navigation("User"); }); - modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => - { - b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") - .WithMany() - .HasForeignKey("UserId") - .HasConstraintName("fk_github_user_mappings_users_user_id"); - - b.Navigation("User"); - }); - modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => { b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category") diff --git a/server/src/Program.cs b/server/src/Program.cs index b74f348..1d831e3 100644 --- a/server/src/Program.cs +++ b/server/src/Program.cs @@ -38,8 +38,6 @@ global using IOL.GreatOffice.Api.Data.Static; global using IOL.GreatOffice.Api.Services; global using IOL.GreatOffice.Api.Utilities; using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using System.Text; using IOL.GreatOffice.Api.Endpoints.V1; using IOL.GreatOffice.Api.Jobs; using Microsoft.AspNetCore.HttpOverrides; @@ -127,16 +125,6 @@ public static class Program return Task.FromResult<object>(null); }; }) - .AddGitHub(options => { - options.ClientSecret = configuration.GITHUB_CLIENT_SECRET; - options.ClientId = configuration.GITHUB_CLIENT_ID; - options.SaveTokens = true; - options.CorrelationCookie.Name = "gh_correlation"; - options.CorrelationCookie.SameSite = SameSiteMode.Lax; - options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always; - options.CorrelationCookie.HttpOnly = true; - options.Events.OnCreatingTicket = context => GithubAuthenticationHelpers.HandleGithubTicketCreation(context, builder.Configuration, configuration); - }) .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(AppConstants.BASIC_AUTH_SCHEME, default); builder.Services.AddDbContext<AppDbContext>(options => { |
