Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/reusable-developer-portal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Reusable Developer Portal

on:
workflow_call:
inputs:
dotnet-version:
description: .NET SDK version used for OpenAPI generation
required: false
default: "8.0.x"
type: string
node-version:
description: Node.js version used for Redoc CLI
required: false
default: "24.x"
type: string

permissions:
contents: read

env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages

jobs:
developer-portal:
name: Generate Developer Portal
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ inputs.dotnet-version }}
cache: true
cache-dependency-path: |
backend/Taskdeck.sln
backend/**/*.csproj

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}

- name: Generate OpenAPI spec
shell: pwsh
run: ./scripts/ci/generate-openapi-artifact.ps1 -OutputPath "artifacts/openapi/taskdeck-api.json"

- name: Generate Redoc static HTML
run: |
npx @redocly/cli build-docs artifacts/openapi/taskdeck-api.json \
--output artifacts/developer-portal/index.html \
--title "Taskdeck API Reference"

- name: Copy integration docs
run: |
cp -r docs/api/ artifacts/developer-portal/guides/

- name: Upload developer portal artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: developer-portal
path: artifacts/developer-portal/
if-no-files-found: warn
39 changes: 39 additions & 0 deletions backend/src/Taskdeck.Api/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ namespace Taskdeck.Api.Controllers;
public record ChangePasswordRequest(Guid UserId, string CurrentPassword, string NewPassword);
public record ExchangeCodeRequest(string Code);

/// <summary>
/// Authentication endpoints — register, login, change password, and GitHub OAuth flow.
/// All endpoints return a JWT token on successful authentication.
/// </summary>
[ApiController]
[Route("api/auth")]
[Produces("application/json")]
public class AuthController : ControllerBase
{
private readonly AuthenticationService _authService;
Expand All @@ -34,9 +39,20 @@ public AuthController(AuthenticationService authService, GitHubOAuthSettings git
_gitHubOAuthSettings = gitHubOAuthSettings;
}

/// <summary>
/// Authenticate with username/email and password. Returns a JWT token.
/// </summary>
/// <param name="dto">Login credentials.</param>
/// <returns>JWT token and user profile.</returns>
/// <response code="200">Login successful — JWT token returned.</response>
/// <response code="401">Invalid credentials.</response>
/// <response code="429">Rate limit exceeded.</response>
[HttpPost("login")]
[SuppressModelStateValidation]
[EnableRateLimiting(RateLimitingPolicyNames.AuthPerIp)]
[ProducesResponseType(typeof(AuthResultDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status429TooManyRequests)]
public async Task<IActionResult> Login([FromBody] LoginDto? dto)
{
if (dto is null
Expand All @@ -52,16 +68,39 @@ public async Task<IActionResult> Login([FromBody] LoginDto? dto)
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}

/// <summary>
/// Register a new user account. Returns a JWT token.
/// </summary>
/// <param name="dto">Registration details: username, email, password.</param>
/// <returns>JWT token and user profile.</returns>
/// <response code="200">Registration successful — JWT token returned.</response>
/// <response code="400">Validation error (e.g., duplicate username/email).</response>
/// <response code="429">Rate limit exceeded.</response>
[HttpPost("register")]
[EnableRateLimiting(RateLimitingPolicyNames.AuthPerIp)]
[ProducesResponseType(typeof(AuthResultDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status429TooManyRequests)]
public async Task<IActionResult> Register([FromBody] CreateUserDto dto)
{
var result = await _authService.RegisterAsync(dto);
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}

/// <summary>
/// Change the password for an existing user.
/// </summary>
/// <param name="request">Current and new password.</param>
/// <response code="204">Password changed successfully.</response>
/// <response code="400">Validation error.</response>
/// <response code="401">Current password is incorrect.</response>
/// <response code="429">Rate limit exceeded.</response>
[HttpPost("change-password")]
[EnableRateLimiting(RateLimitingPolicyNames.AuthPerIp)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status429TooManyRequests)]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
{
var result = await _authService.ChangePasswordAsync(request.UserId, request.CurrentPassword, request.NewPassword);
Expand Down
60 changes: 60 additions & 0 deletions backend/src/Taskdeck.Api/Controllers/BoardsController.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Taskdeck.Api.Contracts;
using Taskdeck.Api.Extensions;
using Taskdeck.Application.DTOs;
using Taskdeck.Application.Interfaces;
using Taskdeck.Application.Services;

namespace Taskdeck.Api.Controllers;

/// <summary>
/// Manage boards for the authenticated user. Boards are the top-level container
/// for columns, cards, and labels.
/// </summary>
[ApiController]
[Authorize]
[Route("api/[controller]")]
[Produces("application/json")]
public class BoardsController : AuthenticatedControllerBase
{
private readonly BoardService _boardService;
Expand All @@ -19,7 +25,17 @@ public BoardsController(BoardService boardService, IUserContext userContext) : b
_boardService = boardService;
}

/// <summary>
/// List boards accessible to the current user.
/// </summary>
/// <param name="search">Optional text filter applied to board names.</param>
/// <param name="includeArchived">When true, archived boards are included in the results.</param>
/// <returns>A list of boards matching the criteria.</returns>
/// <response code="200">Returns the list of boards.</response>
/// <response code="401">Authentication required.</response>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<BoardDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetBoards([FromQuery] string? search, [FromQuery] bool includeArchived = false)
{
if (!TryGetCurrentUserId(out var userId, out var errorResult))
Expand All @@ -29,7 +45,18 @@ public async Task<IActionResult> GetBoards([FromQuery] string? search, [FromQuer
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}

/// <summary>
/// Get a board by ID, including its columns.
/// </summary>
/// <param name="id">The board identifier.</param>
/// <returns>The board detail including columns.</returns>
/// <response code="200">Returns the board detail.</response>
/// <response code="401">Authentication required.</response>
/// <response code="404">Board not found or not accessible.</response>
[HttpGet("{id}")]
[ProducesResponseType(typeof(BoardDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetBoard(Guid id)
{
if (!TryGetCurrentUserId(out var userId, out var errorResult))
Expand All @@ -39,7 +66,18 @@ public async Task<IActionResult> GetBoard(Guid id)
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}

/// <summary>
/// Create a new board.
/// </summary>
/// <param name="dto">Board creation parameters.</param>
/// <returns>The newly created board.</returns>
/// <response code="201">Board created successfully.</response>
/// <response code="400">Validation error in the request body.</response>
/// <response code="401">Authentication required.</response>
[HttpPost]
[ProducesResponseType(typeof(BoardDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> CreateBoard([FromBody] CreateBoardDto dto)
{
if (!TryGetCurrentUserId(out var userId, out var errorResult))
Expand All @@ -51,7 +89,19 @@ public async Task<IActionResult> CreateBoard([FromBody] CreateBoardDto dto)
: result.ToErrorActionResult();
}

/// <summary>
/// Update an existing board.
/// </summary>
/// <param name="id">The board identifier.</param>
/// <param name="dto">Fields to update (all optional).</param>
/// <returns>The updated board.</returns>
/// <response code="200">Board updated successfully.</response>
/// <response code="401">Authentication required.</response>
/// <response code="404">Board not found or not accessible.</response>
[HttpPut("{id}")]
[ProducesResponseType(typeof(BoardDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateBoard(Guid id, [FromBody] UpdateBoardDto dto)
{
if (!TryGetCurrentUserId(out var userId, out var errorResult))
Expand All @@ -61,7 +111,17 @@ public async Task<IActionResult> UpdateBoard(Guid id, [FromBody] UpdateBoardDto
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}

/// <summary>
/// Delete a board.
/// </summary>
/// <param name="id">The board identifier.</param>
/// <response code="204">Board deleted successfully.</response>
/// <response code="401">Authentication required.</response>
/// <response code="404">Board not found or not accessible.</response>
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteBoard(Guid id)
{
if (!TryGetCurrentUserId(out var userId, out var errorResult))
Expand Down
Loading
Loading