Extra profile properties and collabs are clickable
3dc82eb06bf7f56f9f4b7386854c4e71e49e5e51
25 files changed
src/GitServer/Data/Migrations/20260719132808_AddUserProfileFields.Designer.cssrc/GitServer/Data/Migrations/20260719132808_AddUserProfileFields.cssrc/GitServer/Data/Migrations/AppDbContextModelSnapshot.cssrc/GitServer/Localization/ar.jsonsrc/GitServer/Localization/de.jsonsrc/GitServer/Localization/en.jsonsrc/GitServer/Localization/es.jsonsrc/GitServer/Localization/fr.jsonsrc/GitServer/Localization/ja.jsonsrc/GitServer/Localization/nl.jsonsrc/GitServer/Localization/pt.jsonsrc/GitServer/Localization/ru.jsonsrc/GitServer/Localization/zh.jsonsrc/GitServer/Models/AppUser.cssrc/GitServer/gitserver.dbsrc/GitServer/gitserver.db-shmsrc/GitServer/gitserver.db-walsrc/GitServer/wwwroot/Auth/Login.cshtml.cssrc/GitServer/wwwroot/ExploreUsers.cshtmlsrc/GitServer/wwwroot/ExploreUsers.cshtml.cssrc/GitServer/wwwroot/Shared/_ProfileHeader.cshtmlsrc/GitServer/wwwroot/User/Profile.cshtmlsrc/GitServer/wwwroot/User/Settings.cshtmlsrc/GitServer/wwwroot/User/Settings.cshtml.cssrc/GitServer/wwwroot/css/components.css
diff --git a/src/GitServer/Data/Migrations/20260719132808_AddUserProfileFields.Designer.cs b/src/GitServer/Data/Migrations/20260719132808_AddUserProfileFields.Designer.cs
new file mode 100644
index 0000000..0b6b02f
--- /dev/null
+++ b/src/GitServer/Data/Migrations/20260719132808_AddUserProfileFields.Designer.cs
@@ -0,0 +1,533 @@
+// <auto-generated />
+using System;
+using GitServer.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace GitServer.Data.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260719132808_AddUserProfileFields")]
+ partial class AddUserProfileFields
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.10");
+
+ modelBuilder.Entity("GitServer.Models.AppUser", b =>
+ {
+ b.Property<string>("Id")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AvatarUrl")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Bio")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CompanyName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Country")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DisplayName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAdmin")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastLoginAt")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTimeOffset?>("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("PreferredLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("GitServer.Models.BlockedEmailPattern", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Pattern")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("BlockedEmailPatterns");
+ });
+
+ modelBuilder.Entity("GitServer.Models.Issue", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AuthorId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Body")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsClosed")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("RepositoryId")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Title")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("RepositoryId");
+
+ b.ToTable("Issues");
+ });
+
+ modelBuilder.Entity("GitServer.Models.IssueComment", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AuthorId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Body")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("IssueId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("IssueId");
+
+ b.ToTable("IssueComments");
+ });
+
+ modelBuilder.Entity("GitServer.Models.Repository", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DefaultBranch")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Description")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsPrivate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId", "Name")
+ .IsUnique();
+
+ b.ToTable("Repositories");
+ });
+
+ modelBuilder.Entity("GitServer.Models.RepositoryAccess", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Level")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("RepositoryId")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("UserId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("RepositoryId", "UserId")
+ .IsUnique();
+
+ b.ToTable("RepositoryAccesses");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property<string>("Id")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("RoleId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UserId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
+ {
+ b.Property<string>("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderDisplayName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UserId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
+ {
+ b.Property<string>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("RoleId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
+ {
+ b.Property<string>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("GitServer.Models.Issue", b =>
+ {
+ b.HasOne("GitServer.Models.AppUser", "Author")
+ .WithMany()
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GitServer.Models.Repository", "Repository")
+ .WithMany("Issues")
+ .HasForeignKey("RepositoryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Author");
+
+ b.Navigation("Repository");
+ });
+
+ modelBuilder.Entity("GitServer.Models.IssueComment", b =>
+ {
+ b.HasOne("GitServer.Models.AppUser", "Author")
+ .WithMany()
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GitServer.Models.Issue", "Issue")
+ .WithMany("Comments")
+ .HasForeignKey("IssueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Author");
+
+ b.Navigation("Issue");
+ });
+
+ modelBuilder.Entity("GitServer.Models.Repository", b =>
+ {
+ b.HasOne("GitServer.Models.AppUser", "Owner")
+ .WithMany("Repositories")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("GitServer.Models.RepositoryAccess", b =>
+ {
+ b.HasOne("GitServer.Models.Repository", "Repository")
+ .WithMany("Accesses")
+ .HasForeignKey("RepositoryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GitServer.Models.AppUser", "User")
+ .WithMany("RepositoryAccesses")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Repository");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
+ {
+ b.HasOne("GitServer.Models.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
+ {
+ b.HasOne("GitServer.Models.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GitServer.Models.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
+ {
+ b.HasOne("GitServer.Models.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GitServer.Models.AppUser", b =>
+ {
+ b.Navigation("Repositories");
+
+ b.Navigation("RepositoryAccesses");
+ });
+
+ modelBuilder.Entity("GitServer.Models.Issue", b =>
+ {
+ b.Navigation("Comments");
+ });
+
+ modelBuilder.Entity("GitServer.Models.Repository", b =>
+ {
+ b.Navigation("Accesses");
+
+ b.Navigation("Issues");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/GitServer/Data/Migrations/20260719132808_AddUserProfileFields.cs b/src/GitServer/Data/Migrations/20260719132808_AddUserProfileFields.cs
new file mode 100644
index 0000000..594e045
--- /dev/null
+++ b/src/GitServer/Data/Migrations/20260719132808_AddUserProfileFields.cs
@@ -0,0 +1,48 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace GitServer.Data.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddUserProfileFields : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "CompanyName",
+ table: "AspNetUsers",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn<string>(
+ name: "Country",
+ table: "AspNetUsers",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn<string>(
+ name: "PreferredLanguage",
+ table: "AspNetUsers",
+ type: "TEXT",
+ nullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "CompanyName",
+ table: "AspNetUsers");
+
+ migrationBuilder.DropColumn(
+ name: "Country",
+ table: "AspNetUsers");
+
+ migrationBuilder.DropColumn(
+ name: "PreferredLanguage",
+ table: "AspNetUsers");
+ }
+ }
+}
diff --git a/src/GitServer/Data/Migrations/AppDbContextModelSnapshot.cs b/src/GitServer/Data/Migrations/AppDbContextModelSnapshot.cs
index 0518331..6305948 100644
--- a/src/GitServer/Data/Migrations/AppDbContextModelSnapshot.cs
+++ b/src/GitServer/Data/Migrations/AppDbContextModelSnapshot.cs
@@ -31,10 +31,16 @@ namespace GitServer.Data.Migrations
b.Property<string>("Bio")
.HasColumnType("TEXT");
+ b.Property<string>("CompanyName")
+ .HasColumnType("TEXT");
+
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
+ b.Property<string>("Country")
+ .HasColumnType("TEXT");
+
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
@@ -78,6 +84,9 @@ namespace GitServer.Data.Migrations
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
+ b.Property<string>("PreferredLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
diff --git a/src/GitServer/Localization/ar.json b/src/GitServer/Localization/ar.json
index 8291c26..4bc454e 100644
--- a/src/GitServer/Localization/ar.json
+++ b/src/GitServer/Localization/ar.json
@@ -156,6 +156,11 @@
"profile_section": "الملف الشخصي",
"bio_label": "نبذة شخصية",
"avatar_url_label": "رابط الصورة الرمزية",
+ "country_label": "الدولة",
+ "company_name_label": "اسم الشركة",
+ "preferred_language_label": "اللغة المفضلة",
+ "preferred_language_default": "افتراضي (الإنجليزية)",
+ "preferred_language_hint": "يتم اختيارها تلقائيًا عند تسجيل الدخول.",
"settings_save_profile": "حفظ الملف الشخصي",
"settings_change_password": "تغيير كلمة المرور",
"settings_set_password": "تعيين كلمة المرور",
@@ -188,6 +193,7 @@
"success_settings_saved": "تم حفظ الإعدادات.",
"success_profile_saved": "تم حفظ الملف الشخصي.",
"success_password_changed": "تم تغيير كلمة المرور.",
+ "error_new_password_required": "الرجاء إدخال كلمة مرور جديدة.",
"admin_now_is_admin": "{0} أصبح مديراً الآن.",
"admin_now_not_admin": "{0} لم يعد مديراً.",
"admin_now_enabled": "{0} مفعّل الآن.",
diff --git a/src/GitServer/Localization/de.json b/src/GitServer/Localization/de.json
index a3715f7..d65916d 100644
--- a/src/GitServer/Localization/de.json
+++ b/src/GitServer/Localization/de.json
@@ -156,6 +156,11 @@
"profile_section": "Profil",
"bio_label": "Biografie",
"avatar_url_label": "Avatar-URL",
+ "country_label": "Land",
+ "company_name_label": "Firma",
+ "preferred_language_label": "Bevorzugte Sprache",
+ "preferred_language_default": "Standard (Englisch)",
+ "preferred_language_hint": "Wird beim Anmelden automatisch ausgewählt.",
"settings_save_profile": "Profil speichern",
"settings_change_password": "Passwort ändern",
"settings_set_password": "Passwort festlegen",
@@ -188,6 +193,7 @@
"success_settings_saved": "Einstellungen gespeichert.",
"success_profile_saved": "Profil gespeichert.",
"success_password_changed": "Passwort geändert.",
+ "error_new_password_required": "Bitte gib ein neues Passwort ein.",
"admin_now_is_admin": "{0} ist jetzt Admin.",
"admin_now_not_admin": "{0} ist kein Admin mehr.",
"admin_now_enabled": "{0} ist jetzt aktiviert.",
diff --git a/src/GitServer/Localization/en.json b/src/GitServer/Localization/en.json
index b75c1f9..36f9fdc 100644
--- a/src/GitServer/Localization/en.json
+++ b/src/GitServer/Localization/en.json
@@ -157,6 +157,11 @@
"profile_section": "Profile",
"bio_label": "Bio",
"avatar_url_label": "Avatar URL",
+ "country_label": "Country",
+ "company_name_label": "Company",
+ "preferred_language_label": "Preferred language",
+ "preferred_language_default": "Default (English)",
+ "preferred_language_hint": "Automatically selected when you sign in.",
"settings_save_profile": "Save profile",
"settings_change_password": "Change password",
"settings_set_password": "Set password",
@@ -189,6 +194,7 @@
"success_settings_saved": "Settings saved.",
"success_profile_saved": "Profile saved.",
"success_password_changed": "Password changed.",
+ "error_new_password_required": "Please enter a new password.",
"admin_now_is_admin": "{0} is now admin.",
"admin_now_not_admin": "{0} is no longer admin.",
"admin_now_enabled": "{0} is now enabled.",
diff --git a/src/GitServer/Localization/es.json b/src/GitServer/Localization/es.json
index 7f8ba24..81fc2e6 100644
--- a/src/GitServer/Localization/es.json
+++ b/src/GitServer/Localization/es.json
@@ -156,6 +156,11 @@
"profile_section": "Perfil",
"bio_label": "Biografía",
"avatar_url_label": "URL del avatar",
+ "country_label": "País",
+ "company_name_label": "Empresa",
+ "preferred_language_label": "Idioma preferido",
+ "preferred_language_default": "Predeterminado (inglés)",
+ "preferred_language_hint": "Se selecciona automáticamente al iniciar sesión.",
"settings_save_profile": "Guardar perfil",
"settings_change_password": "Cambiar contraseña",
"settings_set_password": "Establecer contraseña",
@@ -188,6 +193,7 @@
"success_settings_saved": "Configuración guardada.",
"success_profile_saved": "Perfil guardado.",
"success_password_changed": "Contraseña cambiada.",
+ "error_new_password_required": "Introduce una nueva contraseña.",
"admin_now_is_admin": "{0} ahora es admin.",
"admin_now_not_admin": "{0} ya no es admin.",
"admin_now_enabled": "{0} está ahora habilitado.",
diff --git a/src/GitServer/Localization/fr.json b/src/GitServer/Localization/fr.json
index 8313def..f126228 100644
--- a/src/GitServer/Localization/fr.json
+++ b/src/GitServer/Localization/fr.json
@@ -156,6 +156,11 @@
"profile_section": "Profil",
"bio_label": "Biographie",
"avatar_url_label": "URL de l'avatar",
+ "country_label": "Pays",
+ "company_name_label": "Société",
+ "preferred_language_label": "Langue préférée",
+ "preferred_language_default": "Par défaut (anglais)",
+ "preferred_language_hint": "Sélectionnée automatiquement lors de la connexion.",
"settings_save_profile": "Enregistrer le profil",
"settings_change_password": "Changer le mot de passe",
"settings_set_password": "Définir le mot de passe",
@@ -188,6 +193,7 @@
"success_settings_saved": "Paramètres enregistrés.",
"success_profile_saved": "Profil enregistré.",
"success_password_changed": "Mot de passe modifié.",
+ "error_new_password_required": "Veuillez saisir un nouveau mot de passe.",
"admin_now_is_admin": "{0} est maintenant admin.",
"admin_now_not_admin": "{0} n'est plus admin.",
"admin_now_enabled": "{0} est maintenant activé.",
diff --git a/src/GitServer/Localization/ja.json b/src/GitServer/Localization/ja.json
index b4b2243..143014c 100644
--- a/src/GitServer/Localization/ja.json
+++ b/src/GitServer/Localization/ja.json
@@ -156,6 +156,11 @@
"profile_section": "プロフィール",
"bio_label": "自己紹介",
"avatar_url_label": "アバター URL",
+ "country_label": "国",
+ "company_name_label": "会社名",
+ "preferred_language_label": "優先言語",
+ "preferred_language_default": "デフォルト(英語)",
+ "preferred_language_hint": "ログイン時に自動的に選択されます。",
"settings_save_profile": "プロフィールを保存",
"settings_change_password": "パスワードを変更",
"settings_set_password": "パスワードを設定",
@@ -188,6 +193,7 @@
"success_settings_saved": "設定を保存しました。",
"success_profile_saved": "プロフィールを保存しました。",
"success_password_changed": "パスワードを変更しました。",
+ "error_new_password_required": "新しいパスワードを入力してください。",
"admin_now_is_admin": "{0} は管理者になりました。",
"admin_now_not_admin": "{0} は管理者ではなくなりました。",
"admin_now_enabled": "{0} は有効になりました。",
diff --git a/src/GitServer/Localization/nl.json b/src/GitServer/Localization/nl.json
index 2979a03..95461c4 100644
--- a/src/GitServer/Localization/nl.json
+++ b/src/GitServer/Localization/nl.json
@@ -157,6 +157,11 @@
"profile_section": "Profiel",
"bio_label": "Bio",
"avatar_url_label": "Avatar URL",
+ "country_label": "Land",
+ "company_name_label": "Bedrijfsnaam",
+ "preferred_language_label": "Voorkeurstaal",
+ "preferred_language_default": "Standaard (Engels)",
+ "preferred_language_hint": "Wordt automatisch geselecteerd bij het inloggen.",
"settings_save_profile": "Profiel opslaan",
"settings_change_password": "Wachtwoord wijzigen",
"settings_set_password": "Wachtwoord instellen",
@@ -189,6 +194,7 @@
"success_settings_saved": "Instellingen opgeslagen.",
"success_profile_saved": "Profiel opgeslagen.",
"success_password_changed": "Wachtwoord gewijzigd.",
+ "error_new_password_required": "Vul een nieuw wachtwoord in.",
"admin_now_is_admin": "{0} is nu admin.",
"admin_now_not_admin": "{0} is geen admin meer.",
"admin_now_enabled": "{0} is nu ingeschakeld.",
diff --git a/src/GitServer/Localization/pt.json b/src/GitServer/Localization/pt.json
index f0d2194..3753762 100644
--- a/src/GitServer/Localization/pt.json
+++ b/src/GitServer/Localization/pt.json
@@ -156,6 +156,11 @@
"profile_section": "Perfil",
"bio_label": "Biografia",
"avatar_url_label": "URL do avatar",
+ "country_label": "País",
+ "company_name_label": "Empresa",
+ "preferred_language_label": "Idioma preferido",
+ "preferred_language_default": "Padrão (inglês)",
+ "preferred_language_hint": "Selecionado automaticamente ao entrar.",
"settings_save_profile": "Salvar perfil",
"settings_change_password": "Alterar senha",
"settings_set_password": "Definir senha",
@@ -188,6 +193,7 @@
"success_settings_saved": "Configurações salvas.",
"success_profile_saved": "Perfil salvo.",
"success_password_changed": "Senha alterada.",
+ "error_new_password_required": "Informe uma nova senha.",
"admin_now_is_admin": "{0} agora é admin.",
"admin_now_not_admin": "{0} não é mais admin.",
"admin_now_enabled": "{0} está agora ativado.",
diff --git a/src/GitServer/Localization/ru.json b/src/GitServer/Localization/ru.json
index 8dc270c..6de57de 100644
--- a/src/GitServer/Localization/ru.json
+++ b/src/GitServer/Localization/ru.json
@@ -156,6 +156,11 @@
"profile_section": "Профиль",
"bio_label": "Биография",
"avatar_url_label": "URL аватара",
+ "country_label": "Страна",
+ "company_name_label": "Компания",
+ "preferred_language_label": "Предпочитаемый язык",
+ "preferred_language_default": "По умолчанию (английский)",
+ "preferred_language_hint": "Выбирается автоматически при входе.",
"settings_save_profile": "Сохранить профиль",
"settings_change_password": "Изменить пароль",
"settings_set_password": "Установить пароль",
@@ -188,6 +193,7 @@
"success_settings_saved": "Настройки сохранены.",
"success_profile_saved": "Профиль сохранён.",
"success_password_changed": "Пароль изменён.",
+ "error_new_password_required": "Введите новый пароль.",
"admin_now_is_admin": "{0} теперь администратор.",
"admin_now_not_admin": "{0} больше не администратор.",
"admin_now_enabled": "{0} теперь включён.",
diff --git a/src/GitServer/Localization/zh.json b/src/GitServer/Localization/zh.json
index cb16f7b..7eb476c 100644
--- a/src/GitServer/Localization/zh.json
+++ b/src/GitServer/Localization/zh.json
@@ -156,6 +156,11 @@
"profile_section": "个人资料",
"bio_label": "个人简介",
"avatar_url_label": "头像链接",
+ "country_label": "国家",
+ "company_name_label": "公司名称",
+ "preferred_language_label": "首选语言",
+ "preferred_language_default": "默认(英语)",
+ "preferred_language_hint": "登录时会自动选择该语言。",
"settings_save_profile": "保存资料",
"settings_change_password": "修改密码",
"settings_set_password": "设置密码",
@@ -188,6 +193,7 @@
"success_settings_saved": "设置已保存。",
"success_profile_saved": "资料已保存。",
"success_password_changed": "密码已修改。",
+ "error_new_password_required": "请输入新密码。",
"admin_now_is_admin": "{0} 现在是管理员。",
"admin_now_not_admin": "{0} 不再是管理员。",
"admin_now_enabled": "{0} 现已启用。",
diff --git a/src/GitServer/Models/AppUser.cs b/src/GitServer/Models/AppUser.cs
index afc0de0..e133ce9 100644
--- a/src/GitServer/Models/AppUser.cs
+++ b/src/GitServer/Models/AppUser.cs
@@ -9,6 +9,9 @@ public class AppUser : IdentityUser
public string DisplayName { get; set; } = "";
public string? Bio { get; set; }
public string? AvatarUrl { get; set; }
+ public string? Country { get; set; }
+ public string? CompanyName { get; set; }
+ public string? PreferredLanguage { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastLoginAt { get; set; }
public bool IsAdmin { get; set; }
diff --git a/src/GitServer/gitserver.db b/src/GitServer/gitserver.db
index 0568f8b..74413ff 100644
Binary files a/src/GitServer/gitserver.db and b/src/GitServer/gitserver.db differ
diff --git a/src/GitServer/gitserver.db-shm b/src/GitServer/gitserver.db-shm
index 7ee0cb0..ccc51b4 100644
Binary files a/src/GitServer/gitserver.db-shm and b/src/GitServer/gitserver.db-shm differ
diff --git a/src/GitServer/gitserver.db-wal b/src/GitServer/gitserver.db-wal
index dff709b..7db7fea 100644
Binary files a/src/GitServer/gitserver.db-wal and b/src/GitServer/gitserver.db-wal differ
diff --git a/src/GitServer/wwwroot/Auth/Login.cshtml.cs b/src/GitServer/wwwroot/Auth/Login.cshtml.cs
index c31ee49..710f66e 100644
--- a/src/GitServer/wwwroot/Auth/Login.cshtml.cs
+++ b/src/GitServer/wwwroot/Auth/Login.cshtml.cs
@@ -37,6 +37,18 @@ public class LoginModel(
{
user.LastLoginAt = DateTime.UtcNow;
await db.SaveChangesAsync();
+
+ if (!string.IsNullOrEmpty(user.PreferredLanguage))
+ {
+ Response.Cookies.Append("lang", user.PreferredLanguage, new CookieOptions
+ {
+ Expires = DateTimeOffset.UtcNow.AddYears(1),
+ IsEssential = true,
+ SameSite = SameSiteMode.Lax,
+ HttpOnly = true
+ });
+ }
+
return LocalRedirect(returnUrl ?? "/");
}
diff --git a/src/GitServer/wwwroot/ExploreUsers.cshtml b/src/GitServer/wwwroot/ExploreUsers.cshtml
index 4cd6186..f6ef41b 100644
--- a/src/GitServer/wwwroot/ExploreUsers.cshtml
+++ b/src/GitServer/wwwroot/ExploreUsers.cshtml
@@ -35,6 +35,19 @@
{
<p class="user-card-bio">@user.Bio</p>
}
+ @if (!string.IsNullOrEmpty(user.CompanyName) || !string.IsNullOrEmpty(user.Country))
+ {
+ <p class="text-muted user-card-meta">
+ @if (!string.IsNullOrEmpty(user.CompanyName))
+ {
+ <span>🏢 @user.CompanyName</span>
+ }
+ @if (!string.IsNullOrEmpty(user.Country))
+ {
+ <span>🌐 @user.Country</span>
+ }
+ </p>
+ }
</div>
</a>
}
diff --git a/src/GitServer/wwwroot/ExploreUsers.cshtml.cs b/src/GitServer/wwwroot/ExploreUsers.cshtml.cs
index 5a77b51..dffacf9 100644
--- a/src/GitServer/wwwroot/ExploreUsers.cshtml.cs
+++ b/src/GitServer/wwwroot/ExploreUsers.cshtml.cs
@@ -24,7 +24,9 @@ public class ExploreUsersModel(AppDbContext db) : PageModel
query = query.Where(u =>
u.UserName!.ToLower().Contains(lower) ||
u.DisplayName.ToLower().Contains(lower) ||
- (u.Bio != null && u.Bio.ToLower().Contains(lower)));
+ (u.Bio != null && u.Bio.ToLower().Contains(lower)) ||
+ (u.CompanyName != null && u.CompanyName.ToLower().Contains(lower)) ||
+ (u.Country != null && u.Country.ToLower().Contains(lower)));
}
Users = await query
diff --git a/src/GitServer/wwwroot/Shared/_ProfileHeader.cshtml b/src/GitServer/wwwroot/Shared/_ProfileHeader.cshtml
index 0f1a005..fd05047 100644
--- a/src/GitServer/wwwroot/Shared/_ProfileHeader.cshtml
+++ b/src/GitServer/wwwroot/Shared/_ProfileHeader.cshtml
@@ -11,6 +11,19 @@
{
<p>@Model.Bio</p>
}
+ @if (!string.IsNullOrEmpty(Model.CompanyName) || !string.IsNullOrEmpty(Model.Country))
+ {
+ <p class="text-muted profile-company-country">
+ @if (!string.IsNullOrEmpty(Model.CompanyName))
+ {
+ <span>🏢 @Model.CompanyName</span>
+ }
+ @if (!string.IsNullOrEmpty(Model.Country))
+ {
+ <span>🌐 @Model.Country</span>
+ }
+ </p>
+ }
<p class="text-muted">@L["profile_member_since"] @Model.CreatedAt.ToString("MMMM yyyy")</p>
</div>
</div>
diff --git a/src/GitServer/wwwroot/User/Profile.cshtml b/src/GitServer/wwwroot/User/Profile.cshtml
index ae41cde..2e13a5f 100644
--- a/src/GitServer/wwwroot/User/Profile.cshtml
+++ b/src/GitServer/wwwroot/User/Profile.cshtml
@@ -48,7 +48,15 @@
{
<p class="text-muted repo-collaborators">
@L["repo_card_collaborators"]
- @string.Join(", ", repo.Accesses.Select(a => a.User.UserName))
+ @for (var i = 0; i < repo.Accesses.Count; i++)
+ {
+ var access = repo.Accesses.ElementAt(i);
+ <a href="/User/@access.User.UserName">@access.User.UserName</a>
+ if (i < repo.Accesses.Count - 1)
+ {
+ @:,
+ }
+ }
</p>
}
<div class="repo-meta">
diff --git a/src/GitServer/wwwroot/User/Settings.cshtml b/src/GitServer/wwwroot/User/Settings.cshtml
index 5cce557..24bd5f9 100644
--- a/src/GitServer/wwwroot/User/Settings.cshtml
+++ b/src/GitServer/wwwroot/User/Settings.cshtml
@@ -33,6 +33,27 @@
<input id="avatarUrl" name="NewAvatarUrl" type="url" class="form-control"
value="@Model.AvatarUrl" />
</div>
+ <div class="form-group">
+ <label for="country">@L["country_label"]</label>
+ <input id="country" name="NewCountry" type="text" class="form-control"
+ value="@Model.Country" />
+ </div>
+ <div class="form-group">
+ <label for="companyName">@L["company_name_label"]</label>
+ <input id="companyName" name="NewCompanyName" type="text" class="form-control"
+ value="@Model.CompanyName" />
+ </div>
+ <div class="form-group">
+ <label for="preferredLanguage">@L["preferred_language_label"]</label>
+ <select id="preferredLanguage" name="NewPreferredLanguage" class="form-control">
+ <option value="">@L["preferred_language_default"]</option>
+ @foreach (var (code, name) in L.AvailableLanguages())
+ {
+ <option value="@code" selected="@(code == Model.PreferredLanguage)">@name</option>
+ }
+ </select>
+ <span class="form-hint">@L["preferred_language_hint"]</span>
+ </div>
<button type="submit" class="btn btn-primary">@L["settings_save_profile"]</button>
</form>
</section>
diff --git a/src/GitServer/wwwroot/User/Settings.cshtml.cs b/src/GitServer/wwwroot/User/Settings.cshtml.cs
index e34924c..f86e7df 100644
--- a/src/GitServer/wwwroot/User/Settings.cshtml.cs
+++ b/src/GitServer/wwwroot/User/Settings.cshtml.cs
@@ -14,6 +14,9 @@ public class UserSettingsModel(UserManager<AppUser> userManager, LocalizationSer
public string DisplayName { get; set; } = "";
public string? Bio { get; set; }
public string? AvatarUrl { get; set; }
+ public string? Country { get; set; }
+ public string? CompanyName { get; set; }
+ public string? PreferredLanguage { get; set; }
public string? Message { get; set; }
public bool IsError { get; set; }
public bool HasPassword { get; set; }
@@ -21,17 +24,28 @@ public class UserSettingsModel(UserManager<AppUser> userManager, LocalizationSer
[BindProperty] public string NewDisplayName { get; set; } = "";
[BindProperty] public string? NewBio { get; set; }
[BindProperty] public string? NewAvatarUrl { get; set; }
+ [BindProperty] public string? NewCountry { get; set; }
+ [BindProperty] public string? NewCompanyName { get; set; }
+ [BindProperty] public string? NewPreferredLanguage { get; set; }
[BindProperty] public string CurrentPassword { get; set; } = "";
[BindProperty] public string NewPassword { get; set; } = "";
- public async Task OnGetAsync()
+ private void LoadFrom(AppUser user)
{
- var user = await userManager.GetUserAsync(User);
- if (user == null) return;
CurrentUser = user;
DisplayName = user.DisplayName;
Bio = user.Bio;
AvatarUrl = user.AvatarUrl;
+ Country = user.Country;
+ CompanyName = user.CompanyName;
+ PreferredLanguage = user.PreferredLanguage;
+ }
+
+ public async Task OnGetAsync()
+ {
+ var user = await userManager.GetUserAsync(User);
+ if (user == null) return;
+ LoadFrom(user);
HasPassword = await userManager.HasPasswordAsync(user);
}
@@ -43,13 +57,16 @@ public class UserSettingsModel(UserManager<AppUser> userManager, LocalizationSer
user.DisplayName = NewDisplayName;
user.Bio = NewBio;
user.AvatarUrl = NewAvatarUrl;
+ user.Country = string.IsNullOrWhiteSpace(NewCountry) ? null : NewCountry.Trim();
+ user.CompanyName = string.IsNullOrWhiteSpace(NewCompanyName) ? null : NewCompanyName.Trim();
+ user.PreferredLanguage = string.IsNullOrWhiteSpace(NewPreferredLanguage) ? null : NewPreferredLanguage;
var result = await userManager.UpdateAsync(user);
Message = result.Succeeded ? L["success_profile_saved"] : string.Join(" ", result.Errors.Select(e => e.Description));
IsError = !result.Succeeded;
- CurrentUser = user;
- DisplayName = user.DisplayName; Bio = user.Bio; AvatarUrl = user.AvatarUrl;
+ LoadFrom(user);
+ HasPassword = await userManager.HasPasswordAsync(user);
return Page();
}
@@ -58,8 +75,18 @@ public class UserSettingsModel(UserManager<AppUser> userManager, LocalizationSer
var user = await userManager.GetUserAsync(User);
if (user == null) return NotFound();
+ LoadFrom(user);
+ HasPassword = await userManager.HasPasswordAsync(user);
+
+ if (string.IsNullOrEmpty(NewPassword))
+ {
+ Message = L["error_new_password_required"];
+ IsError = true;
+ return Page();
+ }
+
IdentityResult result;
- if (await userManager.HasPasswordAsync(user))
+ if (HasPassword)
result = await userManager.ChangePasswordAsync(user, CurrentPassword, NewPassword);
else
result = await userManager.AddPasswordAsync(user, NewPassword);
@@ -67,8 +94,6 @@ public class UserSettingsModel(UserManager<AppUser> userManager, LocalizationSer
Message = result.Succeeded ? L["success_password_changed"] : string.Join(" ", result.Errors.Select(e => e.Description));
IsError = !result.Succeeded;
- CurrentUser = user;
- DisplayName = user.DisplayName; Bio = user.Bio; AvatarUrl = user.AvatarUrl;
HasPassword = await userManager.HasPasswordAsync(user);
return Page();
}
diff --git a/src/GitServer/wwwroot/css/components.css b/src/GitServer/wwwroot/css/components.css
index 1a5ff71..060dc84 100644
--- a/src/GitServer/wwwroot/css/components.css
+++ b/src/GitServer/wwwroot/css/components.css
@@ -286,7 +286,7 @@ textarea.form-control { resize: vertical; }
/* === User directory (Explore > Users) === */
.user-list {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 0.75rem;
}
@@ -325,6 +325,14 @@ textarea.form-control { resize: vertical; }
-webkit-box-orient: vertical;
}
+.user-card-meta {
+ display: flex;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+ font-size: 0.8125rem;
+ margin-top: 0.25rem;
+}
+
.badge {
display: inline-block;
padding: 0.15rem 0.55rem;
@@ -411,6 +419,12 @@ textarea.form-control { resize: vertical; }
.profile-info h1 { margin-bottom: 0.125rem; }
+.profile-company-country {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
/* === Issues === */
.issues-header {
display: flex;