diff --git a/.env b/.env new file mode 100644 index 0000000..fba0b89 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ + +sike you thought I was like that + +hehehehee (urp so full) diff --git a/.gitea/workflows/deploy-api.yaml b/.gitea/workflows/deploy-api.yaml index bff3cdb..6dc4943 100644 --- a/.gitea/workflows/deploy-api.yaml +++ b/.gitea/workflows/deploy-api.yaml @@ -5,8 +5,8 @@ run-name: "${{ gitea.event.head_commit.message }}: Deploy API" on: push: - branches: - - main + #branches: + # - main paths: - "api/**" - ".gitea/workflows/deploy-api.yaml" @@ -38,5 +38,6 @@ jobs: - name: Deploy container run: | + export POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} 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 diff --git a/.gitea/workflows/deploy-client.yaml b/.gitea/workflows/deploy-client.yaml index 1b435a7..1facb46 100644 --- a/.gitea/workflows/deploy-client.yaml +++ b/.gitea/workflows/deploy-client.yaml @@ -5,8 +5,8 @@ run-name: "${{ gitea.event.head_commit.message }}: Deploy Client" on: push: - branches: - - main + #branches: + # - main paths: - "client/**" - ".gitea/workflows/deploy-client.yaml" diff --git a/.gitignore b/.gitignore index de621d6..6e48099 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ # empty for now + +# .env # urrp I eated it :33 + +# this one is for real though +client/.env diff --git a/README.md b/README.md index c4c9031..53428f4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ ## agologum 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 diff --git a/api/.gitignore b/api/.gitignore index 01e74fd..f7b1493 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -25,6 +25,7 @@ bld/ project.lock.json project.fragment.lock.json artifacts/ +#[Mm]igrations/ # NuGet Packages *.nupkg diff --git a/api/Dockerfile b/api/Dockerfile index ca64b1e..41f3dfc 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -17,4 +17,5 @@ COPY --from=build /app/publish ./ EXPOSE 5000 +ENV ASPNETCORE_ENVIRONMENT="Production" ENTRYPOINT ["dotnet", "agologum-api.dll"] diff --git a/api/Migrations/20260314152859_InitialCreate.Designer.cs b/api/Migrations/20260314152859_InitialCreate.Designer.cs new file mode 100644 index 0000000..7d957ce --- /dev/null +++ b/api/Migrations/20260314152859_InitialCreate.Designer.cs @@ -0,0 +1,49 @@ +// +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("20260314152859_InitialCreate")] + partial class InitialCreate + { + /// + 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("agologumApi.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/Migrations/20260314152859_InitialCreate.cs b/api/Migrations/20260314152859_InitialCreate.cs new file mode 100644 index 0000000..2e0df2d --- /dev/null +++ b/api/Migrations/20260314152859_InitialCreate.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace agologum_api.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/api/Migrations/AppDbContextModelSnapshot.cs b/api/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..112584c --- /dev/null +++ b/api/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,46 @@ +// +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("agologumApi.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/Program.cs b/api/Program.cs index 36d8d30..efa938c 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,8 +1,17 @@ using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.EntityFrameworkCore; + +using agologumApi.Services; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); + +builder.Services.AddControllers(); +builder.Services.AddScoped(); + // configuration for behind my nginx proxy builder.Services.Configure(options => { @@ -10,7 +19,7 @@ builder.Services.Configure(options => ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - options.KnownNetworks.Clear(); + options.KnownIPNetworks.Clear(); options.KnownProxies.Clear(); }); @@ -18,42 +27,49 @@ builder.Services.Configure(options => // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +builder.Services.AddCors(options => +{ + options.AddPolicy("dev", + policy => + { + policy.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + var app = builder.Build(); app.UseForwardedHeaders(); +app.UseCors("dev"); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ +if (app.Environment.IsEnvironment("Development")) { 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(); -// below is a placeholder endpoint -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("api/weatherforecast", () => -{ - 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"); + var retries = 10; + while (retries-- > 0) { + try { + db.Database.Migrate(); + break; + } catch { + Thread.Sleep(5000); + } + } +} app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} diff --git a/api/Properties/launchSettings.json b/api/Properties/launchSettings.json index f689500..841996f 100644 --- a/api/Properties/launchSettings.json +++ b/api/Properties/launchSettings.json @@ -1,23 +1,23 @@ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "http-dev": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5227", + "applicationUrl": "http://0.0.0.0:5227", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "https": { + "https-dev": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7182;http://localhost:5227", + "applicationUrl": "https://0.0.0.0:7182;http://0.0.0.0:5227", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } - } + }, } } diff --git a/api/agologum-api.csproj b/api/agologum-api.csproj index bcac402..77eaeef 100644 --- a/api/agologum-api.csproj +++ b/api/agologum-api.csproj @@ -9,6 +9,17 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/api/agologum-api.http b/api/agologum-api.http index 39b8ff9..c422b39 100644 --- a/api/agologum-api.http +++ b/api/agologum-api.http @@ -1,4 +1,4 @@ -@agologum_api_HostAddress = http://localhost:5227 +@agologum_api_HostAddress = http://0.0.0.0:5227 GET {{agologum_api_HostAddress}}/weatherforecast/ Accept: application/json diff --git a/api/appsettings.json b/api/appsettings.json index 822760b..3bbbad3 100644 --- a/api/appsettings.json +++ b/api/appsettings.json @@ -5,6 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "ConnectionStrings": { + "DefaultConnection": "Host=agologum-net;Port=5432;Database=agologum;Username=agologum;Password=${POSTGRES_PASSWORD}" + }, "AllowedHosts": "*", "https_port": 443 } diff --git a/api/docker-compose.prod.yaml b/api/docker-compose.prod.yaml index 17d2751..0a86a5f 100644 --- a/api/docker-compose.prod.yaml +++ b/api/docker-compose.prod.yaml @@ -1,8 +1,18 @@ +version: "3.9" + services: agologum-api: image: git.vxbard.net/homeburger/agologum-api:latest container_name: agologum-api restart: always + environment: + ConnectionStrings__DefaultConnection: Host=agologum-db;Port=5432;Database=agologum;Username=agologum;Password=${POSTGRES_PASSWORD} ports: - "5000:5000" + networks: + - agologum-net + +networks: + agologum-net: + external: true \ No newline at end of file diff --git a/api/src/Controllers/UsersController.cs b/api/src/Controllers/UsersController.cs new file mode 100644 index 0000000..4178d19 --- /dev/null +++ b/api/src/Controllers/UsersController.cs @@ -0,0 +1,64 @@ + +using Microsoft.AspNetCore.Mvc; +using agologumApi.Models; +using agologumApi.Services; + +[ApiController] +[Route("api/[controller]")] +public class UsersController : ControllerBase +{ + private readonly UserService service_; + + public UsersController(UserService service) + { + service_ = service; + } + + [HttpGet] + public async Task>> getUsers() + { + return Ok(await service_.GetAll()); + } + + [HttpGet("{id:int}")] + public async Task> getUser(int id) + { + var user = await service_.Get(id); + + if (user == null) return NotFound(); + + return Ok(user); + } + + [HttpPost] + public async Task> createUser(User user) + { + var created = await service_.Create(user); + + return CreatedAtAction( + nameof(getUser), + new { id = created.Id }, + created + ); + } + + [HttpPut("{id}")] + public async Task> updateUser(int id, User user) + { + var updated = await service_.Update(user); + + if (updated == null) return NotFound(); + + return Ok(updated); + } + + [HttpDelete("{id}")] + public async Task deleteUser(int id) + { + var success = await service_.Delete(id); + + if (!success) return NotFound(); + + return NoContent(); + } +} \ No newline at end of file diff --git a/api/src/Data/AppDbContext.cs b/api/src/Data/AppDbContext.cs new file mode 100644 index 0000000..49df799 --- /dev/null +++ b/api/src/Data/AppDbContext.cs @@ -0,0 +1,13 @@ + +using agologumApi.Models; + +using Microsoft.EntityFrameworkCore; + +public class AppDbContext : DbContext { + + public AppDbContext(DbContextOptions options) : base(options) { + + } + + public DbSet Users { get; set; } +} \ No newline at end of file diff --git a/api/src/Models/User.cs b/api/src/Models/User.cs new file mode 100644 index 0000000..b42e2d8 --- /dev/null +++ b/api/src/Models/User.cs @@ -0,0 +1,10 @@ + +namespace agologumApi.Models; + +public class User { + + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Email { get; set; } = ""; + +}; diff --git a/api/src/Services/UserService.cs b/api/src/Services/UserService.cs new file mode 100644 index 0000000..0b085ec --- /dev/null +++ b/api/src/Services/UserService.cs @@ -0,0 +1,47 @@ + +using Microsoft.EntityFrameworkCore; + +using agologumApi.Models; + +namespace agologumApi.Services; + +public class UserService { + + private readonly AppDbContext db_; + + public UserService(AppDbContext db) { + db_ = db; + } + + public async Task> GetAll() { + return await db_.Users.ToListAsync(); + } + + public async Task Get(int id) { + return await db_.Users.FindAsync(id); + } + + public async Task Create(User user) { + db_.Users.Add(user); + await db_.SaveChangesAsync(); + return user; + } + + public async Task Update(User user) { + db_.Users.Update(user); + await db_.SaveChangesAsync(); + return user; + } + + public async Task Delete(int id) { + User? user = await db_.Users.FindAsync(id); + if(user != null) { + db_.Users.Remove(user); + await db_.SaveChangesAsync(); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/api/test.cs b/api/test.cs deleted file mode 100644 index ed14715..0000000 --- a/api/test.cs +++ /dev/null @@ -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); -*/ diff --git a/client/docker-compose.prod.yaml b/client/docker-compose.prod.yaml index 7cf6298..58be736 100644 --- a/client/docker-compose.prod.yaml +++ b/client/docker-compose.prod.yaml @@ -5,5 +5,5 @@ services: container_name: agologum-client restart: always ports: - - "8080:80" + - "7000:80" \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 55b27d0..40e20bc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,8 @@ "name": "agologum", "version": "0.0.0", "dependencies": { + "axios": "^1.13.6", + "pinia": "^3.0.4", "vue": "^3.5.29", "vue-router": "^5.0.3" }, @@ -1760,6 +1762,23 @@ "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": { "version": "2.10.0", "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" } }, + "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": { "version": "1.0.30001774", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", @@ -1868,6 +1900,18 @@ "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": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", @@ -2001,6 +2045,29 @@ "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": { "version": "1.5.302", "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" } }, + "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": { "version": "0.27.3", "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": { "version": "2.3.3", "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_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": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2136,6 +2293,94 @@ "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": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", @@ -2314,6 +2559,15 @@ "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": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -2323,6 +2577,27 @@ "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": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -2529,6 +2804,66 @@ "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", @@ -2568,6 +2903,12 @@ "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": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", diff --git a/client/package.json b/client/package.json index 18ee0de..f287f3c 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,8 @@ "type-check": "vue-tsc --build" }, "dependencies": { + "axios": "^1.13.6", + "pinia": "^3.0.4", "vue": "^3.5.29", "vue-router": "^5.0.3" }, diff --git a/client/scripts/launch_dev.sh b/client/scripts/launch_dev.sh new file mode 100644 index 0000000..a2f70aa --- /dev/null +++ b/client/scripts/launch_dev.sh @@ -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 diff --git a/client/src/App.vue b/client/src/App.vue index abfd315..cd279a6 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,11 +1,8 @@ - + + - - diff --git a/client/src/api/UsersApi.ts b/client/src/api/UsersApi.ts new file mode 100644 index 0000000..150a63a --- /dev/null +++ b/client/src/api/UsersApi.ts @@ -0,0 +1,25 @@ + +// 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 user service will handle all to <-> from the server when handling user objects +// should be injected with the http client (I think its axios ?) + +import axios from "axios"; +import type {AxiosResponse } from "axios"; +import type { User } from "../models/User.ts"; + +const API_URL: string = "/users"; + +const baseUrl: string = import.meta.env.DEV ? import.meta.env.VITE_DEV_API_URL : "https://app.vxbard.net/api" // TODO: overarching api service +const api = axios.create({ + baseURL: baseUrl +}); + +export const getUsers = () => api.get(`${API_URL}`); + +export const getUser = (id: number) => api.get(`${API_URL}/${id}`); + +export const createUser = (data: User) => api.post(`${API_URL}`, data); + +export const updateUser = (id: number, data: User) => api.put(`${API_URL}/${id}`, data); + +export const deleteUser = (id: number) => api.delete(`${API_URL}/${id}`); diff --git a/client/src/assets/mobus.txt b/client/src/assets/mobus.txt new file mode 100644 index 0000000..1748201 --- /dev/null +++ b/client/src/assets/mobus.txt @@ -0,0 +1,3 @@ + +// assets will contain common public resources +// icons, fonts (if needed locally), images, whatever diff --git a/client/src/components/Header.vue b/client/src/components/Header.vue new file mode 100644 index 0000000..a017076 --- /dev/null +++ b/client/src/components/Header.vue @@ -0,0 +1,13 @@ + + + + + + + diff --git a/client/src/components/UsersTable.vue b/client/src/components/UsersTable.vue new file mode 100644 index 0000000..b9c87e0 --- /dev/null +++ b/client/src/components/UsersTable.vue @@ -0,0 +1,33 @@ + + + + diff --git a/client/src/composables/what.ts b/client/src/composables/what.ts new file mode 100644 index 0000000..ccdf101 --- /dev/null +++ b/client/src/composables/what.ts @@ -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 diff --git a/client/src/main.ts b/client/src/main.ts index c8e37b0..a03cc32 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1,9 +1,11 @@ import { createApp } from 'vue' +import { createPinia } from "pinia" import App from './App.vue' import router from './router' const app = createApp(App) +app.use(createPinia()) app.use(router) app.mount('#app') diff --git a/client/src/models/User.ts b/client/src/models/User.ts new file mode 100644 index 0000000..03387ef --- /dev/null +++ b/client/src/models/User.ts @@ -0,0 +1,8 @@ + +// models are the data objects stored in the database. models defined here must match models defined in api/models + +export interface User { + id: number; + name: string; + email: string +} diff --git a/client/src/pages/UserForm.vue b/client/src/pages/UserForm.vue new file mode 100644 index 0000000..96510d3 --- /dev/null +++ b/client/src/pages/UserForm.vue @@ -0,0 +1,54 @@ + + + + + \ No newline at end of file diff --git a/client/src/pages/UsersList.vue b/client/src/pages/UsersList.vue new file mode 100644 index 0000000..0fb4214 --- /dev/null +++ b/client/src/pages/UsersList.vue @@ -0,0 +1,35 @@ + + + + \ No newline at end of file diff --git a/client/src/pages/index.vue b/client/src/pages/index.vue new file mode 100644 index 0000000..4e9fc51 --- /dev/null +++ b/client/src/pages/index.vue @@ -0,0 +1,16 @@ + + + + + + diff --git a/client/src/router/index.ts b/client/src/router/index.ts index e1eab52..e96dc27 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -1,8 +1,22 @@ -import { createRouter, createWebHistory } from 'vue-router' + +// the router creates front-end endpoints and serves pages to them + +import { createRouter, createWebHistory } from "vue-router"; +import UsersList from "../pages/UsersList.vue"; +import UserForm from "../pages/UserForm.vue"; +import index from "../pages/index.vue"; + +// link path to the page component +const routes = [ + { path: "/", component: index }, + { path: "/users", component: UsersList }, + { path: "/user/new", component: UserForm }, + { path: "/user/:id", component: UserForm } +]; // I really like this const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), - routes: [], -}) + routes: routes, +}); -export default router +export default router; diff --git a/client/src/stores/UsersStore.ts b/client/src/stores/UsersStore.ts new file mode 100644 index 0000000..a4d4b01 --- /dev/null +++ b/client/src/stores/UsersStore.ts @@ -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 { User } from "../models/User.ts"; +import * as api from "../api/UsersApi"; + +interface UserState { + users: User[]; + loading: boolean; +} + +export const useUsersStore = defineStore("users", { + + state: (): UserState => ({ + users: [], + loading: false + }), + + actions: { + async fetchUsers() { + this.loading = true; + const response = await api.getUsers(); + this.users = response.data; + this.loading = false; + }, + + async addUser(user: User) { + const response = await api.createUser(user); + this.users.push(response.data); + }, + + async updateUser(id: number, user: User) { + await api.updateUser(id, user); + const index = this.users.findIndex(i => i.id === id); + this.users[index] = user; + }, + + async removeUser(id: number) { + await api.deleteUser(id); + this.users = this.users.filter(i => i.id !== id); + } + } + +}); diff --git a/client/vite.config.ts b/client/vite.config.ts index 4217010..7c295c6 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -15,4 +15,7 @@ export default defineConfig({ '@': fileURLToPath(new URL('./src', import.meta.url)) }, }, + server: { + host: '0.0.0.0', + } }) diff --git a/scripts/execute_all.sh b/scripts/execute_all.sh index b0ac7d1..3d17957 100755 --- a/scripts/execute_all.sh +++ b/scripts/execute_all.sh @@ -1,4 +1,9 @@ # idk yet -dotnet ./api/test.cs +#dotnet ./api/test.cs + +set ASPNETCORE_ENVIRONMENT=Development +dotnet run + +npm run dev