Huge work

This commit is contained in:
Karolis Kundrotas
2021-10-25 22:00:01 +03:00
parent c3bb8983ef
commit aff6f8df82
26 changed files with 578 additions and 68 deletions

View File

@@ -14,4 +14,8 @@
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.1" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project> </Project>

View File

@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace KTUSAPS.Data.Migrations namespace KTUSAPS.Data.Migrations
{ {
[DbContext(typeof(SAPSDataContext))] [DbContext(typeof(SAPSDataContext))]
[Migration("20210909173149_Initial")] [Migration("20211015122630_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -50,6 +50,9 @@ namespace KTUSAPS.Data.Migrations
.HasMaxLength(320) .HasMaxLength(320)
.HasColumnType("varchar(320)"); .HasColumnType("varchar(320)");
b.Property<int>("IssueTypeId")
.HasColumnType("int");
b.Property<bool>("Publishable") b.Property<bool>("Publishable")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@@ -62,9 +65,28 @@ namespace KTUSAPS.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IssueTypeId");
b.ToTable("Issues"); b.ToTable("Issues");
}); });
modelBuilder.Entity("KTUSAPS.Data.Model.IssueType", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("longtext");
b.Property<string>("NameEn")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("IssueTypes");
});
modelBuilder.Entity("KTUSAPS.Data.Model.PublishedFeedback", b => modelBuilder.Entity("KTUSAPS.Data.Model.PublishedFeedback", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -169,6 +191,17 @@ namespace KTUSAPS.Data.Migrations
b.ToTable("Votes"); b.ToTable("Votes");
}); });
modelBuilder.Entity("KTUSAPS.Data.Model.Issue", b =>
{
b.HasOne("KTUSAPS.Data.Model.IssueType", "IssueType")
.WithMany("Issues")
.HasForeignKey("IssueTypeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("IssueType");
});
modelBuilder.Entity("KTUSAPS.Data.Model.PublishedFeedback", b => modelBuilder.Entity("KTUSAPS.Data.Model.PublishedFeedback", b =>
{ {
b.HasOne("KTUSAPS.Data.Model.Issue", "Issue") b.HasOne("KTUSAPS.Data.Model.Issue", "Issue")
@@ -211,6 +244,11 @@ namespace KTUSAPS.Data.Migrations
b.Navigation("Problem"); b.Navigation("Problem");
}); });
modelBuilder.Entity("KTUSAPS.Data.Model.IssueType", b =>
{
b.Navigation("Issues");
});
modelBuilder.Entity("KTUSAPS.Data.Model.PublishedProblem", b => modelBuilder.Entity("KTUSAPS.Data.Model.PublishedProblem", b =>
{ {
b.Navigation("Votes"); b.Navigation("Votes");

View File

@@ -27,24 +27,19 @@ namespace KTUSAPS.Data.Migrations
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Issues", name: "IssueTypes",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(type: "int", nullable: false) Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserID = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: true) Name = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"), .Annotation("MySql:CharSet", "utf8mb4"),
Email = table.Column<string>(type: "varchar(320)", maxLength: 320, nullable: true) NameEn = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Publishable = table.Column<bool>(type: "tinyint(1)", nullable: false),
Solved = table.Column<bool>(type: "tinyint(1)", nullable: false),
Created = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Description = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4") .Annotation("MySql:CharSet", "utf8mb4")
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Issues", x => x.Id); table.PrimaryKey("PK_IssueTypes", x => x.Id);
}) })
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");
@@ -66,6 +61,35 @@ namespace KTUSAPS.Data.Migrations
}) })
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Issues",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserID = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Email = table.Column<string>(type: "varchar(320)", maxLength: 320, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Publishable = table.Column<bool>(type: "tinyint(1)", nullable: false),
Solved = table.Column<bool>(type: "tinyint(1)", nullable: false),
Created = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Description = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
IssueTypeId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Issues", x => x.Id);
table.ForeignKey(
name: "FK_Issues_IssueTypes_IssueTypeId",
column: x => x.IssueTypeId,
principalTable: "IssueTypes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "PublishedFeedbacks", name: "PublishedFeedbacks",
columns: table => new columns: table => new
@@ -147,6 +171,11 @@ namespace KTUSAPS.Data.Migrations
}) })
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Issues_IssueTypeId",
table: "Issues",
column: "IssueTypeId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_PublishedFeedbacks_IssueId", name: "IX_PublishedFeedbacks_IssueId",
table: "PublishedFeedbacks", table: "PublishedFeedbacks",
@@ -190,6 +219,9 @@ namespace KTUSAPS.Data.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Solutions"); name: "Solutions");
migrationBuilder.DropTable(
name: "IssueTypes");
} }
} }
} }

View File

@@ -48,6 +48,9 @@ namespace KTUSAPS.Data.Migrations
.HasMaxLength(320) .HasMaxLength(320)
.HasColumnType("varchar(320)"); .HasColumnType("varchar(320)");
b.Property<int>("IssueTypeId")
.HasColumnType("int");
b.Property<bool>("Publishable") b.Property<bool>("Publishable")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@@ -60,9 +63,28 @@ namespace KTUSAPS.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IssueTypeId");
b.ToTable("Issues"); b.ToTable("Issues");
}); });
modelBuilder.Entity("KTUSAPS.Data.Model.IssueType", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("longtext");
b.Property<string>("NameEn")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("IssueTypes");
});
modelBuilder.Entity("KTUSAPS.Data.Model.PublishedFeedback", b => modelBuilder.Entity("KTUSAPS.Data.Model.PublishedFeedback", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -167,6 +189,17 @@ namespace KTUSAPS.Data.Migrations
b.ToTable("Votes"); b.ToTable("Votes");
}); });
modelBuilder.Entity("KTUSAPS.Data.Model.Issue", b =>
{
b.HasOne("KTUSAPS.Data.Model.IssueType", "IssueType")
.WithMany("Issues")
.HasForeignKey("IssueTypeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("IssueType");
});
modelBuilder.Entity("KTUSAPS.Data.Model.PublishedFeedback", b => modelBuilder.Entity("KTUSAPS.Data.Model.PublishedFeedback", b =>
{ {
b.HasOne("KTUSAPS.Data.Model.Issue", "Issue") b.HasOne("KTUSAPS.Data.Model.Issue", "Issue")
@@ -209,6 +242,11 @@ namespace KTUSAPS.Data.Migrations
b.Navigation("Problem"); b.Navigation("Problem");
}); });
modelBuilder.Entity("KTUSAPS.Data.Model.IssueType", b =>
{
b.Navigation("Issues");
});
modelBuilder.Entity("KTUSAPS.Data.Model.PublishedProblem", b => modelBuilder.Entity("KTUSAPS.Data.Model.PublishedProblem", b =>
{ {
b.Navigation("Votes"); b.Navigation("Votes");

View File

@@ -22,7 +22,15 @@ namespace KTUSAPS.Data.Model
[MaxLength] [MaxLength]
public string Description { get; set; } public string Description { get; set; }
public PublishedProblem Problem { get; set; } public int IssueTypeId { get; set; }
public PublishedFeedback Feedback { get; set; } public virtual IssueType IssueType { get; set; }
public virtual PublishedProblem Problem { get; set; }
public virtual PublishedFeedback Feedback { get; set; }
public Issue()
{
Created = DateTime.Now;
}
} }
} }

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KTUSAPS.Data.Model
{
public class IssueType
{
public int Id { get; set; }
public string Name { get; set; }
public string NameEn { get; set; }
public virtual ICollection<Issue> Issues { get; set; }
}
}

View File

@@ -1,18 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ClassDiagram MajorVersion="1" MinorVersion="1"> <ClassDiagram MajorVersion="1" MinorVersion="1">
<Class Name="KTUSAPS.Data.Model.Issue"> <Class Name="KTUSAPS.Data.Model.Issue">
<Position X="0.5" Y="0.75" Width="1.5" /> <Position X="10.25" Y="1" Width="1.5" />
<TypeIdentifier> <TypeIdentifier>
<HashCode>AAACAAJAACAgAAAAAAACAAgIAAAAAAQAAAAAAAAAAAA=</HashCode> <HashCode>AIACAAJQACAgAAAAAAACAAgIAAAAAAQAAAAAAAAAAAA=</HashCode>
<FileName>Model\Issue.cs</FileName> <FileName>Model\Issue.cs</FileName>
</TypeIdentifier> </TypeIdentifier>
<ShowAsAssociation> <ShowAsAssociation>
<Property Name="Problem" /> <Property Name="Problem" />
<Property Name="Feedback" /> <Property Name="Feedback" />
<Property Name="IssueType" />
</ShowAsAssociation> </ShowAsAssociation>
</Class> </Class>
<Class Name="KTUSAPS.Data.Model.PublishedFeedback"> <Class Name="KTUSAPS.Data.Model.PublishedFeedback">
<Position X="2.75" Y="3.75" Width="1.5" /> <Position X="6.5" Y="3" Width="1.5" />
<TypeIdentifier> <TypeIdentifier>
<HashCode>AAECAAAAAAAAAAAACAIAAAAAAAAAAAQAEAAAAAAAAAA=</HashCode> <HashCode>AAECAAAAAAAAAAAACAIAAAAAAAAAAAQAEAAAAAAAAAA=</HashCode>
<FileName>Model\PublishedFeedback.cs</FileName> <FileName>Model\PublishedFeedback.cs</FileName>
@@ -22,9 +23,9 @@
</ShowAsAssociation> </ShowAsAssociation>
</Class> </Class>
<Class Name="KTUSAPS.Data.Model.PublishedProblem"> <Class Name="KTUSAPS.Data.Model.PublishedProblem">
<Position X="2.75" Y="0.75" Width="1.5" /> <Position X="4" Y="0.5" Width="1.5" />
<TypeIdentifier> <TypeIdentifier>
<HashCode>ACECABAAAAEAAAgAAAABAAAAEAAAQAQAEAAAAAAAAAA=</HashCode> <HashCode>ACECABAAAAEAAAgAAAABAAAAAAAAAAQAEAAAAAAAAAA=</HashCode>
<FileName>Model\PublishedProblem.cs</FileName> <FileName>Model\PublishedProblem.cs</FileName>
</TypeIdentifier> </TypeIdentifier>
<ShowAsAssociation> <ShowAsAssociation>
@@ -36,7 +37,7 @@
</ShowAsCollectionAssociation> </ShowAsCollectionAssociation>
</Class> </Class>
<Class Name="KTUSAPS.Data.Model.Solution"> <Class Name="KTUSAPS.Data.Model.Solution">
<Position X="6" Y="0.5" Width="1.5" /> <Position X="0.75" Y="0.75" Width="1.5" />
<TypeIdentifier> <TypeIdentifier>
<HashCode>AAACAAIABAAAAQAAAAAAAAAAAAAAAAQAAAAAAAAAAAA=</HashCode> <HashCode>AAACAAIABAAAAQAAAAAAAAAAAAAAAAQAAAAAAAAAAAA=</HashCode>
<FileName>Model\Solution.cs</FileName> <FileName>Model\Solution.cs</FileName>
@@ -46,7 +47,7 @@
</ShowAsAssociation> </ShowAsAssociation>
</Class> </Class>
<Class Name="KTUSAPS.Data.Model.Vote"> <Class Name="KTUSAPS.Data.Model.Vote">
<Position X="6" Y="2.5" Width="1.5" /> <Position X="3.5" Y="4.5" Width="1.5" />
<TypeIdentifier> <TypeIdentifier>
<HashCode>AAAAAAIAAAAAEAAAAAAAAAgAAAAAAAAAAAAAAAAAAAA=</HashCode> <HashCode>AAAAAAIAAAAAEAAAAAAAAAgAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Model\Vote.cs</FileName> <FileName>Model\Vote.cs</FileName>
@@ -55,5 +56,15 @@
<Property Name="Problem" /> <Property Name="Problem" />
</ShowAsAssociation> </ShowAsAssociation>
</Class> </Class>
<Class Name="KTUSAPS.Data.Model.IssueType">
<Position X="12.25" Y="5.25" Width="1.5" />
<TypeIdentifier>
<HashCode>AAECAAAAAAAAAAAAACAAAAQAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Model\IssueType.cs</FileName>
</TypeIdentifier>
<ShowAsCollectionAssociation>
<Property Name="Issues" />
</ShowAsCollectionAssociation>
</Class>
<Font Name="Segoe UI" Size="9" /> <Font Name="Segoe UI" Size="9" />
</ClassDiagram> </ClassDiagram>

View File

@@ -22,6 +22,11 @@ namespace KTUSAPS.Data.Model
public DateTime Created { get; set; } public DateTime Created { get; set; }
public int? IssueId { get; set; } public int? IssueId { get; set; }
public Issue Issue { get; set; } public virtual Issue Issue { get; set; }
public PublishedFeedback()
{
Created = DateTime.Now;
}
} }
} }

View File

@@ -18,20 +18,19 @@ namespace KTUSAPS.Data.Model
[Required] [Required]
[MaxLength] [MaxLength]
public string ProblemEn { get; set; } public string ProblemEn { get; set; }
[MaxLength]
public string ResponseLt { get; set; }
[MaxLength]
public string ResponseEn { get; set; }
public DateTime Created { get; set; } public DateTime Created { get; set; }
public int? IssueId { get; set; } public int? IssueId { get; set; }
public Issue Issue { get; set; } public virtual Issue Issue { get; set; }
public int? SolutionId { get; set; } public int? SolutionId { get; set; }
public Solution Solution { get; set; } public virtual Solution Solution { get; set; }
public ICollection<Vote> Votes { get; set; } public ICollection<Vote> Votes { get; set; }
public PublishedProblem()
{
Created = DateTime.Now;
}
} }
} }

View File

@@ -14,7 +14,12 @@ namespace KTUSAPS.Data.Model
public string SolutionEn { get; set; } public string SolutionEn { get; set; }
public PublishedProblem Problem { get; set; } public virtual PublishedProblem Problem { get; set; }
public DateTime Created { get; set; } public DateTime Created { get; set; }
public Solution()
{
Created = DateTime.Now;
}
} }
} }

View File

@@ -12,7 +12,7 @@ namespace KTUSAPS.Data.Model
[MaxLength(64)] [MaxLength(64)]
public string UserId { get; set; } public string UserId { get; set; }
public int ProblemId { get; set; } public int ProblemId { get; set; }
public PublishedProblem Problem { get; set; } public virtual PublishedProblem Problem { get; set; }
} }
} }

View File

@@ -26,6 +26,7 @@ namespace KTUSAPS.Data
} }
} }
public DbSet<IssueType> IssueTypes { get; set; }
public DbSet<Issue> Issues { get; set; } public DbSet<Issue> Issues { get; set; }
public DbSet<PublishedFeedback> PublishedFeedbacks { get; set; } public DbSet<PublishedFeedback> PublishedFeedbacks { get; set; }
public DbSet<PublishedProblem> PublishedProblems { get; set; } public DbSet<PublishedProblem> PublishedProblems { get; set; }

View File

@@ -19,6 +19,15 @@
</span> </span>
</div> </div>
</div> </div>
<div v-if="$store.getters['auth/isExpiringSoon']" class="container">
<div class="alert alert-warning">
<h4 class="alert-heading">Greitai baisis jūsų sesija</h4>
<span>
Po {{ expiresIn }} baigsis jūsų sesija.
<a :href="$store.getters['auth/loginUrl']">Pratęsti sesija.</a>
</span>
</div>
</div>
<router-view /> <router-view />
</template> </template>
@@ -30,6 +39,12 @@ export default {
components: { components: {
NavMenu, NavMenu,
}, },
data() {
return {
expiresIn: '',
interval: null,
}
},
created() { created() {
this.$store.dispatch('auth/initialize') this.$store.dispatch('auth/initialize')
}, },
@@ -43,6 +58,26 @@ export default {
return location.protocol !== 'https:' return location.protocol !== 'https:'
}, },
}, },
methods: {
updateExpiry() {
const totalSeconds = Math.floor(
(this.$store.getters['auth/expires'] - new Date()) / 1000
)
const seconds = totalSeconds % 60
const minutes = Math.floor(totalSeconds / 60)
if (minutes) {
this.expiresIn = `${minutes} min. ir ${seconds} sek.`
} else {
this.expiresIn = `${seconds} sek.`
}
},
},
mounted() {
this.interval = setInterval(this.updateExpiry, 1000)
},
beforeUnmount() {
clearInterval(this.interval)
},
} }
</script> </script>

View File

@@ -27,6 +27,9 @@
>Pagrindinis</router-link >Pagrindinis</router-link
> >
</li> </li>
<li class="nav-item">
<a href="/swagger" class="nav-link">Swagger UI</a>
</li>
</ul> </ul>
<div class="navbar-nav"> <div class="navbar-nav">
<span v-if="$store.getters['auth/isValid']" class="navbar-text" <span v-if="$store.getters['auth/isValid']" class="navbar-text"

View File

@@ -29,7 +29,7 @@
</table> </table>
<h3>Techninė duomenų reprezentacija</h3> <h3>Techninė duomenų reprezentacija</h3>
<a <a
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens" href="https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens"
>Dokumentacija apie laukų reikšmes</a >Dokumentacija apie laukų reikšmes</a
> >
<pre>{{ $store.state.auth.tokenData }}</pre> <pre>{{ $store.state.auth.tokenData }}</pre>
@@ -92,7 +92,7 @@ export default {
serverVerify() { serverVerify() {
this.verificationResult = null this.verificationResult = null
axios axios
.get('/test/authed', { .get('/api/AuthMetadata/authed', {
headers: { Authorization: `Bearer ${this.$store.state.auth.token}` }, headers: { Authorization: `Bearer ${this.$store.state.auth.token}` },
}) })
.then(response => { .then(response => {

View File

@@ -1,7 +1,7 @@
import { createStore, createLogger } from "vuex"; import { createStore, createLogger } from 'vuex'
import auth from "./modules/auth"; import auth from './modules/auth'
const debug = process.env.NODE_ENV !== "production"; const debug = process.env.NODE_ENV !== 'production'
export default createStore({ export default createStore({
modules: { modules: {
@@ -9,4 +9,4 @@ export default createStore({
}, },
strict: debug, strict: debug,
plugins: debug ? [createLogger()] : [], plugins: debug ? [createLogger()] : [],
}); })

View File

@@ -52,7 +52,15 @@ const getters = {
}, },
userId(state, getters) { userId(state, getters) {
if (!getters.isValid) return null if (!getters.isValid) return null
return state.tokenData.email return state.tokenData.sub
},
isExpiringSoon(state, getters) {
if (!getters.isValid) return false
return true
},
expires(state, getters) {
if (!getters.isValid) return 0
return new Date(state.tokenData.exp * 1000)
}, },
loginUrl(state, getters) { loginUrl(state, getters) {
if (!getters.isReady) return null if (!getters.isReady) return null

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System; using System;
@@ -10,15 +11,39 @@ namespace KTUSAPS.Controllers
{ {
[Route("api/[controller]")] [Route("api/[controller]")]
[ApiController] [ApiController]
[Produces("application/json")]
public class AuthMetadataController : ControllerBase public class AuthMetadataController : ControllerBase
{ {
public class AuthMetadata
{
public string ClientId { get; set; }
public string Authority { get; set; }
public string Tenant { get; set; }
}
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
public AuthMetadataController(IConfiguration configuration) public AuthMetadataController(IConfiguration configuration)
{ {
_configuration = configuration; _configuration = configuration;
} }
/// <summary>
/// Get authethication metadata needed to obtain token.
/// </summary>
/// <returns></returns>
[HttpGet] [HttpGet]
public object Index() => new { ClientId = _configuration["ClientId"], Authority = _configuration["Authority"], Tenant = _configuration["Tenant"] }; public AuthMetadata Index() => new AuthMetadata { ClientId = _configuration["ClientId"], Authority = _configuration["Authority"], Tenant = _configuration["Tenant"] };
/// <summary>
/// Returns true is provided token is valid, else throws exception
/// </summary>
/// <returns></returns>
/// <response code="200">Provided token is correct.</response>
/// <response code="401">No valid token provided.</response>
[Authorize]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status200OK)]
[HttpGet("Authed")]
public bool IsAuthed() => true;
} }
} }

View File

@@ -0,0 +1,96 @@
using KTUSAPS.Data.Model;
using KTUSAPS.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace KTUSAPS.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class IssueController : ControllerBase
{
private readonly Data.SAPSDataContext dataContext;
public IssueController(Data.SAPSDataContext dataContext)
{
this.dataContext = dataContext;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public IEnumerable<Issue> GetIssues()
{
return dataContext.Issues;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Issue>> CreateIssueAsync([FromBody] Issue issueToCreate)
{
if (issueToCreate == null)
return BadRequest("No data provided for object to be created.");
if (issueToCreate.Id != default)
return BadRequest("Id has been set on create request, please do not do that, set id to 0 or ommit it.");
if (issueToCreate.IssueTypeId == default)
return BadRequest("No typeId has been specified");
if (issueToCreate.Problem != null && issueToCreate.Feedback != null && issueToCreate.IssueType != null)
return BadRequest("Do not privide navigation property values.");
// TODO: Enable next line and make thoes two fields come from user identity
//if (issueToCreate.UserID != default || issueToCreate.Email != default)
// return BadRequest("Do not provide indentity values.");
var createdValue = await dataContext.AddAsync(issueToCreate);
await dataContext.SaveChangesAsync();
var url = Url.ActionLink(action: nameof(GetIssue), values: new { Id = createdValue.Entity.Id });
return Created(url, createdValue.Entity);
}
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Issue> GetIssue(int id)
{
var issue = dataContext.Issues.AsQueryable().Where(i => i.Id == id).FirstOrDefault();
if(issue == default)
return NotFound();
return Ok(issue);
}
[HttpPatch("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Issue>> UpdateIssueAsync(int id, [FromBody] Issue issue)
{
var databaseIssue = dataContext.Issues.AsQueryable().Where(i => i.Id == id).FirstOrDefault();
if (databaseIssue == default)
return NotFound();
var eIssue = dataContext.Attach(databaseIssue);
eIssue.MovePropertyDataWhiteList(issue, new string[] {
nameof(databaseIssue.Description),
nameof(databaseIssue.IssueTypeId),
nameof(databaseIssue.Publishable)
});
await dataContext.SaveChangesAsync();
return Ok(eIssue.Entity);
}
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteIssueAsync(int id)
{
var issue = dataContext.Issues.AsQueryable().Where(i => i.Id == id).FirstOrDefault();
if (issue == default)
return NotFound();
dataContext.Issues.Remove(issue);
await dataContext.SaveChangesAsync();
return NoContent();
}
}
}

View File

@@ -1,26 +0,0 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace KTUSAPS.Controllers
{
[Route("[controller]")]
[Authorize]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
public object[] Index()
{
return HttpContext.User.Claims.Select(x => new { Name = x.Type, Value= x.Value }).ToArray();
}
[HttpGet("authed")]
public bool IsAuthed() => true;
}
}

View File

@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace KTUSAPS.Extensions
{
public static class EntityEntryExtensions
{
public static void MovePropertyDataBlackList(this EntityEntry target, object source, IEnumerable<string> blacklistedProprties)
{
MovePropertyData(target, source, (prop) => blacklistedProprties.Contains(prop.Metadata.Name));
}
public static void MovePropertyDataWhiteList(this EntityEntry target, object source, IEnumerable<string> whitelistedProprties)
{
MovePropertyData(target, source, (prop) => !whitelistedProprties.Contains(prop.Metadata.Name));
}
public static void MovePropertyData(this EntityEntry target, object source, Func<PropertyEntry, bool> isBlacklisted)
{
foreach (var prop in target.Properties)
{
if (isBlacklisted(prop))
continue;
var propertyInfo = prop.Metadata.PropertyInfo;
var newValue = propertyInfo.GetValue(source);
if (!newValue.isDefault()) {
prop.CurrentValue = newValue;
}
}
}
private static bool isDefault(this object value)
{
if(value == default)
return true;
if (value is int || value is long)
return (int)value == default(int);
return false;
}
}
}

View File

@@ -11,8 +11,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.10" />
<PackageReference Include="VueCliMiddleware" Version="3.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.2.1" />
<PackageReference Include="VueCliMiddleware" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -22,6 +24,10 @@
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\KTUSAPS.Data\KTUSAPS.Data.csproj" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed --> <!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true"> <Exec Command="node --version" ContinueOnError="true">

View File

@@ -0,0 +1,138 @@
using KTUSAPS.Data;
using KTUSAPS.Data.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace KTUSAPS.Services
{
public class DatabaseInitializationService : IHostedService
{
private readonly IServiceProvider serviceProvider;
private readonly ILogger<DatabaseInitializationService> logger;
public DatabaseInitializationService(IServiceProvider serviceProvider, ILogger<DatabaseInitializationService> logger)
{
this.serviceProvider = serviceProvider;
this.logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = serviceProvider.CreateScope();
var dataContext = scope.ServiceProvider.GetRequiredService<SAPSDataContext>();
var migrations = (await dataContext.Database.GetPendingMigrationsAsync(cancellationToken: cancellationToken)).ToList();
if(migrations.Any())
{
logger.LogInformation($"There are {migrations.Count} pending migrations. Applying them");
try
{
await dataContext.Database.MigrateAsync(cancellationToken: cancellationToken);
await Seed(dataContext);
}
catch (Exception ex)
{
logger.LogError("Migration failed. Database may be corrupt!");
logger.LogError(ex, "Migration failed.");
}
}
}
public async Task Seed(SAPSDataContext dataContext)
{
var generalIssueType = await dataContext.IssueTypes.AddAsync(new IssueType()
{
Name = "Bendra",
NameEn = "General"
});
var otherIssueType = await dataContext.IssueTypes.AddAsync(new IssueType()
{
Name = "Kita",
NameEn = "Other"
});
var feedbackIssueType = await dataContext.IssueTypes.AddAsync(new IssueType()
{
Name = "Atsiliepimas",
NameEn = "Feedback"
});
await dataContext.SaveChangesAsync();
var issue1 = await dataContext.Issues.AddAsync(new Issue()
{
Created = DateTime.Now.AddDays(-5),
Description = "Man nepatinka dėstytojas.",
Email = "karolis.kundrotas@ktu.edu",
Publishable = true,
IssueType = generalIssueType.Entity
});
var issue2 = await dataContext.Issues.AddAsync(new Issue()
{
Created = DateTime.Now.AddDays(-12).AddHours(3),
Description = "Dėtytoja atsiskaitymo metu leido nusirašynėti kitiems, o man neleido.",
Email = "karolis.kundrotas@ktu.edu",
Publishable = true,
IssueType = otherIssueType.Entity
});
var issue3 = await dataContext.Issues.AddAsync(new Issue()
{
Created = DateTime.Now.AddDays(-18),
Description = "Tinklų destytoja per paskaitą neatsako į klausimus ir per paskaitą nieko neišmoko.",
Email = "karolis.kundrotas@ktu.edu",
Publishable = false,
IssueType = generalIssueType.Entity
});
var issue4 = await dataContext.Issues.AddAsync(new Issue()
{
Created = DateTime.Now.AddDays(-18),
Description = "Saitynų destytojas Tomas labai maloniai ir profesonaliai bendrauja.",
Email = "karolis.kundrotas@ktu.edu",
Publishable = true,
IssueType = feedbackIssueType.Entity,
Solved = true
});
await dataContext.SaveChangesAsync();
await dataContext.PublishedFeedbacks.AddAsync(new PublishedFeedback()
{
Issue = issue4.Entity,
FeedbackLt = "Studentas mano kad Saitynų dėstytojas Tomas yra profesonalus ir mandagiai bendraujantis.",
FeedbackEn = "Student thinks that Site creation module lecturer Tomas is profesonal ir pleasant at communications.",
});
var problem1 = await dataContext.PublishedProblems.AddAsync(new PublishedProblem()
{
Issue = issue2.Entity,
ProblemLt = "Atsikaitymo metu buvo leista nusirašynėti.",
ProblemEn = "During exam cheating was allowed.",
});
var problem2 = await dataContext.PublishedProblems.AddAsync(new PublishedProblem()
{
Issue = issue3.Entity,
ProblemLt = "Dėstytoja V. Pavardenė nemoko studentų per paskaitas, neraguoja į studentų klausimus, nesuteikia pagalbos.",
ProblemEn = "Lecturer V. Pavardenė does not lecture students, do not react to student questions and doesn't provide help."
});
await dataContext.SaveChangesAsync();
await dataContext.Solutions.AddAsync(new Solution()
{
Problem = problem2.Entity,
SolutionLt = "V. Parvedenei buvo priskirta tarnybinę nuobauda.",
SolutionEn = ""
});
await dataContext.SaveChangesAsync();
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@@ -1,9 +1,12 @@
using KTUSAPS.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using System;
using VueCliMiddleware; using VueCliMiddleware;
namespace KTUSAPS namespace KTUSAPS
@@ -40,6 +43,11 @@ namespace KTUSAPS
.Build(); .Build();
}); });
var connectionString = Configuration.GetConnectionString("Main");
services.AddDbContext<Data.SAPSDataContext>((options) => options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
services.AddHostedService<DatabaseInitializationService>();
services.AddSwaggerGen();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -48,8 +56,16 @@ namespace KTUSAPS
if (env.IsDevelopment()) if (env.IsDevelopment())
{ {
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API");
});
} }
app.UseSwagger(c =>
{
});
app.UseRouting(); app.UseRouting();
app.UseSpaStaticFiles(); app.UseSpaStaticFiles();
app.UseAuthentication(); app.UseAuthentication();

View File

@@ -1,4 +1,7 @@
{ {
"ConnectionStrings": {
"Main": "Server=localhost;User=saps_dev;Password=;Database=saps_dev"
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",

View File

@@ -1,4 +1,7 @@
{ {
"ConnectionStrings": {
"Main": "Server=localhost;User=saps;Password=B35eJUmIJxeG0g9yi6ni;Database=saps"
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",