Compare commits
77 Commits
f9ec722f2e
...
feature/fr
| Author | SHA1 | Date | |
|---|---|---|---|
| e3cd44422c | |||
| 93e8b6ee0d | |||
| f1e693cbc0 | |||
| ba7db77505 | |||
| cd3fad95a5 | |||
| 04e2b6acc3 | |||
| 7e02d3cfe1 | |||
| 74307e614c | |||
| 31db3bc58c | |||
| 1b32456173 | |||
| eb94a1bcb9 | |||
| 10ac1a6076 | |||
| da6ffa8167 | |||
| 89b4398442 | |||
| 99ff02b01b | |||
| 67baddf9d0 | |||
| 3d219b8cf7 | |||
| 7ab03d8073 | |||
| a9b4d136d5 | |||
| cda10dfaa4 | |||
| 4fe23699c8 | |||
| fc064dd01c | |||
| 9b4c2062a7 | |||
| 826b654dc9 | |||
| a3a86d4fde | |||
| 27f3be7761 | |||
| efde701ba9 | |||
| 9645695535 | |||
| 3dd0460209 | |||
| ef4f0c0159 | |||
| 817e0b97e6 | |||
| cdc8b5c7a4 | |||
| be731be724 | |||
| 9fea2c5b7b | |||
| f7b537cbed | |||
| 109d5f88ea | |||
| 7229e369ae | |||
| a343c2e246 | |||
| ae888d2973 | |||
| 21fad5f698 | |||
| ce701cd43d | |||
| 74200c4575 | |||
| 8c86f5ddce | |||
| 661bb03d1d | |||
| c19cd0c718 | |||
| d8f64754b4 | |||
| 96026d448f | |||
| 3dfb1ee0a7 | |||
| 5fe1666163 | |||
| 59b5344377 | |||
| d5aa15ccca | |||
| 4b38a5e597 | |||
| fef9af6e91 | |||
| 60215782c0 | |||
| ea0afc4e62 | |||
| 2011d1fb36 | |||
| 28f222ee28 | |||
| c8c0a46bb6 | |||
| dd8864c56c | |||
| 509b8b003c | |||
| 0da09d7594 | |||
| 58ff76fdc6 | |||
| 3c65a825db | |||
| 2a066b7209 | |||
| b1865afced | |||
| 9b6a4c75b9 | |||
| 728258465d | |||
| 242872cba0 | |||
| 31225b51b2 | |||
| 533571859f | |||
| 28eba3f0fd | |||
| 67598f60bf | |||
| fdac0859ea | |||
| c807412076 | |||
| 20ea31e63a | |||
| 2128d5dfa4 | |||
| 8db6ea4754 |
4
.env
Normal file
4
.env
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
sike you thought I was like that
|
||||||
|
|
||||||
|
hehehehee (urp so full)
|
||||||
@@ -5,8 +5,8 @@ run-name: "${{ gitea.event.head_commit.message }}: Deploy API"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
#branches:
|
||||||
- main
|
# - main
|
||||||
paths:
|
paths:
|
||||||
- "api/**"
|
- "api/**"
|
||||||
- ".gitea/workflows/deploy-api.yaml"
|
- ".gitea/workflows/deploy-api.yaml"
|
||||||
@@ -38,5 +38,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Deploy container
|
- name: Deploy container
|
||||||
run: |
|
run: |
|
||||||
|
export POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
|
||||||
|
export JWT_SECRET=${{ secrets.JWT_SECRET }}
|
||||||
docker compose -f ./api/docker-compose.prod.yaml pull agologum-api
|
docker compose -f ./api/docker-compose.prod.yaml pull agologum-api
|
||||||
docker compose -f ./api/docker-compose.prod.yaml up -d --force-recreate agologum-api
|
docker compose -f ./api/docker-compose.prod.yaml up -d --force-recreate agologum-api
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ run-name: "${{ gitea.event.head_commit.message }}: Deploy Client"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
#branches:
|
||||||
- main
|
# - main
|
||||||
paths:
|
paths:
|
||||||
- "client/**"
|
- "client/**"
|
||||||
- ".gitea/workflows/deploy-client.yaml"
|
- ".gitea/workflows/deploy-client.yaml"
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,2 +1,7 @@
|
|||||||
|
|
||||||
# empty for now
|
# empty for now
|
||||||
|
|
||||||
|
# .env # urrp I eated it :33
|
||||||
|
|
||||||
|
# this one is for real though
|
||||||
|
client/.env
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## agologum
|
## agologum
|
||||||
A web server/web client template.
|
A web server/web client template.
|
||||||
Backend: a .NET web API with a [READACTED] database (probably sql for tinkering)
|
Backend: a .NET web API with a [READACTED] database (probably prostgressql for tinkering)
|
||||||
Frontend: Vue.js because I enjoy life and the splendor of God's creation
|
Frontend: Vue.js because I enjoy life and the splendor of God's creation
|
||||||
|
|||||||
1
api/.gitignore
vendored
1
api/.gitignore
vendored
@@ -25,6 +25,7 @@ bld/
|
|||||||
project.lock.json
|
project.lock.json
|
||||||
project.fragment.lock.json
|
project.fragment.lock.json
|
||||||
artifacts/
|
artifacts/
|
||||||
|
#[Mm]igrations/
|
||||||
|
|
||||||
# NuGet Packages
|
# NuGet Packages
|
||||||
*.nupkg
|
*.nupkg
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ COPY --from=build /app/publish ./
|
|||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT="Production"
|
||||||
ENTRYPOINT ["dotnet", "agologum-api.dll"]
|
ENTRYPOINT ["dotnet", "agologum-api.dll"]
|
||||||
|
|||||||
279
api/Migrations/20260321221316_InitialCreate.Designer.cs
generated
Normal file
279
api/Migrations/20260321221316_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace agologum_api.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260321221316_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
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("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
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("agologumApi.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("agologumApi.Models.User", 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("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
224
api/Migrations/20260321221316_InitialCreate.cs
Normal file
224
api/Migrations/20260321221316_InitialCreate.cs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace agologum_api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUsers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
PasswordHash = table.Column<string>(type: "text", nullable: true),
|
||||||
|
SecurityStamp = table.Column<string>(type: "text", nullable: true),
|
||||||
|
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
|
||||||
|
PhoneNumber = table.Column<string>(type: "text", nullable: true),
|
||||||
|
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetRoleClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
RoleId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||||
|
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "AspNetRoles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
UserId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||||
|
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserLogins",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ProviderKey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
|
||||||
|
UserId = table.Column<string>(type: "text", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
RoleId = table.Column<string>(type: "text", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "AspNetRoles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserTokens",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Value = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetRoleClaims_RoleId",
|
||||||
|
table: "AspNetRoleClaims",
|
||||||
|
column: "RoleId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "RoleNameIndex",
|
||||||
|
table: "AspNetRoles",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserClaims_UserId",
|
||||||
|
table: "AspNetUserClaims",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserLogins_UserId",
|
||||||
|
table: "AspNetUserLogins",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserRoles_RoleId",
|
||||||
|
table: "AspNetUserRoles",
|
||||||
|
column: "RoleId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "EmailIndex",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
column: "NormalizedEmail");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UserNameIndex",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
column: "NormalizedUserName",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetRoleClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserLogins");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserTokens");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
306
api/Migrations/20260322011947_AddItems.Designer.cs
generated
Normal file
306
api/Migrations/20260322011947_AddItems.Designer.cs
generated
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace agologum_api.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260322011947_AddItems")]
|
||||||
|
partial class AddItems
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
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("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
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("agologumApi.Models.Item", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastEditedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("agologumApi.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("agologumApi.Models.User", 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("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
api/Migrations/20260322011947_AddItems.cs
Normal file
39
api/Migrations/20260322011947_AddItems.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace agologum_api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddItems : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Items",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
LastEditedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Items", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
336
api/Migrations/20260322214843_AddRefreshTokens.Designer.cs
generated
Normal file
336
api/Migrations/20260322214843_AddRefreshTokens.Designer.cs
generated
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace agologum_api.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260322214843_AddRefreshTokens")]
|
||||||
|
partial class AddRefreshTokens
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
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("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
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("RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRevoked")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("agologumApi.Models.Item", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastEditedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("agologumApi.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("agologumApi.Models.User", 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("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
api/Migrations/20260322214843_AddRefreshTokens.cs
Normal file
40
api/Migrations/20260322214843_AddRefreshTokens.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace agologum_api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddRefreshTokens : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "RefreshTokens",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Token = table.Column<string>(type: "text", nullable: false),
|
||||||
|
UserId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
IsRevoked = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_RefreshTokens", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "RefreshTokens");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
333
api/Migrations/AppDbContextModelSnapshot.cs
Normal file
333
api/Migrations/AppDbContextModelSnapshot.cs
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace agologum_api.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
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("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
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("RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRevoked")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("agologumApi.Models.Item", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastEditedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("agologumApi.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("agologumApi.Models.User", 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("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("agologumApi.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
api/Program.cs
113
api/Program.cs
@@ -1,8 +1,51 @@
|
|||||||
|
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using agologumApi.Models;
|
||||||
|
using agologumApi.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
var key = builder.Configuration["Jwt:Key"];
|
||||||
|
if(key == null) return;
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
// services
|
||||||
|
builder.Services.AddScoped<ItemService>();
|
||||||
|
builder.Services.AddScoped<JwtService>();
|
||||||
|
|
||||||
|
// configuration for jwt authentication
|
||||||
|
builder.Services.AddIdentity<User, IdentityRole>()
|
||||||
|
.AddEntityFrameworkStores<AppDbContext>()
|
||||||
|
.AddDefaultTokenProviders();
|
||||||
|
builder.Services.AddAuthentication(options => {
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
}).AddJwtBearer(options => {
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters {
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = "agologum",
|
||||||
|
ValidAudience = "agologum",
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),
|
||||||
|
ClockSkew = TimeSpan.Zero
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
// configuration for behind my nginx proxy
|
// configuration for behind my nginx proxy
|
||||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
{
|
{
|
||||||
@@ -10,7 +53,7 @@ builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|||||||
ForwardedHeaders.XForwardedFor |
|
ForwardedHeaders.XForwardedFor |
|
||||||
ForwardedHeaders.XForwardedProto;
|
ForwardedHeaders.XForwardedProto;
|
||||||
|
|
||||||
options.KnownNetworks.Clear();
|
options.KnownIPNetworks.Clear();
|
||||||
options.KnownProxies.Clear();
|
options.KnownProxies.Clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,42 +61,56 @@ builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("dev",
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
// https://www.reddit.com/r/dotnet/comments/1h7vzbs/how_do_you_guys_handle_authorization_on_a_web_api/
|
||||||
|
// add authorization here
|
||||||
|
// controllers will have endpoints based on authorization
|
||||||
|
// frontend is a different story
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseForwardedHeaders();
|
app.UseForwardedHeaders();
|
||||||
|
app.UseCors("dev");
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsEnvironment("Development")) {
|
||||||
{
|
|
||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
} else {
|
||||||
|
app.UseHttpsRedirection();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.MapControllers();
|
||||||
|
|
||||||
|
// attempt enitity-framework migrations at startup. love you stack overflow
|
||||||
|
using (var scope = app.Services.CreateScope()) {
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
// below is a placeholder endpoint
|
var retries = 10;
|
||||||
var summaries = new[]
|
while (retries-- > 0) {
|
||||||
{
|
try {
|
||||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
db.Database.Migrate();
|
||||||
};
|
break;
|
||||||
|
} catch {
|
||||||
app.MapGet("api/weatherforecast", () =>
|
Thread.Sleep(5000);
|
||||||
{
|
}
|
||||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
}
|
||||||
new WeatherForecast
|
}
|
||||||
(
|
|
||||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
|
||||||
Random.Shared.Next(-20, 55),
|
|
||||||
summaries[Random.Shared.Next(summaries.Length)]
|
|
||||||
))
|
|
||||||
.ToArray();
|
|
||||||
return forecast;
|
|
||||||
})
|
|
||||||
.WithName("GetWeatherForecast");
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
|
||||||
{
|
|
||||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"http": {
|
"http-dev": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "http://localhost:5227",
|
"applicationUrl": "http://0.0.0.0:5227",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"https": {
|
"https-dev": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "https://localhost:7182;http://localhost:5227",
|
"applicationUrl": "https://0.0.0.0:7182;http://0.0.0.0:5227",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,21 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@agologum_api_HostAddress = http://localhost:5227
|
@agologum_api_HostAddress = http://0.0.0.0:5227
|
||||||
|
|
||||||
GET {{agologum_api_HostAddress}}/weatherforecast/
|
GET {{agologum_api_HostAddress}}/weatherforecast/
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Host=agologum-net;Port=5432;Database=agologum;Username=agologum;Password=${POSTGRES_PASSWORD}"
|
||||||
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"https_port": 443
|
"https_port": 443,
|
||||||
|
"Jwt": {
|
||||||
|
"Key": "",
|
||||||
|
"Issuer": "agologum-api",
|
||||||
|
"Audience": "agologum-users"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
|
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
agologum-api:
|
agologum-api:
|
||||||
image: git.vxbard.net/homeburger/agologum-api:latest
|
image: git.vxbard.net/homeburger/agologum-api:latest
|
||||||
container_name: agologum-api
|
container_name: agologum-api
|
||||||
restart: always
|
restart: always
|
||||||
|
environment:
|
||||||
|
ConnectionStrings__DefaultConnection: Host=agologum-db;Port=5432;Database=agologum;Username=agologum;Password=${POSTGRES_PASSWORD}
|
||||||
|
Jwt__Key: ${JWT_SECRET} # must export the secret as a variable in the ci script
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
|
networks:
|
||||||
|
- agologum-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
agologum-net:
|
||||||
|
external: true
|
||||||
112
api/src/Controllers/AuthController.cs
Normal file
112
api/src/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
using agologumApi.Models;
|
||||||
|
using agologumApi.Services;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AuthController : ControllerBase {
|
||||||
|
|
||||||
|
// identity things
|
||||||
|
private readonly UserManager<User> userManager_;
|
||||||
|
private readonly SignInManager<User> signInManager_;
|
||||||
|
|
||||||
|
private readonly JwtService jwt_;
|
||||||
|
|
||||||
|
public AuthController(UserManager<User> userManager, SignInManager<User> signInManager, JwtService jwt) {
|
||||||
|
|
||||||
|
userManager_ = userManager;
|
||||||
|
signInManager_ = signInManager;
|
||||||
|
jwt_ = jwt;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<ActionResult> Register(RegisterDto dto) {
|
||||||
|
var user = new User {
|
||||||
|
UserName = dto.UserName,
|
||||||
|
Email = dto.Email,
|
||||||
|
CreatedAt = DateTime.UtcNow // yeah why not utc
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await userManager_.CreateAsync(user, dto.Password);
|
||||||
|
if(!result.Succeeded) return BadRequest(result.Errors);
|
||||||
|
|
||||||
|
return CreatedAtAction(
|
||||||
|
nameof(Register),
|
||||||
|
new { id = user.Id }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<ActionResult> Login(LoginDto dto)
|
||||||
|
{
|
||||||
|
var user = await userManager_.FindByNameAsync(dto.UserName);
|
||||||
|
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var result = await signInManager_.CheckPasswordSignInAsync(user, dto.Password, false);
|
||||||
|
|
||||||
|
if(!result.Succeeded) return Unauthorized();
|
||||||
|
|
||||||
|
var accessToken = jwt_.GenerateJwt(user);
|
||||||
|
var refreshToken = jwt_.GenerateRefreshToken();
|
||||||
|
RefreshToken newTokenObject = new RefreshToken {
|
||||||
|
Token = refreshToken,
|
||||||
|
UserId = user.Id,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddDays(30),
|
||||||
|
IsRevoked = false
|
||||||
|
};
|
||||||
|
await jwt_.AddRefreshToken(newTokenObject);
|
||||||
|
|
||||||
|
return Ok(new { accessToken, refreshToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize] // authorize is handled by middleware
|
||||||
|
[HttpPost("logout")]
|
||||||
|
public async Task<ActionResult> Logout(string refreshTokenString) {
|
||||||
|
// revoke refresh token
|
||||||
|
bool success = await jwt_.RevokeRefreshToken(refreshTokenString);
|
||||||
|
if(!success) return NotFound();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("refresh")] // allow-anonymous by default
|
||||||
|
public async Task<ActionResult> Refresh(TokenDto request) {
|
||||||
|
|
||||||
|
RefreshToken? storedToken = await jwt_.GetRefreshToken(request.RefreshToken);
|
||||||
|
if (storedToken == null) return Unauthorized();
|
||||||
|
bool valid = (storedToken.IsRevoked) ||
|
||||||
|
(storedToken.ExpiresAt < DateTime.UtcNow);
|
||||||
|
if(!valid) return Unauthorized(); // TODO: delete the invalid token
|
||||||
|
|
||||||
|
User? user = await jwt_.GetUser(storedToken.UserId);
|
||||||
|
if(user == null) return NotFound();
|
||||||
|
string? newAccessToken = jwt_.GenerateJwt(user);
|
||||||
|
if(newAccessToken == null) return NotFound();
|
||||||
|
string newRefreshToken = jwt_.GenerateRefreshToken();
|
||||||
|
|
||||||
|
storedToken.IsRevoked = true;
|
||||||
|
RefreshToken newTokenObject = new RefreshToken {
|
||||||
|
Token = newRefreshToken,
|
||||||
|
UserId = storedToken.UserId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddDays(30),
|
||||||
|
IsRevoked = false
|
||||||
|
};
|
||||||
|
|
||||||
|
await jwt_.AddRefreshToken(newTokenObject);
|
||||||
|
|
||||||
|
return Ok(new { accessToken = newAccessToken, refreshToken = newRefreshToken });
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// email verification
|
||||||
|
// password reset
|
||||||
|
// oh hell naw 2FA I do not care enough
|
||||||
|
}
|
||||||
76
api/src/Controllers/ItemsController.cs
Normal file
76
api/src/Controllers/ItemsController.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
using agologumApi.Models;
|
||||||
|
using agologumApi.Services;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class ItemsController : ControllerBase {
|
||||||
|
|
||||||
|
private readonly ItemService service_;
|
||||||
|
|
||||||
|
public ItemsController(ItemService service) {
|
||||||
|
service_ = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<List<Item>>> getItems() {
|
||||||
|
return Ok(await service_.GetAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
public async Task<ActionResult<Item>> getItem(int id) {
|
||||||
|
|
||||||
|
var item = await service_.Get(id);
|
||||||
|
|
||||||
|
if (item == null) return NotFound();
|
||||||
|
|
||||||
|
return Ok(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize] // testing the authorization
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<Item>> createItem(ItemDto item) {
|
||||||
|
|
||||||
|
Item newItem = new Item {
|
||||||
|
Name = item.Name,
|
||||||
|
Description = item.Description,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
LastEditedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
var created = await service_.Create(newItem);
|
||||||
|
|
||||||
|
return CreatedAtAction(
|
||||||
|
nameof(getItem),
|
||||||
|
new { id = created.Id },
|
||||||
|
created
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<ActionResult<Item>> updateItem(int id, ItemDto item) {
|
||||||
|
|
||||||
|
var updated = await service_.Update(id, item);
|
||||||
|
|
||||||
|
if (updated == null) return NotFound();
|
||||||
|
|
||||||
|
return Ok(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<ActionResult> deleteItem(int id) {
|
||||||
|
|
||||||
|
var success = await service_.Delete(id);
|
||||||
|
|
||||||
|
if (!success) return NotFound();
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
api/src/Data/AppDbContext.cs
Normal file
17
api/src/Data/AppDbContext.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
using agologumApi.Models;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
|
||||||
|
public class AppDbContext : IdentityDbContext<User> {
|
||||||
|
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Db set for each model besides Users (DbSet<template> is already defined in IdenityDbContext<template>)
|
||||||
|
public DbSet<Item> Items { get; set; }
|
||||||
|
public DbSet<RefreshToken> RefreshTokens { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
19
api/src/Models/Item.cs
Normal file
19
api/src/Models/Item.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
namespace agologumApi.Models;
|
||||||
|
|
||||||
|
public class Item {
|
||||||
|
|
||||||
|
public int Id { get; set; }
|
||||||
|
public String Name { get; set; } = "";
|
||||||
|
public String Description { get; set; } = "";
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime LastEditedAt { get; set; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
public class ItemDto {
|
||||||
|
|
||||||
|
public String Name { get; set; } = "";
|
||||||
|
public String Description { get; set; } = "";
|
||||||
|
|
||||||
|
}
|
||||||
21
api/src/Models/RefreshToken.cs
Normal file
21
api/src/Models/RefreshToken.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
// a refresh token's purpose is to authenticate user's without logging in
|
||||||
|
public class RefreshToken {
|
||||||
|
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Token { get; set; } = "";
|
||||||
|
|
||||||
|
public string UserId { get; set; } = ""; // in EF Identity the IdentityUser's id is a GUID string (32 hex digits)
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
public bool IsRevoked { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TokenDto {
|
||||||
|
|
||||||
|
public string RefreshToken { get; set; } = "";
|
||||||
|
|
||||||
|
}
|
||||||
48
api/src/Models/User.cs
Normal file
48
api/src/Models/User.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace agologumApi.Models;
|
||||||
|
|
||||||
|
public class User : IdentityUser {
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
// properties inherited from IdentityUser:
|
||||||
|
/*
|
||||||
|
AccessFailedCount: Gets or sets the number of failed login attempts for the current user.
|
||||||
|
Claims: Navigation property for the claims this user possesses.
|
||||||
|
ConcurrencyStamp: A random value that must change whenever a user is persisted to the store
|
||||||
|
Email: Gets or sets the email address for this user.
|
||||||
|
EmailConfirmed: Gets or sets a flag indicating if a user has confirmed their email address.
|
||||||
|
Id: Gets or sets the primary key for this user.
|
||||||
|
LockoutEnabled: Gets or sets a flag indicating if the user could be locked out.
|
||||||
|
LockoutEnd: Gets or sets the date and time, in UTC, when any user lockout ends.
|
||||||
|
Logins: Navigation property for this users login accounts.
|
||||||
|
NormalizedEmail: Gets or sets the normalized email address for this user.
|
||||||
|
NormalizedUserName: Gets or sets the normalized user name for this user.
|
||||||
|
PasswordHash: Gets or sets a salted and hashed representation of the password for this user.
|
||||||
|
PhoneNumber: Gets or sets a telephone number for the user.
|
||||||
|
PhoneNumberConfirmed: Gets or sets a flag indicating if a user has confirmed their telephone address.
|
||||||
|
Roles: Navigation property for the roles this user belongs to.
|
||||||
|
SecurityStamp: A random value that must change whenever a users credentials change (password changed, login removed)
|
||||||
|
TwoFactorEnabled: Gets or sets a flag indicating if two factor authentication is enabled for this user.
|
||||||
|
UserName: Gets or sets the user name for this user.
|
||||||
|
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.entityframeworkcore.identityuser?view=aspnetcore-1.1
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
// DTOs include only the minimum information for transit
|
||||||
|
public class RegisterDto {
|
||||||
|
|
||||||
|
public string UserName { get; set; } = "";
|
||||||
|
public string Email { get; set; } = "";
|
||||||
|
public string Password { get; set; } = "";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoginDto {
|
||||||
|
|
||||||
|
public string UserName { get; set; } = "";
|
||||||
|
public string Password { get; set; } = "";
|
||||||
|
|
||||||
|
}
|
||||||
58
api/src/Services/ItemService.cs
Normal file
58
api/src/Services/ItemService.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
using agologumApi.Models;
|
||||||
|
|
||||||
|
namespace agologumApi.Services;
|
||||||
|
|
||||||
|
public class ItemService {
|
||||||
|
|
||||||
|
private readonly AppDbContext db_;
|
||||||
|
|
||||||
|
public ItemService(AppDbContext db) {
|
||||||
|
db_ = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Item>> GetAll() {
|
||||||
|
return await db_.Items.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Item?> Get(int id) {
|
||||||
|
return await db_.Items.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Item?> Get(string name) {
|
||||||
|
return await db_.Items.FirstOrDefaultAsync(u => u.Name == name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Item> Create(Item item) {
|
||||||
|
db_.Items.Add(item);
|
||||||
|
await db_.SaveChangesAsync();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Item?> Update(int id, ItemDto item) {
|
||||||
|
|
||||||
|
Item? oldItem = await db_.Items.FindAsync(id);
|
||||||
|
if(oldItem == null) return oldItem;
|
||||||
|
|
||||||
|
oldItem.Name = item.Name;
|
||||||
|
oldItem.Description = item.Description;
|
||||||
|
oldItem.LastEditedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await db_.SaveChangesAsync();
|
||||||
|
return oldItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Delete(int id) {
|
||||||
|
Item? item = await db_.Items.FindAsync(id);
|
||||||
|
if(item != null) {
|
||||||
|
db_.Items.Remove(item);
|
||||||
|
await db_.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
80
api/src/Services/JwtService.cs
Normal file
80
api/src/Services/JwtService.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Text;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
using agologumApi.Models;
|
||||||
|
|
||||||
|
public class JwtService {
|
||||||
|
|
||||||
|
private readonly IConfiguration config_;
|
||||||
|
private readonly AppDbContext db_;
|
||||||
|
|
||||||
|
public JwtService(IConfiguration config, AppDbContext db) { // why the heck does c# not have initializer lists ?
|
||||||
|
config_ = config;
|
||||||
|
db_ = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GenerateJwt(User user) {
|
||||||
|
|
||||||
|
string? jwtKey = config_["Jwt:Key"];
|
||||||
|
if(jwtKey == null) return null;
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
|
||||||
|
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
if(user.UserName == null) return null;
|
||||||
|
|
||||||
|
// not too sure
|
||||||
|
var claims = new[] {
|
||||||
|
new Claim(ClaimTypes.Name, user.UserName),
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: "agologum",
|
||||||
|
audience: "agologum",
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddHours(2), // will add a refresher later
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateRefreshToken() {
|
||||||
|
|
||||||
|
byte[] randomBytes = new byte[64];
|
||||||
|
RandomNumberGenerator.Fill(randomBytes.AsSpan());
|
||||||
|
return Convert.ToBase64String(randomBytes);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RefreshToken?> GetRefreshToken(string refreshTokenString) {
|
||||||
|
return await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RefreshToken> AddRefreshToken(RefreshToken refreshToken) {
|
||||||
|
db_.RefreshTokens.Add(refreshToken);
|
||||||
|
await db_.SaveChangesAsync();
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to get the User from the id that exists in a refresh token object
|
||||||
|
public async Task<User?> GetUser(string id) {
|
||||||
|
return await db_.Users.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RevokeRefreshToken(string refreshTokenString) {
|
||||||
|
var refreshToken = await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString);
|
||||||
|
if(refreshToken == null) return false;
|
||||||
|
refreshToken.IsRevoked = true;
|
||||||
|
await db_.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
api/test.cs
15
api/test.cs
@@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
// this is a test program for making sure your dotnet environment is working properly
|
|
||||||
|
|
||||||
/*
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
Stopwatch sw = new Stopwatch();
|
|
||||||
sw.Start();
|
|
||||||
Console.WriteLine("Hi mom !");
|
|
||||||
Console.WriteLine("doing a schmunguss");
|
|
||||||
sw.Stop();
|
|
||||||
|
|
||||||
Console.WriteLine("Time elapsed: {0}", sw.Elapsed);
|
|
||||||
*/
|
|
||||||
@@ -5,5 +5,5 @@ services:
|
|||||||
container_name: agologum-client
|
container_name: agologum-client
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "7000:80"
|
||||||
|
|
||||||
341
client/package-lock.json
generated
341
client/package-lock.json
generated
@@ -8,6 +8,8 @@
|
|||||||
"name": "agologum",
|
"name": "agologum",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.13.6",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.29",
|
"vue": "^3.5.29",
|
||||||
"vue-router": "^5.0.3"
|
"vue-router": "^5.0.3"
|
||||||
},
|
},
|
||||||
@@ -1760,6 +1762,23 @@
|
|||||||
"url": "https://github.com/sponsors/sxzz"
|
"url": "https://github.com/sponsors/sxzz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||||
|
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.11",
|
||||||
|
"form-data": "^4.0.5",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||||
@@ -1832,6 +1851,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001774",
|
"version": "1.0.30001774",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
|
||||||
@@ -1868,6 +1900,18 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/confbox": {
|
"node_modules/confbox": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||||
@@ -2001,6 +2045,29 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.302",
|
"version": "1.5.302",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
||||||
@@ -2030,6 +2097,51 @@
|
|||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.3",
|
"version": "0.27.3",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||||
@@ -2111,6 +2223,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -2126,6 +2274,15 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -2136,6 +2293,94 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
@@ -2314,6 +2559,15 @@
|
|||||||
"url": "https://github.com/sponsors/sxzz"
|
"url": "https://github.com/sponsors/sxzz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/memorystream": {
|
"node_modules/memorystream": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
||||||
@@ -2323,6 +2577,27 @@
|
|||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mitt": {
|
"node_modules/mitt": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
@@ -2529,6 +2804,66 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^7.7.7"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.5.0",
|
||||||
|
"vue": "^3.5.11"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/@vue/devtools-api": {
|
||||||
|
"version": "7.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
|
||||||
|
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-kit": "^7.7.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/@vue/devtools-kit": {
|
||||||
|
"version": "7.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
||||||
|
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-shared": "^7.7.9",
|
||||||
|
"birpc": "^2.3.0",
|
||||||
|
"hookable": "^5.5.3",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"speakingurl": "^14.0.1",
|
||||||
|
"superjson": "^2.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/@vue/devtools-shared": {
|
||||||
|
"version": "7.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
||||||
|
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"rfdc": "^1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/perfect-debounce": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pkg-types": {
|
"node_modules/pkg-types": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||||
@@ -2568,6 +2903,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/quansync": {
|
"node_modules/quansync": {
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
"type-check": "vue-tsc --build"
|
"type-check": "vue-tsc --build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.13.6",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.29",
|
"vue": "^3.5.29",
|
||||||
"vue-router": "^5.0.3"
|
"vue-router": "^5.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
9
client/scripts/launch_dev.sh
Normal file
9
client/scripts/launch_dev.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
# this script builds and launches the vue client served raw and locally
|
||||||
|
# (without docker) on localhost (default) or via the machine's ip given the argument -I
|
||||||
|
|
||||||
|
|
||||||
|
# launch the app
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# TODO: more configuration
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import index from './pages/index.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>You did it!</h1>
|
<router-view /> <!-- Routed components appear here -->
|
||||||
<p>
|
|
||||||
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
|
|
||||||
documentation
|
|
||||||
</p>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
46
client/src/api/AuthApi.ts
Normal file
46
client/src/api/AuthApi.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
// service to interact with the api/auth endpoints
|
||||||
|
// handles user registration, user logins, tokens, password reset, etc.
|
||||||
|
|
||||||
|
import { api, authStorage } from "./axios.ts"
|
||||||
|
import type { User, RegisterDto, LoginDto } from "../models/User.ts";
|
||||||
|
|
||||||
|
const API_URL: string = "/auth";
|
||||||
|
|
||||||
|
export const register = async (user: RegisterDto) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await api.post(`${API_URL}/register`, user);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// else return false
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const login = async (user: LoginDto ) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post(`${API_URL}/login`, user);
|
||||||
|
authStorage.setTokens(response.data);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
authStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getToken = () => {
|
||||||
|
authStorage.getAccessToken();
|
||||||
|
}
|
||||||
18
client/src/api/ItemsApi.ts
Normal file
18
client/src/api/ItemsApi.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
// services are kinda whatever, but in general its a good idea for all api calls to be within a service (at least thats how angular handles it)
|
||||||
|
// this item service will handle all to <-> from the server when handling item objects
|
||||||
|
|
||||||
|
import api from "./axios.ts"
|
||||||
|
import type { Item, ItemDto } from "../models/Item.ts";
|
||||||
|
|
||||||
|
const API_URL: string = "/items";
|
||||||
|
|
||||||
|
export const getItems = () => api.get<Item[]>(`${API_URL}`);
|
||||||
|
|
||||||
|
export const getItem = (id: number) => api.get<Item>(`${API_URL}/${id}`);
|
||||||
|
|
||||||
|
export const createItem = (data: ItemDto) => api.post<Item>(`${API_URL}`, data);
|
||||||
|
|
||||||
|
export const updateItem = (id: number, data: ItemDto) => api.put<Item>(`${API_URL}/${id}`, data);
|
||||||
|
|
||||||
|
export const deleteItem = (id: number) => api.delete<Item>(`${API_URL}/${id}`);
|
||||||
90
client/src/api/axios.ts
Normal file
90
client/src/api/axios.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
// http service hub
|
||||||
|
// handles interceptors and such
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const baseUrl: string = import.meta.env.DEV ? import.meta.env.VITE_DEV_API_URL : "https://app.vxbard.net/api"
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: baseUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
type FailedRequest = { resolve: (token: string) => void, reject: (error: unknown) => void}
|
||||||
|
let isRefreshing: boolean = false;
|
||||||
|
let failedQueue: FailedRequest[] = [];
|
||||||
|
|
||||||
|
export const authStorage = {
|
||||||
|
getAccessToken: () => localStorage.getItem("accessToken"),
|
||||||
|
getRefreshToken: () => localStorage.getItem("refreshToken"),
|
||||||
|
|
||||||
|
setTokens: ({ accessToken, refreshToken } : { accessToken: string, refreshToken: string }) => {
|
||||||
|
localStorage.setItem("accessToken", accessToken)
|
||||||
|
localStorage.setItem("refreshToken", refreshToken)
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: () => {
|
||||||
|
localStorage.removeItem("accessToken")
|
||||||
|
localStorage.removeItem("refreshToken")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processQueue = (error: unknown, token: string | null = null): void => {
|
||||||
|
failedQueue.forEach(prom => {
|
||||||
|
if (error) prom.reject(error);
|
||||||
|
else prom.resolve(token as string);
|
||||||
|
})
|
||||||
|
failedQueue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// intercept on each request
|
||||||
|
api.interceptors.request.use(config => { // add access token to request headers
|
||||||
|
|
||||||
|
const token = authStorage.getAccessToken();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// intercept on each response
|
||||||
|
api.interceptors.response.use(response => response, async error => { // mainly for authentication refreshTokens
|
||||||
|
const originalRequest = error.config;
|
||||||
|
|
||||||
|
// if un authorized then refresh the token
|
||||||
|
if(error.response?.status === 401 && !originalRequest._retry) {
|
||||||
|
if(isRefreshing) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
failedQueue.push({ resolve, reject })
|
||||||
|
}).then(token => {
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||||
|
return api(originalRequest);
|
||||||
|
}).catch(err => Promise.reject(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest._retry = true;
|
||||||
|
isRefreshing = true;
|
||||||
|
const refreshToken = authStorage.getRefreshToken();
|
||||||
|
try {
|
||||||
|
// request refresh endpoint get back a new accessToken
|
||||||
|
const res = await axios.post(`${baseUrl}/auth/refresh`, { refreshToken });
|
||||||
|
const { accessToken, refreshToken: newRefresh } = res.data;
|
||||||
|
authStorage.setTokens({ accessToken, refreshToken: newRefresh });
|
||||||
|
processQueue(null, accessToken);
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
return api(originalRequest);
|
||||||
|
} catch (err) {
|
||||||
|
processQueue(err, null);
|
||||||
|
authStorage.clear()
|
||||||
|
window.location.href = "/login";
|
||||||
|
return Promise.reject(err);
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default api;
|
||||||
3
client/src/assets/mobus.txt
Normal file
3
client/src/assets/mobus.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
// assets will contain common public resources
|
||||||
|
// icons, fonts (if needed locally), images, whatever
|
||||||
13
client/src/components/Header.vue
Normal file
13
client/src/components/Header.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
<!-- you know what components are :D -->
|
||||||
|
<!-- though I do miss angular's support for separating the file ( though i guess its technically possible in vue but it gets disorganized fast ) -->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button @click="count++">You clicked me {{ count }} times.</button>
|
||||||
|
</template>
|
||||||
33
client/src/components/ItemsTable.vue
Normal file
33
client/src/components/ItemsTable.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { useItemsStore} from "../stores/ItemsStore.ts";
|
||||||
|
|
||||||
|
const store = useItemsStore();
|
||||||
|
|
||||||
|
onMounted(() => { // register callback for when component is loaded on page
|
||||||
|
store.fetchItems();
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1>Items</h1>
|
||||||
|
|
||||||
|
<router-link to="/item/new">Create Item</router-link>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr v-for="item in store.items" :key="item.id">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>
|
||||||
|
<router-link :to="`/item/${item.id}`">Edit</router-link>
|
||||||
|
<button @click="store.removeItem(item.id)">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
3
client/src/composables/what.ts
Normal file
3
client/src/composables/what.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
// idk really what composables are but I think its extra service code that can be used in components
|
||||||
|
// I think they're useful for moving data from a data store to the component but I could just be trolling
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from "pinia"
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
13
client/src/models/Item.ts
Normal file
13
client/src/models/Item.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
export interface Item {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastEditedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemDto {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
21
client/src/models/User.ts
Normal file
21
client/src/models/User.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
// models are the data objects stored in the database. models defined here must match models defined in api/models
|
||||||
|
// dtos here must match the the dtos in api/src/Modelts/Dto.cs in name (case insensitive) (types are intermediately serialized to strings)
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterDto {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginDto {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
56
client/src/pages/ItemForm.vue
Normal file
56
client/src/pages/ItemForm.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!-- pages/views in vue are basically root-level full-page components -->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
|
import { useItemsStore } from "../stores/ItemsStore.ts";
|
||||||
|
import type { Item } from "../models/Item.ts";
|
||||||
|
|
||||||
|
const store = useItemsStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const item = ref<Item>({
|
||||||
|
id: 0,
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
|
lastEditedAt: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const id: string | undefined = route.params.id as string | undefined
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(id) {
|
||||||
|
const existing = store.items.find(i => i.id == Number(id));
|
||||||
|
if (existing) item.value = { ...existing };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function save(): Promise<void> {
|
||||||
|
if(id) {
|
||||||
|
await store.updateItem(Number(id), item.value);
|
||||||
|
} else {
|
||||||
|
await store.addItem(item.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push("/items"); // redirect
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>{{ id ? "Edit Item" : "Create Item" }}</h2> <!-- omg I love ternary operator :D -->
|
||||||
|
|
||||||
|
<form @submit.prevent="save">
|
||||||
|
<input v-model="item.name" placeholder="Name" />
|
||||||
|
<input v-model="item.description" placeholder="Name" />
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
44
client/src/pages/ItemsList.vue
Normal file
44
client/src/pages/ItemsList.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { onMounted } from "vue"
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { useItemsStore } from "../stores/ItemsStore.ts"
|
||||||
|
import * as authApi from "../api/AuthApi";
|
||||||
|
|
||||||
|
const store = useItemsStore()
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.fetchItems()
|
||||||
|
})
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
authApi.logout();
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Items</h1>
|
||||||
|
|
||||||
|
<router-link to="/item/new">Create Item</router-link>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr v-for="item in store.items" :key="item.id">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
<router-link :to="`/item/${item.id}`" custom v-slot="{ navigate }">
|
||||||
|
<button @click="navigate" role="link">Edit</button>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<button @click="store.removeItem(item.id)">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<button @click="logout()">Logout</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
50
client/src/pages/LoginForm.vue
Normal file
50
client/src/pages/LoginForm.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { onMounted, reactive } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
|
import type { LoginDto } from "../models/User.ts";
|
||||||
|
import * as authApi from "../api/AuthApi";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const user = reactive<LoginDto>({ // the template ensures type consistency
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
async function login(): Promise<void> {
|
||||||
|
|
||||||
|
const success: boolean = await authApi.login(user);
|
||||||
|
|
||||||
|
if(success) {
|
||||||
|
router.push("/"); // redirect
|
||||||
|
} else {
|
||||||
|
// prompt try again
|
||||||
|
}
|
||||||
|
// TODO: interceptor for when a request returns unauthorized to redirect to login
|
||||||
|
// TODO: when redirected to login, save previous url as a query parameter then redirect back to that url after login
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Login</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="login">
|
||||||
|
<input v-model="user.username" placeholder="username" />
|
||||||
|
<input v-model="user.password" type="password" placeholder="password" />
|
||||||
|
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
51
client/src/pages/RegisterForm.vue
Normal file
51
client/src/pages/RegisterForm.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { onMounted, reactive } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
|
import type { RegisterDto } from "../models/User.ts";
|
||||||
|
import * as authApi from "../api/AuthApi";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const user = reactive<RegisterDto>({ // the template ensures type consistency
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
async function register(): Promise<void> {
|
||||||
|
|
||||||
|
const success: boolean = await authApi.register(user);
|
||||||
|
|
||||||
|
if(success) {
|
||||||
|
router.push("/login"); // redirect
|
||||||
|
} else {
|
||||||
|
// prompt try again
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Register</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="register">
|
||||||
|
<input v-model="user.username" placeholder="username" />
|
||||||
|
<input v-model="user.email" placeholder="email" />
|
||||||
|
<input v-model="user.password" placeholder="password" />
|
||||||
|
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
24
client/src/pages/index.vue
Normal file
24
client/src/pages/index.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
<!-- pages are the base instance for routes. an endpoint serves a page, and the page loads components -->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>straight up gargoyling it !</h1>
|
||||||
|
<h3>yeah im so cool rn</h3>
|
||||||
|
<h1>imagining what I could do with themes :o</h1>
|
||||||
|
|
||||||
|
<router-link to="/items" custom v-slot="{ navigate }">
|
||||||
|
<button @click="navigate" role="link">Items</button>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link to="/register" custom v-slot="{ navigate }"> <!-- TODO: only if token == invalid -->
|
||||||
|
<button @click="navigate" role="link">Register</button>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link to="/login" custom v-slot="{ navigate }"> <!-- TODO: only if token == invalid -->
|
||||||
|
<button @click="navigate" role="link">Login</button>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,40 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
|
||||||
|
// the router creates front-end endpoints and serves pages to them
|
||||||
|
|
||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
import LoginForm from "../pages/LoginForm.vue";
|
||||||
|
import RegisterForm from "../pages/RegisterForm.vue";
|
||||||
|
import ItemsList from "../pages/ItemsList.vue";
|
||||||
|
import ItemForm from "../pages/ItemForm.vue";
|
||||||
|
import index from "../pages/index.vue";
|
||||||
|
|
||||||
|
import { authStorage } from "../api/axios.ts"
|
||||||
|
|
||||||
|
// link path to the page component
|
||||||
|
const routes = [
|
||||||
|
{ path: "/", component: index },
|
||||||
|
{ path: "/login", component: LoginForm },
|
||||||
|
{ path: "/register", component: RegisterForm },
|
||||||
|
{ path: "/items", component: ItemsList, meta: { requiresAuth: true } },
|
||||||
|
{ path: "/item/new", component: ItemForm, meta: { requiresAuth: true } },
|
||||||
|
{ path: "/item/:id", component: ItemForm, meta: { requiresAuth: true } }
|
||||||
|
]; // I really like this
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [],
|
routes: routes,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default router
|
// intercept before routing
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
|
||||||
|
const token: string | null = authStorage.getAccessToken();
|
||||||
|
if(to.meta.requiresAuth && !token) { // if the page requires use to be signed in, they must have at least a token set
|
||||||
|
next("/login");
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|||||||
48
client/src/stores/ItemsStore.ts
Normal file
48
client/src/stores/ItemsStore.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
// stores are for component state management
|
||||||
|
// Pinia (?) i kinda dont get it because in angular you just hook a component to a service and that's it,
|
||||||
|
// though I guess the service handled the state management
|
||||||
|
// sighh
|
||||||
|
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import type { Item, ItemDto } from "../models/Item.ts";
|
||||||
|
import * as itemsApi from "../api/ItemsApi";
|
||||||
|
|
||||||
|
interface ItemState {
|
||||||
|
items: Item[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useItemsStore = defineStore("items", {
|
||||||
|
|
||||||
|
state: (): ItemState => ({
|
||||||
|
items: [],
|
||||||
|
loading: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchItems() {
|
||||||
|
this.loading = true;
|
||||||
|
const response = await itemsApi.getItems();
|
||||||
|
this.items = response.data;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async addItem(item: ItemDto) {
|
||||||
|
const response = await itemsApi.createItem(item);
|
||||||
|
this.items.push(response.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateItem(id: number, item: ItemDto) {
|
||||||
|
const response = await itemsApi.updateItem(id, item);
|
||||||
|
const index = this.items.findIndex(i => i.id === id);
|
||||||
|
this.items[index] = response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeItem(id: number) {
|
||||||
|
await itemsApi.deleteItem(id);
|
||||||
|
this.items = this.items.filter(i => i.id !== id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -15,4 +15,7 @@ export default defineConfig({
|
|||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
24
scripts/DEV_README.md
Normal file
24
scripts/DEV_README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
## These are some notes for development
|
||||||
|
# contains some helpful tips, commands, and knowledge
|
||||||
|
|
||||||
|
Resetting the database (for dev):
|
||||||
|
> set development evironment (specify non-docker network and db password)
|
||||||
|
> dotnet ef database drop
|
||||||
|
> dotnet ef migrations remove
|
||||||
|
> if above errors, dotnet ef database update 0
|
||||||
|
> dotnet ef migrations add InitialCreate
|
||||||
|
|
||||||
|
To see live logs:
|
||||||
|
sudo docker logs -f -t agologum-api
|
||||||
|
|
||||||
|
public user:
|
||||||
|
> username=bard
|
||||||
|
> password=Public*890
|
||||||
|
|
||||||
|
chrome dev tools troubleshooting
|
||||||
|
> response body: Network => url endpoint => Response => expand
|
||||||
|
|
||||||
|
Always test build before committing
|
||||||
|
> for the client: $ npm run dev
|
||||||
|
> for the api: $ dotnet build
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
|
|
||||||
# idk yet
|
# idk yet
|
||||||
|
|
||||||
dotnet ./api/test.cs
|
#dotnet ./api/test.cs
|
||||||
|
|
||||||
|
set ASPNETCORE_ENVIRONMENT=Development
|
||||||
|
dotnet run
|
||||||
|
|
||||||
|
npm run dev
|
||||||
|
|||||||
Reference in New Issue
Block a user