diff --git a/README.md b/README.md
index 59a03be..43f059f 100644
--- a/README.md
+++ b/README.md
@@ -18,3 +18,20 @@ For more information on contributing, please visit the [contributor guide](./CON
## Contributors:
- [Akshay Hosur](https://github.com/akshay-online)
+
+## Web Application
+
+A new ASP.NET Core project (`ADOGenerator.Web`) exposes minimal API endpoints that leverage the existing services to create projects or generate template artifacts. This application can be deployed to Azure App Service.
+
+The web app also serves a simple UI that lets you choose a template and create projects.
+
+### Running locally
+
+```bash
+cd src/ADOGenerator.Web
+dotnet run
+```
+
+### Deploying to Azure
+
+Publish the project and deploy the generated output to an Azure App Service instance using the Azure portal or the `az webapp` CLI commands.
diff --git a/src/ADOGenerator.Web/ADOGenerator.Web.csproj b/src/ADOGenerator.Web/ADOGenerator.Web.csproj
new file mode 100644
index 0000000..479819a
--- /dev/null
+++ b/src/ADOGenerator.Web/ADOGenerator.Web.csproj
@@ -0,0 +1,18 @@
+
+
+ net8.0
+ disable
+ enable
+
+
+
+
+
+
+
+
+ Templates\%(RecursiveDir)%(Filename)%(Extension)
+ PreserveNewest
+
+
+
diff --git a/src/ADOGenerator.Web/Controllers/HomeController.cs b/src/ADOGenerator.Web/Controllers/HomeController.cs
new file mode 100644
index 0000000..0b1e418
--- /dev/null
+++ b/src/ADOGenerator.Web/Controllers/HomeController.cs
@@ -0,0 +1,26 @@
+using ADOGenerator.IServices;
+using ADOGenerator.Web.ViewModels;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using System.Linq;
+
+namespace ADOGenerator.Web.Controllers
+{
+ public class HomeController : Controller
+ {
+ private readonly ITemplateService _templateService;
+
+ public HomeController(ITemplateService templateService)
+ {
+ _templateService = templateService;
+ }
+
+ public IActionResult Index()
+ {
+ var vm = new ProjectViewModel();
+ vm.TemplateOptions = _templateService.GetAvailableTemplates()
+ .Select(t => new SelectListItem { Text = t.Name, Value = t.TemplateFolder });
+ return View(vm);
+ }
+ }
+}
diff --git a/src/ADOGenerator.Web/Controllers/ProjectsController.cs b/src/ADOGenerator.Web/Controllers/ProjectsController.cs
new file mode 100644
index 0000000..608e7cd
--- /dev/null
+++ b/src/ADOGenerator.Web/Controllers/ProjectsController.cs
@@ -0,0 +1,62 @@
+using ADOGenerator.IServices;
+using ADOGenerator.Models;
+using ADOGenerator.Web.ViewModels;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ADOGenerator.Web.Controllers
+{
+ [Route("api/[controller]")]
+ [ApiController]
+ public class ProjectsController : Controller
+ {
+ private readonly IProjectService _projectService;
+ private readonly ITemplateService _templateService;
+
+ public ProjectsController(IProjectService projectService, ITemplateService templateService)
+ {
+ _projectService = projectService;
+ _templateService = templateService;
+ }
+
+ [HttpPost("create")]
+ public IActionResult Create([FromBody] ProjectViewModel request)
+ {
+ var model = new Project
+ {
+ id = Guid.NewGuid().ToString(),
+ accountName = request.Organization,
+ accessToken = request.AccessToken,
+ adoAuthScheme = request.AuthScheme,
+ ProjectName = request.ProjectName,
+ selectedTemplateFolder = request.TemplateFolder,
+ TemplateName = request.TemplateName,
+ isExtensionNeeded = request.InstallExtensions,
+ isAgreeTerms = request.InstallExtensions
+ };
+ bool created = _projectService.CreateProjectEnvironment(model);
+ return Ok(new { Success = created });
+ }
+
+ [HttpPost("artifacts")]
+ public IActionResult Artifacts([FromBody] ArtifactRequest request)
+ {
+ var model = new Project
+ {
+ id = Guid.NewGuid().ToString(),
+ accountName = request.Organization,
+ ProjectName = request.ProjectName,
+ ProjectId = request.ProjectId,
+ accessToken = request.AccessToken,
+ adoAuthScheme = request.AuthScheme
+ };
+ var result = _templateService.GenerateTemplateArtifacts(model);
+ if (result.Item1)
+ {
+ return Ok(new { Template = result.Item2, Location = result.Item3 });
+ }
+ return BadRequest(new { Message = "Artifact generation failed" });
+ }
+ }
+
+ public record ArtifactRequest(string Organization, string ProjectName, string ProjectId, string AccessToken, string AuthScheme);
+}
diff --git a/src/ADOGenerator.Web/Controllers/TemplatesController.cs b/src/ADOGenerator.Web/Controllers/TemplatesController.cs
new file mode 100644
index 0000000..aa5acf9
--- /dev/null
+++ b/src/ADOGenerator.Web/Controllers/TemplatesController.cs
@@ -0,0 +1,25 @@
+using ADOGenerator.IServices;
+using Microsoft.AspNetCore.Mvc;
+using System.Linq;
+
+namespace ADOGenerator.Web.Controllers
+{
+ [Route("api/[controller]")]
+ [ApiController]
+ public class TemplatesController : Controller
+ {
+ private readonly ITemplateService _templateService;
+ public TemplatesController(ITemplateService templateService)
+ {
+ _templateService = templateService;
+ }
+
+ [HttpGet]
+ public IActionResult Get()
+ {
+ var list = _templateService.GetAvailableTemplates()
+ .Select(t => new { t.Name, t.TemplateFolder, t.Description });
+ return Ok(list);
+ }
+ }
+}
diff --git a/src/ADOGenerator.Web/Program.cs b/src/ADOGenerator.Web/Program.cs
new file mode 100644
index 0000000..9216645
--- /dev/null
+++ b/src/ADOGenerator.Web/Program.cs
@@ -0,0 +1,35 @@
+using ADOGenerator.IServices;
+using ADOGenerator.Models;
+using ADOGenerator.Services;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add configuration and services
+builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+builder.Services.AddControllersWithViews();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+var app = builder.Build();
+
+app.UseStaticFiles();
+app.UseRouting();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseAuthorization();
+
+app.MapControllerRoute(
+ name: "default",
+ pattern: "{controller=Home}/{action=Index}/{id?}");
+
+app.Run();
diff --git a/src/ADOGenerator.Web/ViewModels/ProjectViewModel.cs b/src/ADOGenerator.Web/ViewModels/ProjectViewModel.cs
new file mode 100644
index 0000000..e0b28e0
--- /dev/null
+++ b/src/ADOGenerator.Web/ViewModels/ProjectViewModel.cs
@@ -0,0 +1,18 @@
+using Microsoft.AspNetCore.Mvc.Rendering;
+using System.Collections.Generic;
+
+namespace ADOGenerator.Web.ViewModels
+{
+ public class ProjectViewModel
+ {
+ public string Organization { get; set; }
+ public string ProjectName { get; set; }
+ public string TemplateName { get; set; }
+ public string TemplateFolder { get; set; }
+ public string AccessToken { get; set; }
+ public string AuthScheme { get; set; } = "pat";
+ public bool InstallExtensions { get; set; } = true;
+ public IEnumerable TemplateOptions { get; set; } = new List();
+ public string ResultMessage { get; set; }
+ }
+}
diff --git a/src/ADOGenerator.Web/Views/Home/Index.cshtml b/src/ADOGenerator.Web/Views/Home/Index.cshtml
new file mode 100644
index 0000000..8d4462e
--- /dev/null
+++ b/src/ADOGenerator.Web/Views/Home/Index.cshtml
@@ -0,0 +1,42 @@
+@model ADOGenerator.Web.ViewModels.ProjectViewModel
+
+
+
+
+ ADO Generator
+
+
+
+Create Azure DevOps Project
+
+@if (Model.ResultMessage != null)
+{
+ @Model.ResultMessage
+}
+
+
diff --git a/src/ADOGenerator.Web/Views/_ViewImports.cshtml b/src/ADOGenerator.Web/Views/_ViewImports.cshtml
new file mode 100644
index 0000000..a757b41
--- /dev/null
+++ b/src/ADOGenerator.Web/Views/_ViewImports.cshtml
@@ -0,0 +1 @@
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
diff --git a/src/ADOGenerator.Web/Views/_ViewStart.cshtml b/src/ADOGenerator.Web/Views/_ViewStart.cshtml
new file mode 100644
index 0000000..9d06493
--- /dev/null
+++ b/src/ADOGenerator.Web/Views/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = null;
+}
diff --git a/src/ADOGenerator.Web/appsettings.json b/src/ADOGenerator.Web/appsettings.json
new file mode 100644
index 0000000..ec5d773
--- /dev/null
+++ b/src/ADOGenerator.Web/appsettings.json
@@ -0,0 +1,46 @@
+{
+ "AppSettings": {
+ "webpages:Version": "2.0.0.0",
+ "webpages:Enabled": "false",
+ "PreserveLoginUrl": "true",
+ "ClientValidationEnabled": "true",
+ "UnobtrusiveJavaScriptEnabled": "true",
+ "ProjectCreationVersion": "4.1",
+ "ProjectPropertyVersion": "4.1-preview",
+ "RepoVersion": "4.1",
+ "BuildVersion": "4.1",
+ "ReleaseVersion": "4.1",
+ "WikiVersion": "4.1",
+ "BoardVersion": "4.1",
+ "WorkItemsVersion": "4.1",
+ "QueriesVersion": "4.1",
+ "EndPointVersion": "4.1-preview.1",
+ "ExtensionVersion": "4.1",
+ "DashboardVersion": "4.1-preview.2",
+ "AgentQueueVersion": "4.1-preview",
+ "GetSourceCodeVersion": "4.1-preview",
+ "TestPlanVersion": "5.0",
+ "DefaultHost": "https://dev.azure.com/",
+ "ReleaseHost": "https://vsrm.dev.azure.com/",
+ "ExtensionHost": "https://extmgmt.dev.azure.com/",
+ "GetRelease": "4.1-preview.3",
+ "BaseAddress": "https://app.vssps.visualstudio.com/",
+ "GraphAPIHost": "https://vssps.dev.azure.com/",
+ "ProjectProperties": "4.1-preview.1",
+ "DefaultTemplate": "SmartHotel360",
+ "DeloymentGroup": "4.1-preview.1",
+ "GraphApiVersion": "4.1-preview.1",
+ "VariableGroupsApiVersion": "5.0-preview.1",
+ "AnalyticsKey": "",
+ "clientId": "",
+ "tenantId": "",
+ "scopes": ""
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "System": "Warning",
+ "Microsoft": "Warning"
+ }
+ }
+}
diff --git a/src/ADOGenerator/IServices/ITemplateService.cs b/src/ADOGenerator/IServices/ITemplateService.cs
index 4e497d7..59c822f 100644
--- a/src/ADOGenerator/IServices/ITemplateService.cs
+++ b/src/ADOGenerator/IServices/ITemplateService.cs
@@ -7,5 +7,6 @@ public interface ITemplateService
bool AnalyzeProject(Project model);
bool CheckTemplateExists(Project model);
(bool,string,string) GenerateTemplateArtifacts(Project model);
+ IEnumerable GetAvailableTemplates();
}
}
diff --git a/src/ADOGenerator/Services/ProjectService.cs b/src/ADOGenerator/Services/ProjectService.cs
index dab8f5a..c14ecdb 100644
--- a/src/ADOGenerator/Services/ProjectService.cs
+++ b/src/ADOGenerator/Services/ProjectService.cs
@@ -183,7 +183,7 @@ public async Task> SelectProject(string accessToken, HttpResponseMe
public string GetJsonFilePath(bool IsPrivate, string TemplateFolder, string TemplateName, string FileName = "")
{
string filePath = string.Empty;
- filePath = string.Format(Path.Combine(Directory.GetCurrentDirectory(), "Templates", TemplateName, FileName));
+ filePath = Path.Combine(AppContext.BaseDirectory, "Templates", TemplateName, FileName);
return filePath;
}
@@ -366,7 +366,7 @@ public bool CreateProjectEnvironment(Project model)
Console.WriteLine("Error in reading project template file:" + ex.Message);
}
//create team project
- string jsonProject = model.ReadJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "Templates", "CreateProject.json"));
+ string jsonProject = model.ReadJsonFile(Path.Combine(AppContext.BaseDirectory, "Templates", "CreateProject.json"));
jsonProject = jsonProject.Replace("$projectName$", model.ProjectName).Replace("$processTemplateId$", processTemplateId);
Projects proj = new Projects(_projectCreationVersion);
@@ -2260,7 +2260,7 @@ void CreateQueryAndWidgets(Project model, List listQueries, ADOConfigura
bool isFolderCreated = false;
if (!string.IsNullOrEmpty(teamName))
{
- string createQueryFolderJson = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "PreSetting", "CreateQueryFolder.json"));
+ string createQueryFolderJson = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "PreSetting", "CreateQueryFolder.json"));
createQueryFolderJson = createQueryFolderJson.Replace("$TeamName$", teamName);
QueryResponse createFolderResponse = _newobjQuery.CreateQuery(model.ProjectName, createQueryFolderJson);
isFolderCreated = createFolderResponse.id != null ? true : false;
@@ -2787,7 +2787,7 @@ string GetTemplateMessage(string TemplateName)
{
string groupDetails = "";
TemplateSelection.Templates templates = new TemplateSelection.Templates();
- string templatesPath = ""; templatesPath = Path.Combine(Directory.GetCurrentDirectory(), "Templates");
+ string templatesPath = Path.Combine(AppContext.BaseDirectory, "Templates");
if (File.Exists(templatesPath + "TemplateSetting.json"))
{
groupDetails = File.ReadAllText(templatesPath + "\\TemplateSetting.json");
@@ -2889,7 +2889,7 @@ void CreateVaribaleGroups(Project model, ADOConfiguration _variableGroups)
public bool WhereDoseTemplateBelongTo(string templatName)
{
- string privatePath = Path.Combine(Directory.GetCurrentDirectory(), "PrivateTemplates");
+ string privatePath = Path.Combine(AppContext.BaseDirectory, "PrivateTemplates");
string privateTemplate = Path.Combine(privatePath, templatName);
if (!Directory.Exists(privatePath))
diff --git a/src/ADOGenerator/Services/TemplateService.cs b/src/ADOGenerator/Services/TemplateService.cs
index 608c8b3..36eaeb7 100644
--- a/src/ADOGenerator/Services/TemplateService.cs
+++ b/src/ADOGenerator/Services/TemplateService.cs
@@ -2,10 +2,13 @@
using ADOGenerator.Models;
using ADOGenerator.Services;
using Microsoft.Extensions.Configuration;
+using Newtonsoft.Json;
using RestAPI.Extractor;
using RestAPI.ProjectsAndTeams;
using RestAPI;
using ADOGenerator;
+using System.IO;
+using System.Linq;
public class TemplateService : ITemplateService
{
@@ -101,6 +104,24 @@ public bool CheckTemplateExists(Project model)
}
}
+ public IEnumerable GetAvailableTemplates()
+ {
+ var templatesPath = Path.Combine(AppContext.BaseDirectory, "Templates", "TemplateSetting.json");
+ if (!File.Exists(templatesPath))
+ {
+ return Enumerable.Empty();
+ }
+
+ var json = File.ReadAllText(templatesPath);
+ var parsed = JsonConvert.DeserializeObject(json);
+ if (parsed?.GroupwiseTemplates == null)
+ {
+ return Enumerable.Empty();
+ }
+
+ return parsed.GroupwiseTemplates.SelectMany(g => g.Template);
+ }
+
private void LogAnalysisResults(Project model, ExtractorAnalysis analysis)
{
model.id.AddMessage(Environment.NewLine + "-------------------------------------------------------------------");