commit 4c82631733c50d128e7457dbd918c40310b97658 Author: kittyegg Date: Sun May 10 03:16:55 2026 +0300 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fe84159 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +POSTGRES_USER=app +POSTGRES_PASSWORD=app +POSTGRES_DB=appdb +DB_HOST=db \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85fc430 --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +publish/ + +# Visual Studio +.vs/ +*.user +*.suo +*.userosscache +*.sln.docstates + +# Rider / JetBrains +.idea/ +*.sln.iml + +# VS Code +.vscode/ + +# ASP.NET Core +appsettings.Development.json +appsettings.Local.json + +# Environment variables +.env +#.env.* + +# Logs +logs/ +*.log + +# NuGet +*.nupkg +packages/ +!packages/build/ + +# Entity Framework +*.db +*.db-shm +*.db-wal + +# SQLite +*.sqlite +*.sqlite3 + +# OS files +.DS_Store +Thumbs.db + +# Node modules +node_modules/ + +# Coverage / tests +coverage/ +TestResults/ + +# Docker +docker-compose.override.yml + +# Generated files +*.cache +*.tmp +*.temp + +# Publish output +wwwroot/dist/ +wwwroot/build/ + +# Secrets +secrets.json + +# Certificates +*.pfx +*.pem +*.key + +# Resharper +_ReSharper*/ +*.DotSettings.user diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..88824fa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки. + +# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки) +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +# Этот этап используется для сборки проекта службы +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["URL-Shortener/URL-Shortener.csproj", "URL-Shortener/"] +RUN dotnet restore "./URL-Shortener/URL-Shortener.csproj" +COPY . . +WORKDIR "/src/URL-Shortener" +RUN dotnet build "./URL-Shortener.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./URL-Shortener.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "URL-Shortener.dll"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..59a010f --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# URL Shortener diff --git a/URL Shortener.slnx b/URL Shortener.slnx new file mode 100644 index 0000000..27fd245 --- /dev/null +++ b/URL Shortener.slnx @@ -0,0 +1,3 @@ + + + diff --git a/URL-Shortener/AppDbContext.cs b/URL-Shortener/AppDbContext.cs new file mode 100644 index 0000000..59767b0 --- /dev/null +++ b/URL-Shortener/AppDbContext.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using URL_Shortener.Models; + +namespace URL_Shortener +{ + public class AppDbContext(DbContextOptions options) : DbContext(options) + { + public DbSet ShortUrls => Set(); + public DbSet ClickInfos => Set(); + public DbSet Users => Set(); + } +} diff --git a/URL-Shortener/Controllers/ClickInfoController.cs b/URL-Shortener/Controllers/ClickInfoController.cs new file mode 100644 index 0000000..bc9e4e0 --- /dev/null +++ b/URL-Shortener/Controllers/ClickInfoController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using URL_Shortener.Models; +using URL_Shortener.DTO; + +namespace URL_Shortener.Controllers +{ + [ApiController] + [Route("clickInfo")] + public class ClickInfoController(AppDbContext db) : ControllerBase + { + private readonly AppDbContext _db = db; + + [HttpGet("{id:int}", Name = nameof(GetClickInfoByIdAsync))] + public async Task GetClickInfoByIdAsync(int id) + { + var clickInfo = await _db.ClickInfos.FindAsync(id); + if (clickInfo == null) + return NotFound(); + + return Ok(clickInfo); + } + + [HttpGet("by-short-url/{shortUrlId:int}")] + public async Task GetByShortUrlIdAsync(int shortUrlId) + { + var clickInfos = await _db.ClickInfos.Where(c => c.ShortUrlId == shortUrlId).ToListAsync(); + if (clickInfos.Count == 0) + return NotFound(); + + return Ok(clickInfos); + } + } +} diff --git a/URL-Shortener/Controllers/RedirectController.cs b/URL-Shortener/Controllers/RedirectController.cs new file mode 100644 index 0000000..104309d --- /dev/null +++ b/URL-Shortener/Controllers/RedirectController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using URL_Shortener.Models; + +namespace URL_Shortener.Controllers +{ + [ApiController] + [Route("/go")] + public class RedirectController(AppDbContext db) : ControllerBase + { + private readonly AppDbContext _db = db; + + [HttpGet("{shortCode}")] + public async Task RedirectToOriginalAsync(string shortCode) + { + var shortUrl = await _db.ShortUrls.FirstOrDefaultAsync(s => s.ShortCode == shortCode); + if (shortUrl == null) + return NotFound("Short URL not found"); + + if (shortUrl.ExpirationDateTime.HasValue && shortUrl.ExpirationDateTime.Value < DateTime.UtcNow || + shortUrl.ClickLimit > 0 && shortUrl.TotalClickCount >= shortUrl.ClickLimit) + return BadRequest("Short URL has expired"); + + var clickInfo = new ClickInfo + { + ShortUrlId = shortUrl.Id, + ClickDateTime = DateTime.UtcNow + }; + shortUrl.TotalClickCount++; + _db.ClickInfos.Add(clickInfo); + await _db.SaveChangesAsync(); + + return Redirect(shortUrl.OriginalUrl); + } + } +} diff --git a/URL-Shortener/Controllers/ShortUrlController.cs b/URL-Shortener/Controllers/ShortUrlController.cs new file mode 100644 index 0000000..36c6853 --- /dev/null +++ b/URL-Shortener/Controllers/ShortUrlController.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using URL_Shortener.DTO; +using URL_Shortener.Models; +using HashidsNet; + +namespace URL_Shortener.Controllers +{ + [ApiController] + [Route("shortUrl")] + public class ShortUrlController(AppDbContext db, Hashids hashids) : ControllerBase + { + private readonly AppDbContext _db = db; + private readonly Hashids _hashids = hashids; + + [HttpGet("all")] + public async Task GetAll() => + Ok(await _db.ShortUrls.ToListAsync()); + + [HttpGet("{id:int}", Name = nameof(GetShortUrlByIdAsync))] + public async Task GetShortUrlByIdAsync(int id) + { + var shortUrl = await _db.ShortUrls.FindAsync(id); + if (shortUrl == null) + return NotFound(); + + return Ok(shortUrl); + } + + [HttpGet("by-user-id/{userId:int}")] + public async Task GetByUserIdAsync(int userId) + { + var shortUrls = await _db.ShortUrls.Where(s => s.UserId == userId).ToListAsync(); + if (shortUrls.Count == 0) + return NotFound(); + + return Ok(shortUrls); + } + + [HttpPost] + public async Task CreateShortUrlAsync(CreateShortUrlDto dto) + { + var shortUrl = new ShortUrl + { + OriginalUrl = dto.OriginalUrl, + ExpirationDateTime = dto.ExpirationDateTime, + ClickLimit = dto.ClickLimit + }; + await _db.ShortUrls.AddAsync(shortUrl); + await _db.SaveChangesAsync(); + shortUrl.ShortCode = _hashids.Encode(shortUrl.Id); + await _db.SaveChangesAsync(); + + return CreatedAtRoute(nameof(GetShortUrlByIdAsync), new { id = shortUrl.Id }, shortUrl); + } + + [HttpDelete("{id:int}")] + public async Task DeleteShortUrlByIdAsync(int id) + { + var shortUrl = await _db.ShortUrls.FindAsync(id); + if (shortUrl == null) + return NotFound(); + + _db.ShortUrls.Remove(shortUrl); + + var clickInfos = _db.ClickInfos.Where(c => c.ShortUrlId == id); + if (clickInfos != null) + _db.ClickInfos.RemoveRange(clickInfos); + + await _db.SaveChangesAsync(); + + return NoContent(); + } + } +} \ No newline at end of file diff --git a/URL-Shortener/Controllers/UserController.cs b/URL-Shortener/Controllers/UserController.cs new file mode 100644 index 0000000..4e349db --- /dev/null +++ b/URL-Shortener/Controllers/UserController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using URL_Shortener.Models; +using URL_Shortener.DTO; + +namespace URL_Shortener.Controllers +{ + [ApiController] + [Route("user")] + public class UserController(AppDbContext db) : ControllerBase + { + private readonly AppDbContext _db = db; + + [HttpGet("email")] + public async Task GetUserByEmailAsync(string email) + { + var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email); + if (user == null) + return NotFound("User not found"); + + return Ok(user); + } + } +} diff --git a/URL-Shortener/DTO/CreateShortUrlDto.cs b/URL-Shortener/DTO/CreateShortUrlDto.cs new file mode 100644 index 0000000..e346996 --- /dev/null +++ b/URL-Shortener/DTO/CreateShortUrlDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace URL_Shortener.DTO +{ + public class CreateShortUrlDto + { + [Required] + [Url] + public string OriginalUrl { get; set; } = string.Empty; + + public DateTime? ExpirationDateTime { get; set; } = null; + + public int ClickLimit { get; set; } = 0; + } +} diff --git a/URL-Shortener/Dockerfile b/URL-Shortener/Dockerfile new file mode 100644 index 0000000..88824fa --- /dev/null +++ b/URL-Shortener/Dockerfile @@ -0,0 +1,29 @@ +# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки. + +# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки) +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +# Этот этап используется для сборки проекта службы +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["URL-Shortener/URL-Shortener.csproj", "URL-Shortener/"] +RUN dotnet restore "./URL-Shortener/URL-Shortener.csproj" +COPY . . +WORKDIR "/src/URL-Shortener" +RUN dotnet build "./URL-Shortener.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./URL-Shortener.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "URL-Shortener.dll"] \ No newline at end of file diff --git a/URL-Shortener/Migrations/20260510000847_InitialCreate.Designer.cs b/URL-Shortener/Migrations/20260510000847_InitialCreate.Designer.cs new file mode 100644 index 0000000..6fe8d7f --- /dev/null +++ b/URL-Shortener/Migrations/20260510000847_InitialCreate.Designer.cs @@ -0,0 +1,116 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using URL_Shortener; + +#nullable disable + +namespace URL_Shortener.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260510000847_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("URL_Shortener.Models.ClickInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClickDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ShortUrlId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ClickInfos"); + }); + + modelBuilder.Entity("URL_Shortener.Models.ShortUrl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClickLimit") + .HasColumnType("integer"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("OriginalUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalClickCount") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ShortUrls"); + }); + + modelBuilder.Entity("URL_Shortener.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("HashedPassword") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegisterDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/URL-Shortener/Migrations/20260510000847_InitialCreate.cs b/URL-Shortener/Migrations/20260510000847_InitialCreate.cs new file mode 100644 index 0000000..e6de9d5 --- /dev/null +++ b/URL-Shortener/Migrations/20260510000847_InitialCreate.cs @@ -0,0 +1,79 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace URL_Shortener.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ClickInfos", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ShortUrlId = table.Column(type: "integer", nullable: false), + ClickDateTime = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClickInfos", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ShortUrls", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "integer", nullable: false), + OriginalUrl = table.Column(type: "text", nullable: false), + ShortCode = table.Column(type: "text", nullable: false), + TotalClickCount = table.Column(type: "integer", nullable: false), + CreationDateTime = table.Column(type: "timestamp with time zone", nullable: false), + ExpirationDateTime = table.Column(type: "timestamp with time zone", nullable: true), + ClickLimit = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShortUrls", x => x.Id); + }); + + 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), + HashedPassword = table.Column(type: "text", nullable: false), + RegisterDate = table.Column(type: "timestamp with time zone", nullable: false), + LastLoginDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ClickInfos"); + + migrationBuilder.DropTable( + name: "ShortUrls"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/URL-Shortener/Migrations/AppDbContextModelSnapshot.cs b/URL-Shortener/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..960df17 --- /dev/null +++ b/URL-Shortener/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,113 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using URL_Shortener; + +#nullable disable + +namespace URL_Shortener.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("URL_Shortener.Models.ClickInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClickDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ShortUrlId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ClickInfos"); + }); + + modelBuilder.Entity("URL_Shortener.Models.ShortUrl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClickLimit") + .HasColumnType("integer"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("OriginalUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalClickCount") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ShortUrls"); + }); + + modelBuilder.Entity("URL_Shortener.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("HashedPassword") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegisterDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/URL-Shortener/Models/ClickInfo.cs b/URL-Shortener/Models/ClickInfo.cs new file mode 100644 index 0000000..5668209 --- /dev/null +++ b/URL-Shortener/Models/ClickInfo.cs @@ -0,0 +1,9 @@ +namespace URL_Shortener.Models +{ + public class ClickInfo + { + public int Id { get; set; } + public int ShortUrlId { get; set; } + public DateTime ClickDateTime { get; set; } = DateTime.UtcNow; + } +} diff --git a/URL-Shortener/Models/ShortUrl.cs b/URL-Shortener/Models/ShortUrl.cs new file mode 100644 index 0000000..a9acb15 --- /dev/null +++ b/URL-Shortener/Models/ShortUrl.cs @@ -0,0 +1,14 @@ +namespace URL_Shortener.Models +{ + public class ShortUrl + { + public int Id { get; set; } + public int UserId { get; set; } + public string OriginalUrl { get; set; } = string.Empty; + public string ShortCode { get; set; } = string.Empty; + public int TotalClickCount { get; set; } + public DateTime CreationDateTime { get; set; } = DateTime.UtcNow; + public DateTime? ExpirationDateTime { get; set; } = null; + public int ClickLimit { get; set; } = 0; + } +} diff --git a/URL-Shortener/Models/User.cs b/URL-Shortener/Models/User.cs new file mode 100644 index 0000000..bc9767d --- /dev/null +++ b/URL-Shortener/Models/User.cs @@ -0,0 +1,12 @@ +namespace URL_Shortener.Models +{ + public class User + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string HashedPassword { get; set; } = string.Empty; + public DateTime RegisterDate { get; set; } + public DateTime LastLoginDate { get; set; } + } +} diff --git a/URL-Shortener/Program.cs b/URL-Shortener/Program.cs new file mode 100644 index 0000000..ab12a65 --- /dev/null +++ b/URL-Shortener/Program.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using URL_Shortener; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// Чтобы короткие коды генерировать +builder.Services.AddSingleton(); + +builder.Services.AddDbContext(options => +{ + var connString = builder.Configuration.GetConnectionString("DefaultConnection"); + options.UseNpgsql(connString, o => o.EnableRetryOnFailure()); +}); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseSwagger(); +app.UseSwaggerUI(); + +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.Migrate(); +} + +//app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/URL-Shortener/Properties/launchSettings.json b/URL-Shortener/Properties/launchSettings.json new file mode 100644 index 0000000..3f56c33 --- /dev/null +++ b/URL-Shortener/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5212" + }, + "https": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7180;http://localhost:5212" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/URL-Shortener/Services/UserService.cs b/URL-Shortener/Services/UserService.cs new file mode 100644 index 0000000..da27fe2 --- /dev/null +++ b/URL-Shortener/Services/UserService.cs @@ -0,0 +1,10 @@ +namespace URL_Shortener.Services +{ + public class UserService + { + public async Task Register(string userName, string email, string hashedPassword) + { + throw new NotImplementedException(); + } + } +} diff --git a/URL-Shortener/URL-Shortener.csproj b/URL-Shortener/URL-Shortener.csproj new file mode 100644 index 0000000..7699adf --- /dev/null +++ b/URL-Shortener/URL-Shortener.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + URL_Shortener + 8eedc3e1-0d87-47c1-9bd3-32550f156e73 + Linux + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/URL-Shortener/URL-Shortener.http b/URL-Shortener/URL-Shortener.http new file mode 100644 index 0000000..9fcbfa3 --- /dev/null +++ b/URL-Shortener/URL-Shortener.http @@ -0,0 +1,6 @@ +@URL_Shortener_HostAddress = http://localhost:5212 + +GET {{URL_Shortener_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/URL-Shortener/appsettings.json b/URL-Shortener/appsettings.json new file mode 100644 index 0000000..a55381e --- /dev/null +++ b/URL-Shortener/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Host=db;Database=appdb;Username=app;Password=app" + } +} diff --git a/URL-Shortener/dotnet-tools.json b/URL-Shortener/dotnet-tools.json new file mode 100644 index 0000000..ce0aab7 --- /dev/null +++ b/URL-Shortener/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.7", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d76c1c6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +services: + api: + build: . + ports: + - "5000:8080" + depends_on: + - db + environment: + ConnectionStrings__DefaultConnection: Host=${DB_HOST};Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD} + + db: + image: postgres:16 + restart: always + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + ports: + - "5432:5432" + volumes: + - pg_data:/var/lib/postgresql/data + +volumes: + pg_data: \ No newline at end of file