Compare commits

..

10 Commits

Author SHA1 Message Date
8f4c0688f7 Merge pull request 'Migruoja nuo senstančio webpack prie Vite ir Problemų tipų redagavimas ir pagalbinių funkcijų pridėjimas' (#6) from 22-01-25-issueType-admin into master
Reviewed-on: #6
2022-01-26 18:59:21 +01:00
Karolis2011
bc4bc8eba7 Made things do things. 2022-01-26 19:53:41 +02:00
Karolis2011
0b19ee33a8 Migrate to Vite 2022-01-25 22:05:37 +02:00
Karolis2011
df48e88614 Clean up 2022-01-25 21:11:24 +02:00
Karolis2011
b367854887 A lot 2021-12-23 06:42:40 +02:00
Karolis2011
cad4268b79 Some stuff 2021-12-22 22:50:09 +02:00
Karolis2011
997154efa8 Progresss 2021-12-16 17:33:56 +02:00
Karolis Kundrotas
ba413d4330 Controllers 2021-10-29 09:17:17 +03:00
Karolis Kundrotas
aff6f8df82 Huge work 2021-10-25 22:00:01 +03:00
Karolis Kundrotas
c3bb8983ef Rename and fixes 2021-09-10 11:59:34 +03:00
90 changed files with 4129 additions and 13010 deletions

View File

@@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31606.5
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KTUSA PS", "KTUSA PS\KTUSA PS.csproj", "{D86FA38D-E6E6-48DA-AE1A-007D9FDB3ED8}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KTUSAPS", "KTUSAPS\KTUSAPS.csproj", "{D86FA38D-E6E6-48DA-AE1A-007D9FDB3ED8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KTUSAPS.Data", "KTUSAPS.Data\KTUSAPS.Data.csproj", "{CF02E79E-4B41-4E48-B2FC-094665980F89}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KTUSAPS.Data", "KTUSAPS.Data\KTUSAPS.Data.csproj", "{CF02E79E-4B41-4E48-B2FC-094665980F89}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View File

@@ -1,23 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,24 +0,0 @@
# clientapp
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
{
"name": "clientapp",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@popperjs/core": "^2.10.1",
"axios": "^0.20.0-0",
"bootstrap": "^5.1.1",
"cookies-js": "^1.2.3",
"core-js": "^3.7.0",
"jwt-decode": "^3.1.2",
"vue": "^3.0.2",
"vue-loader-v16": "npm:vue-loader@^16.0.0-alpha.3",
"vue-router": "^4.0.0-rc.5",
"vuex": "^4.0.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.9",
"@vue/cli-plugin-eslint": "^4.5.9",
"@vue/cli-service": "^4.5.9",
"@vue/compiler-sfc": "^3.0.2",
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^7.1.0",
"sass": "^1.33.0",
"sass-loader": "^10.2.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -1,49 +0,0 @@
<template>
<nav-menu></nav-menu>
<div v-if="isLocal" class="container">
<div class="alert alert-danger">
<h4 class="alert-heading">Lokali sistemą aptikta</h4>
<span>
Buvo aptikta, kad aplikacija veikia ant lokalios mašinos. Tai didelis
šansas kad sistemoje pateikiama informacija nėra saugi.
</span>
</div>
</div>
<div v-if="isInsecure" class="container">
<div class="alert alert-danger">
<h4 class="alert-heading">Nesaugus ryšys</h4>
<span>
Buvo aptikta, kad yra užmegztas nesaugus ryšys. Tai reiškią kad betkokie
duomenys perduodami naudojant sistemą, įskaitant ir prisijungimo tokeną,
yra neapsaugoti.
</span>
</div>
</div>
<router-view />
</template>
<script>
import NavMenu from './components/NavMenu.vue'
export default {
name: 'App',
components: {
NavMenu,
},
created() {
this.$store.dispatch('auth/initialize')
},
computed: {
isLocal() {
return (
location.hostname === 'localhost' || location.hostname.startsWith('127')
)
},
isInsecure() {
return location.protocol !== 'https:'
},
},
}
</script>
<style></style>

View File

@@ -1 +0,0 @@


Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,2 +0,0 @@
@import "custom";
@import "~bootstrap/scss/bootstrap";

View File

@@ -1,65 +0,0 @@
<template>
<header class="mb-3">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<router-link :to="{ name: 'Home' }" class="navbar-brand"
>KTU SA Problemų sprendimo sistema</router-link
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="collapse navbar-collapse"
:class="{ show: isExpanded }"
id="navbarNav"
>
<ul class="navbar-nav me-auto">
<li class="nav-item">
<router-link :to="{ name: 'Home' }" class="nav-link"
>Pagrindinis</router-link
>
</li>
</ul>
<div class="navbar-nav">
<span v-if="$store.getters['auth/isValid']" class="navbar-text"
>Prisijungta kaip {{ $store.getters['auth/email'] }}</span
>
<div v-else class="nav-item">
<a :href="$store.getters['auth/loginUrl']" class="nav-link"
>Prisijungti</a
>
</div>
</div>
</div>
</div>
</nav>
</header>
</template>
<script>
export default {
name: 'NavMenu',
data() {
return {
isExpanded: false,
}
},
methods: {
collapse() {
this.isExpanded = false
},
toggle() {
this.isExpanded = !this.isExpanded
},
},
}
</script>

View File

@@ -1,107 +0,0 @@
<template>
<div class="container">
<h1>KTU SA Problemų sprendimo sistema</h1>
<template v-if="$store.getters['auth/isValid']">
<div class="alert alert-success">
<h4 class="alert-heading">Tu esi prisijungęs</h4>
<span>
Kliento aplikacija turi tavo saugos raktą. Aplikacija žino, kad tavo
el. paštas yra: <b>{{ $store.getters['auth/email'] }}</b>
</span>
</div>
<h2>Visi laukai gaunami Azure Active Directory</h2>
<table class="table">
<thead>
<tr>
<th scope="col">Pavadinimas</th>
<th scope="col">Reikšmė</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in authDataTable" :key="key">
<td>{{ key }}</td>
<td>
<pre>{{ value }}</pre>
</td>
</tr>
</tbody>
</table>
<h3>Techninė duomenų reprezentacija</h3>
<a
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens"
>Dokumentacija apie laukų reikšmes</a
>
<pre>{{ $store.state.auth.tokenData }}</pre>
<h3>Saugos raktas.</h3>
<pre>{{ $store.state.auth.token }}</pre>
<h3>Serverio tokeno patikrinimas</h3>
<button type="button" class="btn btn-primary" @click="serverVerify">
Patikrinti
</button>
<h5>Verifikacijos atsakas:</h5>
<pre>
{{ verificationResult }}
</pre>
</template>
<div v-else class="alert alert-danger" role="alert">
<h4 class="alert-heading">Tu neprisijungęs</h4>
<p>Prašom paspausti prisijungimo mygtuką navigacijos juostoje.</p>
</div>
</div>
</template>
<script>
import axios from 'axios'
const names = {
aud: 'AppId (Audience)',
iss: 'Išdavėjas',
iat: 'Išdavimo momentas',
nbf: 'Negalioja anksčiau nei',
exp: 'Galiojimo pabaiga',
email: 'El. paštas',
nonce: 'Aplikacijos sugeneruota nepasikartojanti reikšmė',
sub: 'Subjektas (Vartotojo Id)',
tid: 'Tenanto Identifikatorius',
ver: 'OAuth versija',
}
function lookupName(key) {
if (names[key]) return names[key]
return key
}
export default {
data() {
return {
verificationResult: null,
}
},
computed: {
authDataTable() {
return Object.fromEntries(
Object.entries(this.$store.state.auth.tokenData).map(([key, value]) => [
lookupName(key),
value,
])
)
},
},
methods: {
serverVerify() {
this.verificationResult = null
axios
.get('/test/authed', {
headers: { Authorization: `Bearer ${this.$store.state.auth.token}` },
})
.then(response => {
this.verificationResult = response.data
})
.catch(function(error) {
alert(error)
})
},
},
}
</script>

View File

@@ -1,25 +0,0 @@
<template>
<div class="container">
<h1>Palaukite kol nustatysime jūsų tapatybe...</h1>
</div>
</template>
<script>
const tokenRegex = /id_token=(.*\..*\..*)&/
export default {
name: 'OIDC',
created() {
if (this.openIdToken)
this.$store.dispatch('auth/setToken', this.openIdToken)
this.$router.push({ name: 'Home' })
},
computed: {
openIdToken() {
const matches = this.$route.hash.match(tokenRegex)
if (!matches) return null
return matches[1]
},
},
}
</script>

View File

@@ -1,23 +0,0 @@
import { createWebHistory, createRouter } from 'vue-router'
import Home from '@/pages/Home.vue'
import OidcEndpoint from '@/pages/OidcEndpoint.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/oidc',
name: 'OpenID connect endpoint',
component: OidcEndpoint,
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router

View File

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

View File

@@ -1,145 +0,0 @@
import Cookies from 'cookies-js'
import jwt_decode from 'jwt-decode'
import axios from 'axios'
const TokenCookieName = 'ktusaktutoken'
const ClientIdCookieName = 'ktusakacas'
const AuthorityCookieName = 'ktusakeksas'
const TenantCookieName = 'ktusalaimis'
const NonceCookieName = 'ktusakumpikas'
const Scope = 'openid email'
// initial state
const state = () => ({
token: null,
tokenData: null,
clientId: null, // 5931fda0-e9e0-4754-80c2-18bcb9d9561a
authority: null, // https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/v2.0
tenant: null, // 3415f2f7-f5a8-4092-b52a-003aaf844853
})
const callbackUrl =
window.location.protocol + '//' + window.location.host + '/oidc'
// getters
const getters = {
isReady(state) {
if (
state.clientId == null ||
state.authority == null ||
state.tenant == null
)
return false
return true
},
isValid(state, getters) {
if (!getters.isReady) return false
if (state.token == null || state.tokenData == null) return false
const d = state.tokenData
if (d.nonce !== state.nonce) return false
if (d.iss !== state.authority) return false
if (d.aud !== state.clientId) return false
const now = new Date()
const exp = new Date(d.exp * 1000)
if (now > exp) return false
return true
},
email(state, getters) {
if (!getters.isValid) return null
return state.tokenData.email
},
userId(state, getters) {
if (!getters.isValid) return null
return state.tokenData.email
},
loginUrl(state, getters) {
if (!getters.isReady) return null
return `https://login.microsoftonline.com/${
state.tenant
}/oauth2/v2.0/authorize?client_id=${
state.clientId
}&redirect_uri=${encodeURIComponent(
callbackUrl
)}&response_type=id_token&scope=${Scope}&nonce=${state.nonce}`
},
}
// actions
const actions = {
async initialize({ commit }) {
const token = Cookies.get(TokenCookieName)
const primaryClientId = Cookies.get(ClientIdCookieName)
const primaryAuthority = Cookies.get(AuthorityCookieName)
const primaryTenant = Cookies.get(TenantCookieName)
const nonce = Cookies.get(NonceCookieName)
if (!nonce) {
const newNonce =
Date.now().toString(36) +
Math.random()
.toString(36)
.substring(2)
Cookies.set(NonceCookieName, newNonce)
commit('setNonce', newNonce)
} else {
commit('setNonce', nonce)
}
commit('setToken', token)
commit('computeTokenVars')
commit('setMetadata', [primaryClientId, primaryAuthority, primaryTenant])
axios
.get('/api/AuthMetadata')
.then(response => {
Cookies.set(ClientIdCookieName, response.data.clientId)
Cookies.set(AuthorityCookieName, response.data.authority)
Cookies.set(TenantCookieName, response.data.tenant)
commit('setMetadata', [
response.data.clientId,
response.data.authority,
response.data.tenant,
])
})
.catch(error => {
console.error(error)
})
},
async setToken({ commit }, token) {
Cookies.set(TokenCookieName, token)
commit('setToken', token)
commit('computeTokenVars')
},
}
// mutations
const mutations = {
setToken(state, token) {
state.token = token
},
setNonce(state, nonce) {
state.nonce = nonce
},
computeTokenVars(state) {
if (state.token == null) return
try {
state.tokenData = jwt_decode(state.token)
} catch {
console.log('Token was invalid.')
state.tokenData = null
state.token = null
}
},
setMetadata(state, [clientId, authority, tenant]) {
state.clientId = clientId
state.authority = authority
state.tenant = tenant
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}

View File

@@ -1,8 +0,0 @@
module.exports = {
runtimeCompiler: true,
chainWebpack: (config) => {
config.resolve.alias
.set("balm-ui-plus", "balm-ui/dist/balm-ui-plus.js")
.set("balm-ui-css", "balm-ui/dist/balm-ui.css");
},
};

View File

@@ -1,24 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace KTUSA_PS.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthMetadataController : ControllerBase
{
private readonly IConfiguration _configuration;
public AuthMetadataController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpGet]
public object Index() => new { ClientId = _configuration["ClientId"], Authority = _configuration["Authority"], Tenant = _configuration["Tenant"] };
}
}

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 KTUSA_PS.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

@@ -1,85 +0,0 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using VueCliMiddleware;
namespace KTUSA_PS
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp";
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Audience = "5931fda0-e9e0-4754-80c2-18bcb9d9561a";
options.Authority = "https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/v2.0";
});
services.AddAuthorization((configure) =>
{
configure.DefaultPolicy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSpa(spa =>
{
if (env.IsDevelopment())
spa.Options.SourcePath = "ClientApp/";
else
spa.Options.SourcePath = "dist";
if (env.IsDevelopment())
{
spa.UseVueCli(npmScript: "serve");
}
});
}
}
}

View File

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

View File

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

View File

@@ -27,24 +27,19 @@ namespace KTUSAPS.Data.Migrations
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Issues",
name: "IssueTypes",
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)
Name = table.Column<string>(type: "longtext", 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)
NameEn = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Issues", x => x.Id);
table.PrimaryKey("PK_IssueTypes", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
@@ -66,6 +61,35 @@ namespace KTUSAPS.Data.Migrations
})
.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(
name: "PublishedFeedbacks",
columns: table => new
@@ -147,6 +171,11 @@ namespace KTUSAPS.Data.Migrations
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Issues_IssueTypeId",
table: "Issues",
column: "IssueTypeId");
migrationBuilder.CreateIndex(
name: "IX_PublishedFeedbacks_IssueId",
table: "PublishedFeedbacks",
@@ -190,6 +219,9 @@ namespace KTUSAPS.Data.Migrations
migrationBuilder.DropTable(
name: "Solutions");
migrationBuilder.DropTable(
name: "IssueTypes");
}
}
}

View File

@@ -48,6 +48,9 @@ namespace KTUSAPS.Data.Migrations
.HasMaxLength(320)
.HasColumnType("varchar(320)");
b.Property<int>("IssueTypeId")
.HasColumnType("int");
b.Property<bool>("Publishable")
.HasColumnType("tinyint(1)");
@@ -60,9 +63,28 @@ namespace KTUSAPS.Data.Migrations
b.HasKey("Id");
b.HasIndex("IssueTypeId");
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 =>
{
b.Property<int>("Id")
@@ -167,6 +189,17 @@ namespace KTUSAPS.Data.Migrations
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 =>
{
b.HasOne("KTUSAPS.Data.Model.Issue", "Issue")
@@ -209,6 +242,11 @@ namespace KTUSAPS.Data.Migrations
b.Navigation("Problem");
});
modelBuilder.Entity("KTUSAPS.Data.Model.IssueType", b =>
{
b.Navigation("Issues");
});
modelBuilder.Entity("KTUSAPS.Data.Model.PublishedProblem", b =>
{
b.Navigation("Votes");

View File

@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace KTUSAPS.Data.Model
@@ -22,7 +23,17 @@ namespace KTUSAPS.Data.Model
[MaxLength]
public string Description { get; set; }
public PublishedProblem Problem { get; set; }
public PublishedFeedback Feedback { get; set; }
public int IssueTypeId { get; set; }
[JsonIgnore]
public virtual IssueType IssueType { get; set; }
[JsonIgnore]
public virtual PublishedProblem Problem { get; set; }
[JsonIgnore]
public virtual PublishedFeedback Feedback { get; set; }
public Issue()
{
Created = DateTime.Now;
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
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; }
[JsonIgnore]
public virtual HashSet<Issue> Issues { get; set; }
}
}

View File

@@ -1,18 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<ClassDiagram MajorVersion="1" MinorVersion="1">
<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>
<HashCode>AAACAAJAACAgAAAAAAACAAgIAAAAAAQAAAAAAAAAAAA=</HashCode>
<HashCode>AIACAAJQACAgAAAAAAACAAgIAAAAAAQAAAAAAAAAAAA=</HashCode>
<FileName>Model\Issue.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Property Name="IssueType" />
<Property Name="Problem" />
<Property Name="Feedback" />
</ShowAsAssociation>
</Class>
<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>
<HashCode>AAECAAAAAAAAAAAACAIAAAAAAAAAAAQAEAAAAAAAAAA=</HashCode>
<FileName>Model\PublishedFeedback.cs</FileName>
@@ -22,9 +23,9 @@
</ShowAsAssociation>
</Class>
<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>
<HashCode>ACECABAAAAEAAAgAAAABAAAAEAAAQAQAEAAAAAAAAAA=</HashCode>
<HashCode>ACECABAAAAEAAAgAAAABAAAAAAAAAAQAEAAAAAAAAAA=</HashCode>
<FileName>Model\PublishedProblem.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
@@ -36,7 +37,7 @@
</ShowAsCollectionAssociation>
</Class>
<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>
<HashCode>AAACAAIABAAAAQAAAAAAAAAAAAAAAAQAAAAAAAAAAAA=</HashCode>
<FileName>Model\Solution.cs</FileName>
@@ -46,7 +47,7 @@
</ShowAsAssociation>
</Class>
<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>
<HashCode>AAAAAAIAAAAAEAAAAAAAAAgAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Model\Vote.cs</FileName>
@@ -55,5 +56,22 @@
<Property Name="Problem" />
</ShowAsAssociation>
</Class>
<Class Name="KTUSAPS.Data.Model.IssueType">
<Position X="11" Y="5.5" Width="1.5" />
<TypeIdentifier>
<HashCode>AAECAAAAAAAAAAAAACAAAAQAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Model\IssueType.cs</FileName>
</TypeIdentifier>
<ShowAsCollectionAssociation>
<Property Name="Issues" />
</ShowAsCollectionAssociation>
</Class>
<Class Name="KTUSAPS.Data.Model.Admin">
<Position X="0.75" Y="4.5" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAEAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Model\Admin.cs</FileName>
</TypeIdentifier>
</Class>
<Font Name="Segoe UI" Size="9" />
</ClassDiagram>

View File

@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace KTUSAPS.Data.Model
@@ -22,6 +23,12 @@ namespace KTUSAPS.Data.Model
public DateTime Created { get; set; }
public int? IssueId { get; set; }
public Issue Issue { get; set; }
[JsonIgnore]
public virtual Issue Issue { get; set; }
public PublishedFeedback()
{
Created = DateTime.Now;
}
}
}

View File

@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace KTUSAPS.Data.Model
@@ -18,20 +19,22 @@ namespace KTUSAPS.Data.Model
[Required]
[MaxLength]
public string ProblemEn { get; set; }
[MaxLength]
public string ResponseLt { get; set; }
[MaxLength]
public string ResponseEn { get; set; }
public DateTime Created { get; set; }
public int? IssueId { get; set; }
public Issue Issue { get; set; }
[JsonIgnore]
public virtual Issue Issue { get; set; }
public int? SolutionId { get; set; }
public Solution Solution { get; set; }
public ICollection<Vote> Votes { get; set; }
[JsonIgnore]
public virtual Solution Solution { get; set; }
[JsonIgnore]
public virtual HashSet<Vote> Votes { get; set; }
public PublishedProblem()
{
Created = DateTime.Now;
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace KTUSAPS.Data.Model
{
@@ -13,8 +14,13 @@ namespace KTUSAPS.Data.Model
[MaxLength]
public string SolutionEn { get; set; }
public PublishedProblem Problem { get; set; }
[JsonIgnore]
public virtual PublishedProblem Problem { get; set; }
public DateTime Created { get; set; }
public Solution()
{
Created = DateTime.Now;
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace KTUSAPS.Data.Model
@@ -12,7 +13,8 @@ namespace KTUSAPS.Data.Model
[MaxLength(64)]
public string UserId { get; set; }
public int ProblemId { get; set; }
public PublishedProblem Problem { get; set; }
[JsonIgnore]
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<PublishedFeedback> PublishedFeedbacks { get; set; }
public DbSet<PublishedProblem> PublishedProblems { get; set; }

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization;
namespace KTUSAPS.Auth
{
public class AdminRequirement : IAuthorizationRequirement
{
}
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization;
namespace KTUSAPS.Auth
{
public class MyIssueRequirement : IAuthorizationRequirement
{
}
}

View File

@@ -0,0 +1,117 @@
using KTUSAPS.Data;
using KTUSAPS.Data.Model;
using KTUSAPS.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace KTUSAPS.Auth
{
public class SaPsAuthorizationHandler : IAuthorizationHandler
{
private readonly IServiceProvider serviceProvider;
public SaPsAuthorizationHandler(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public async Task HandleAsync(AuthorizationHandlerContext context)
{
var cache = new AuthProcessingCache(context.User, serviceProvider);
foreach (var requirement in context.Requirements)
{
if (requirement is AdminRequirement adminRequirement)
await HandleRequirementAsync(context, adminRequirement, cache);
if (requirement is MyIssueRequirement myIssueRequirement)
await HandleRequirementAsync(context, myIssueRequirement, cache);
}
}
private async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyIssueRequirement myIssueRequirement, AuthProcessingCache cache)
{
if(context.Resource is not Issue issue)
throw new Exception($"'{nameof(MyIssueRequirement)}' must be issued with resource of type '{nameof(Issue)}'");
if(issue.UserID == context.User.GetUserId())
{
context.Succeed(myIssueRequirement);
return;
}
if(await cache.DetermineIsAdminAsync())
{
context.Succeed(myIssueRequirement);
return;
}
}
private async Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement adminRequirement, AuthProcessingCache cache)
{
if(await cache.DetermineIsAdminAsync())
{
context.Succeed(adminRequirement);
return;
}
context.Fail();
}
private class AuthProcessingCache
{
private readonly ClaimsPrincipal _user;
private readonly IServiceProvider _serviceProvider;
private bool? isAdmin;
public bool IsAdmin => determineIsAdminCached();
public AuthProcessingCache(ClaimsPrincipal user, IServiceProvider serviceProvider) {
_user = user;
_serviceProvider = serviceProvider;
}
private bool determineIsAdminCached()
{
if (isAdmin == null)
isAdmin = determineIsAdmin();
return isAdmin.Value;
}
private bool determineIsAdmin()
{
var objectId = _user.GetObjectId();
if (objectId == default)
return false;
using var scope = _serviceProvider.CreateScope();
var dataContext = scope.ServiceProvider.GetRequiredService<SAPSDataContext>();
var admin = dataContext.Admins.Where(a => a.UserId == objectId).FirstOrDefault();
if (admin != default)
return true;
return false;
}
private async ValueTask<bool> determineIsAdminAsync()
{
var idclaim = _user.Claims.Where(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").FirstOrDefault();
if (idclaim == default)
return false;
using var scope = _serviceProvider.CreateScope();
var dataContext = scope.ServiceProvider.GetRequiredService<SAPSDataContext>();
var admin = await dataContext.Admins.Where(a => a.UserId == idclaim.Value).FirstOrDefaultAsync();
if (admin != default)
return true;
return false;
}
public async ValueTask<bool> DetermineIsAdminAsync()
{
if (isAdmin == null)
isAdmin = await determineIsAdminAsync();
return isAdmin.Value;
}
}
}
}

View File

@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Collections.Generic;
using System.Linq;
namespace KTUSAPS
{
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var hasAuthorize =
context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any()
|| context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
if (hasAuthorize)
{
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[
new OpenApiSecurityScheme {Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "msad"}
}
] = new[] {"email", "openid", "profile"}
}
};
}
}
}
}

5
KTUSAPS/ClientApp/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title>KTU SA Problemų sprendimo sistema</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

483
KTUSAPS/ClientApp/package-lock.json generated Normal file
View File

@@ -0,0 +1,483 @@
{
"name": "clientapp",
"version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@azure/msal-browser": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.21.0.tgz",
"integrity": "sha512-y+oAUJJ0m7gRUT505iGT4SDXktrRzOE3HZC0+nQyHDI+3O7BKK5NjQUwD/4V8FNNyrhxEtLk99iwJEo61epinQ==",
"requires": {
"@azure/msal-common": "^6.0.0"
}
},
"@azure/msal-common": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-6.0.0.tgz",
"integrity": "sha512-Vva3snqmWPHJNDCBb4lz3D0rvZsi/0QikAxHvVFNwtNg5pP4NZE4U34tNiXN+m9KhlQFrZBPkE5rk7dIEOGcWw==",
"requires": {
"debug": "^4.1.1"
}
},
"@babel/parser": {
"version": "7.16.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz",
"integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A=="
},
"@popperjs/core": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
"integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA=="
},
"@vitejs/plugin-vue": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.1.0.tgz",
"integrity": "sha512-AZ78WxvFMYd8JmM/GBV6a6SGGTU0GgN/0/4T+FnMMsLzFEzTeAUwuraapy50ifHZsC+G5SvWs86bvaCPTneFlA==",
"dev": true
},
"@vue/compiler-core": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.29.tgz",
"integrity": "sha512-RePZ/J4Ub3sb7atQw6V6Rez+/5LCRHGFlSetT3N4VMrejqJnNPXKUt5AVm/9F5MJriy2w/VudEIvgscCfCWqxw==",
"requires": {
"@babel/parser": "^7.16.4",
"@vue/shared": "3.2.29",
"estree-walker": "^2.0.2",
"source-map": "^0.6.1"
}
},
"@vue/compiler-dom": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.29.tgz",
"integrity": "sha512-y26vK5khdNS9L3ckvkqJk/78qXwWb75Ci8iYLb67AkJuIgyKhIOcR1E8RIt4mswlVCIeI9gQ+fmtdhaiTAtrBQ==",
"requires": {
"@vue/compiler-core": "3.2.29",
"@vue/shared": "3.2.29"
}
},
"@vue/compiler-sfc": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.29.tgz",
"integrity": "sha512-X9+0dwsag2u6hSOP/XsMYqFti/edvYvxamgBgCcbSYuXx1xLZN+dS/GvQKM4AgGS4djqo0jQvWfIXdfZ2ET68g==",
"requires": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.29",
"@vue/compiler-dom": "3.2.29",
"@vue/compiler-ssr": "3.2.29",
"@vue/reactivity-transform": "3.2.29",
"@vue/shared": "3.2.29",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7",
"postcss": "^8.1.10",
"source-map": "^0.6.1"
}
},
"@vue/compiler-ssr": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.29.tgz",
"integrity": "sha512-LrvQwXlx66uWsB9/VydaaqEpae9xtmlUkeSKF6aPDbzx8M1h7ukxaPjNCAXuFd3fUHblcri8k42lfimHfzMICA==",
"requires": {
"@vue/compiler-dom": "3.2.29",
"@vue/shared": "3.2.29"
}
},
"@vue/devtools-api": {
"version": "6.0.0-beta.21.1",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.0-beta.21.1.tgz",
"integrity": "sha512-FqC4s3pm35qGVeXRGOjTsRzlkJjrBLriDS9YXbflHLsfA9FrcKzIyWnLXoNm+/7930E8rRakXuAc2QkC50swAw=="
},
"@vue/reactivity": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.29.tgz",
"integrity": "sha512-Ryhb6Gy62YolKXH1gv42pEqwx7zs3n8gacRVZICSgjQz8Qr8QeCcFygBKYfJm3o1SccR7U+bVBQDWZGOyG1k4g==",
"requires": {
"@vue/shared": "3.2.29"
}
},
"@vue/reactivity-transform": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.29.tgz",
"integrity": "sha512-YF6HdOuhdOw6KyRm59+3rML8USb9o8mYM1q+SH0G41K3/q/G7uhPnHGKvspzceD7h9J3VR1waOQ93CUZj7J7OA==",
"requires": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.29",
"@vue/shared": "3.2.29",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7"
}
},
"@vue/runtime-core": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.29.tgz",
"integrity": "sha512-VMvQuLdzoTGmCwIKTKVwKmIL0qcODIqe74JtK1pVr5lnaE0l25hopodmPag3RcnIcIXe+Ye3B2olRCn7fTCgig==",
"requires": {
"@vue/reactivity": "3.2.29",
"@vue/shared": "3.2.29"
}
},
"@vue/runtime-dom": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.29.tgz",
"integrity": "sha512-YJgLQLwr+SQyORzTsBQLL5TT/5UiV83tEotqjL7F9aFDIQdFBTCwpkCFvX9jqwHoyi9sJqM9XtTrMcc8z/OjPA==",
"requires": {
"@vue/runtime-core": "3.2.29",
"@vue/shared": "3.2.29",
"csstype": "^2.6.8"
}
},
"@vue/server-renderer": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.29.tgz",
"integrity": "sha512-lpiYx7ciV7rWfJ0tPkoSOlLmwqBZ9FTmQm33S+T4g0j1fO/LmhJ9b9Ctl1o5xvIFVDk9QkSUWANZn7H2pXuxVw==",
"requires": {
"@vue/compiler-ssr": "3.2.29",
"@vue/shared": "3.2.29"
}
},
"@vue/shared": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.29.tgz",
"integrity": "sha512-BjNpU8OK6Z0LVzGUppEk0CMYm/hKDnZfYdjSmPOs0N+TR1cLKJAkDwW8ASZUvaaSLEi6d3hVM7jnWnX+6yWnHw=="
},
"axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"requires": {
"follow-redirects": "^1.14.7"
}
},
"bootstrap": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q=="
},
"bootstrap-icons": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.7.2.tgz",
"integrity": "sha512-NiR2PqC73AQOPdVSu6GJfnk+hN2z6powcistXk1JgPnKuoV2FSdSl26w931Oz9HYbKCcKUSB6ncZTYJAYJl3QQ=="
},
"csstype": {
"version": "2.6.19",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz",
"integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ=="
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"esbuild": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz",
"integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==",
"dev": true,
"requires": {
"esbuild-android-arm64": "0.13.15",
"esbuild-darwin-64": "0.13.15",
"esbuild-darwin-arm64": "0.13.15",
"esbuild-freebsd-64": "0.13.15",
"esbuild-freebsd-arm64": "0.13.15",
"esbuild-linux-32": "0.13.15",
"esbuild-linux-64": "0.13.15",
"esbuild-linux-arm": "0.13.15",
"esbuild-linux-arm64": "0.13.15",
"esbuild-linux-mips64le": "0.13.15",
"esbuild-linux-ppc64le": "0.13.15",
"esbuild-netbsd-64": "0.13.15",
"esbuild-openbsd-64": "0.13.15",
"esbuild-sunos-64": "0.13.15",
"esbuild-windows-32": "0.13.15",
"esbuild-windows-64": "0.13.15",
"esbuild-windows-arm64": "0.13.15"
},
"dependencies": {
"esbuild-windows-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz",
"integrity": "sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==",
"dev": true,
"optional": true
}
}
},
"esbuild-android-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz",
"integrity": "sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==",
"dev": true,
"optional": true
},
"esbuild-darwin-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz",
"integrity": "sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==",
"dev": true,
"optional": true
},
"esbuild-darwin-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz",
"integrity": "sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==",
"dev": true,
"optional": true
},
"esbuild-freebsd-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz",
"integrity": "sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==",
"dev": true,
"optional": true
},
"esbuild-freebsd-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz",
"integrity": "sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==",
"dev": true,
"optional": true
},
"esbuild-linux-32": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz",
"integrity": "sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==",
"dev": true,
"optional": true
},
"esbuild-linux-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz",
"integrity": "sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==",
"dev": true,
"optional": true
},
"esbuild-linux-arm": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz",
"integrity": "sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==",
"dev": true,
"optional": true
},
"esbuild-linux-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz",
"integrity": "sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==",
"dev": true,
"optional": true
},
"esbuild-linux-mips64le": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz",
"integrity": "sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==",
"dev": true,
"optional": true
},
"esbuild-linux-ppc64le": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz",
"integrity": "sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==",
"dev": true,
"optional": true
},
"esbuild-netbsd-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz",
"integrity": "sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==",
"dev": true,
"optional": true
},
"esbuild-openbsd-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz",
"integrity": "sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==",
"dev": true,
"optional": true
},
"esbuild-sunos-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz",
"integrity": "sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==",
"dev": true,
"optional": true
},
"esbuild-windows-32": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz",
"integrity": "sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==",
"dev": true,
"optional": true
},
"esbuild-windows-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz",
"integrity": "sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==",
"dev": true,
"optional": true
},
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"follow-redirects": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"is-core-module": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
"integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
},
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"requires": {
"sourcemap-codec": "^1.4.4"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nanoid": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA=="
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"postcss": {
"version": "8.4.5",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz",
"integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==",
"requires": {
"nanoid": "^3.1.30",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.1"
}
},
"resolve": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
"integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
"dev": true,
"requires": {
"is-core-module": "^2.8.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
},
"rollup": {
"version": "2.66.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.66.1.tgz",
"integrity": "sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
},
"supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"vite": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.7.13.tgz",
"integrity": "sha512-Mq8et7f3aK0SgSxjDNfOAimZGW9XryfHRa/uV0jseQSilg+KhYDSoNb9h1rknOy6SuMkvNDLKCYAYYUMCE+IgQ==",
"dev": true,
"requires": {
"esbuild": "^0.13.12",
"fsevents": "~2.3.2",
"postcss": "^8.4.5",
"resolve": "^1.20.0",
"rollup": "^2.59.0"
}
},
"vue": {
"version": "3.2.29",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.29.tgz",
"integrity": "sha512-cFIwr7LkbtCRanjNvh6r7wp2yUxfxeM2yPpDQpAfaaLIGZSrUmLbNiSze9nhBJt5MrZ68Iqt0O5scwAMEVxF+Q==",
"requires": {
"@vue/compiler-dom": "3.2.29",
"@vue/compiler-sfc": "3.2.29",
"@vue/runtime-dom": "3.2.29",
"@vue/server-renderer": "3.2.29",
"@vue/shared": "3.2.29"
}
},
"vue-router": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.12.tgz",
"integrity": "sha512-CPXvfqe+mZLB1kBWssssTiWg4EQERyqJZes7USiqfW9B5N2x+nHlnsM1D3b5CaJ6qgCvMmYJnz+G0iWjNCvXrg==",
"requires": {
"@vue/devtools-api": "^6.0.0-beta.18"
}
},
"vuex": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.2.tgz",
"integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==",
"requires": {
"@vue/devtools-api": "^6.0.0-beta.11"
}
}
}
}

View File

@@ -0,0 +1,26 @@
{
"name": "clientapp",
"version": "0.0.0",
"scripts": {
"serve": "vite",
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@azure/msal-browser": "^2.21.0",
"@popperjs/core": "^2.10.2",
"axios": "^0.25.0",
"bootstrap": "^5.1.3",
"bootstrap-icons": "^1.7.2",
"lodash": "^4.17.21",
"vue": "^3.2.25",
"vue-router": "^4.0.12",
"vuex": "^4.0.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.0.0",
"sass": "^1.49.0",
"vite": "^2.7.2"
}
}

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,101 @@
<template>
<header>
<nav-menu />
<admin-menu />
</header>
<data-alert />
<confirm-alert ref="ca" />
<div class="mb-3"></div>
<div v-if="isLocal" class="container">
<div class="alert alert-danger">
<h4 class="alert-heading">Lokali sistemą aptikta</h4>
<span>
Buvo aptikta, kad aplikacija veikia ant lokalios mašinos. Tai didelis
šansas kad sistemoje pateikiama informacija nėra saugi.
</span>
</div>
</div>
<div v-if="isInsecure" class="container">
<div class="alert alert-danger">
<h4 class="alert-heading">Nesaugus ryšys</h4>
<span>
Buvo aptikta, kad yra užmegztas nesaugus ryšys. Tai reiškią kad betkokie
duomenys perduodami naudojant sistemą, įskaitant ir prisijungimo tokeną,
yra neapsaugoti.
</span>
</div>
</div>
<router-view v-slot="{ Component }">
<transition name="slide-fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<script>
import NavMenu from './components/NavMenu.vue'
import DataAlert from './components/DataAlert.vue'
import ConfirmAlert from './components/ConfirmAlert.vue'
import AdminMenu from './components/AdminMenu.vue'
export default {
name: 'App',
components: {
NavMenu,
DataAlert,
ConfirmAlert,
AdminMenu,
},
data() {
return {
expiresIn: '',
interval: null,
}
},
created() {
this.$store.dispatch('msalAuth/initialize')
},
computed: {
isLocal() {
return (
location.hostname === 'localhost' || location.hostname.startsWith('127')
)
},
isInsecure() {
return location.protocol !== 'https:'
},
},
methods: {
async confirm(text, title, options = {}) {
const passedOptions = Object.assign(
{ text, title, affirmitive: {}, negative: {} },
options
)
await new Promise((res, rej) => {
passedOptions.affirmitive.action = res
passedOptions.negative.action = () =>
rej(new Error('User rejected confirmation.'))
this.$refs.ca.configure(passedOptions)
this.$refs.ca.show()
})
},
async deleteConfirmation() {
await this.confirm(
'Ar tikrai norite ištrinti šį elementą?',
'Ištrinimo patvirtinimas',
{
affirmitive: {
text: 'Ištrinti',
type: 'danger',
},
negative: {
text: 'Atšaukti',
},
}
)
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,5 @@
$font-family-base: 'Open Sans', sans-serif;
$primary: #1862a1;
$danger: #a1181d;
$warning: #eb7d1d;

View File

@@ -0,0 +1,47 @@
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap');
@import 'custom';
@import 'bootstrap/scss/bootstrap';
@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css');
/* Enter and leave animations can use different */
/* durations and timing functions. */
.slide-fade-enter-active {
transition: all 0.15s ease-out;
}
.slide-fade-leave-active {
transition: all 0.15s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
.slide-fade-enter-to,
.slide-fade-leave-from {
transform: translateX(0px);
opacity: 1;
}
.fade-enter-active {
transition: all 0.3s ease-out;
}
.fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scale(0);
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
transform: scale(1);
}

View File

@@ -0,0 +1,16 @@
import Axios from 'axios'
import store from './store'
export const authAxios = Axios.create({})
export const axios = Axios
authAxios.interceptors.request.use(
async (config) => {
await store.dispatch('msalAuth/waitTillReady') // Delay requesrt till we ready
config.headers['Authorization'] = `Bearer ${store.state.msalAuth.idToken}`
return config
},
() => {}
)
export default Axios

View File

@@ -0,0 +1,58 @@
<template>
<nav
v-if="$store.state.msalAuth.isAdmin"
class="navbar navbar-expand-lg navbar-dark bg-black"
>
<div class="container-fluid">
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#adminbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="collapse navbar-collapse"
:class="{ show: isExpanded }"
id="adminbarNav"
>
<ul class="navbar-nav me-auto">
<li class="nav-item">
<router-link :to="{ name: 'Issues' }" class="nav-link"
>Pateiktos problemos</router-link
>
</li>
<li class="nav-item">
<router-link :to="{ name: 'AdminIssueTypes' }" class="nav-link"
>Problemų tipai</router-link
>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default {
name: 'AdminMenu',
data() {
return {
isExpanded: false,
}
},
methods: {
collapse() {
this.isExpanded = false
},
toggle() {
this.isExpanded = !this.isExpanded
},
},
}
</script>

View File

@@ -0,0 +1,67 @@
<template>
<transition name="fade">
<div
v-if="shown"
class="alert"
:class="{ ['alert-' + type]: true }"
role="alert"
>
<h4 v-if="title" class="alert-heading">{{ title }}</h4>
{{ text }}
</div>
</transition>
</template>
<script>
import defaults from 'lodash/defaults'
const defaultOptions = {
title: null,
text: 'You have not configured alerter.',
type: 'danger',
}
export default {
data() {
return {
shown: false,
text: 'A simple alert',
title: null,
type: 'primary',
dismisstimer: null,
}
},
methods: {
show() {
this.shown = true
},
hide() {
this.shown = false
this.cancelAutoDismiss()
},
cancelAutoDismiss() {
if (this.dismisstimer) clearTimeout(this.dismisstimer)
this.dismisstimer = null
},
autoDismiss(timeout) {
this.cancelAutoDismiss()
this.dismisstimer = setTimeout(() => this.hide(), timeout)
},
configure(options) {
const final = defaults(options, defaultOptions)
this.text = final.text
this.title = final.title
this.type = final.type
},
alert(options) {
this.configure(options)
this.cancelAutoDismiss()
this.show()
},
alertTimed(options, timeout = 4000) {
this.alert(options)
this.autoDismiss(timeout)
},
},
}
</script>

View File

@@ -0,0 +1,98 @@
<template>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ options.title }}</h5>
</div>
<div class="modal-body">
<p>
{{ options.text }}
</p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn"
:class="'btn-' + options.affirmitive.type"
@click="accept"
>
{{ options.affirmitive.text }}
</button>
<button
type="button"
class="btn"
:class="'btn-' + options.negative.type"
@click="reject"
>
{{ options.negative.text }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import defaultsDeep from 'lodash/defaultsDeep'
import { Modal } from 'bootstrap'
const defaultValues = {
title: 'Confirmation',
text: 'Do you confirm this action?',
affirmitive: {
hide: false,
text: 'Yes',
type: 'primary',
action: () => null,
},
negative: {
hide: false,
text: 'No',
type: 'secondary',
action: () => null,
},
}
export default {
data() {
return {
modal: null,
beenUsed: false,
options: Object.assign({}, defaultValues),
}
},
mounted() {
var el = this.$refs.modal
el.addEventListener('hide.bs.modal', () => this.handleHideEvent())
this.modal = new Modal(el, {})
},
methods: {
handleHideEvent() {
if (!this.beenUsed) {
this.options.negative.action()
}
},
show() {
this.beenUsed = false
this.modal.show()
},
hide() {
this.modal.hide()
},
accept() {
this.beenUsed = true
this.options.affirmitive.action()
this.hide()
},
reject() {
this.beenUsed = true
this.options.negative.action()
this.hide()
},
configure(options) {
this.options = defaultsDeep({}, options || {}, defaultValues)
},
},
}
</script>

View File

@@ -0,0 +1,58 @@
<template>
<div>
<div ref="modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Duomenų naudojimo pranešimas</h5>
</div>
<div class="modal-body">
<p>
Norime jus informuoti, kad sutikdami su šituo duomenų sutikimo jus
KTU studentų atstovybei sutinkate perduoti savo duomenis, tokius
kaip el. pašto adresas, indentifikacijos kodas, jūsų vardas ir
pavardė, ir kitus svarbūs identifikavimo duomenis apie jus.
</p>
<p>
Norime informuoti, kad ši programa naudoja sausainiukus ir vietinę
saugyklą, kad užtikrintų sklandų veikimą.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" @click="confirm">
Sutinku
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from 'bootstrap'
export default {
data() {
return {
modal: null,
}
},
methods: {
confirm() {
this.modal.hide()
window.localStorage.setItem('conf', 'true')
},
},
mounted() {
var el = this.$refs.modal
this.modal = new Modal(el, {
keyboard: false,
backdrop: 'static',
})
if (!window.localStorage.getItem('conf')) {
this.modal.show()
}
},
}
</script>

View File

@@ -0,0 +1,90 @@
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<router-link :to="{ name: 'Home' }" class="navbar-brand"
>KTU SA Problemų sprendimo sistema</router-link
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="collapse navbar-collapse"
:class="{ show: isExpanded }"
id="navbarNav"
>
<ul class="navbar-nav me-auto">
<li class="nav-item">
<router-link :to="{ name: 'Home' }" class="nav-link"
>Pagrindinis</router-link
>
</li>
<template v-if="$store.state.msalAuth.isLoggedIn">
<li class="nav-item">
<router-link :to="{ name: 'Submit' }" class="nav-link"
>Pateikti problemą</router-link
>
</li>
<li class="nav-item">
<router-link :to="{ name: 'Problems' }" class="nav-link"
>Problemos</router-link
>
</li>
<li class="nav-item">
<router-link :to="{ name: 'Feedbacks' }" class="nav-link"
>Atsiliepimai</router-link
>
</li>
</template>
<li v-if="$root.isLocal" class="nav-item">
<a href="/swagger" class="nav-link">SUI</a>
</li>
</ul>
<div class="navbar-nav">
<template v-if="$store.state.msalAuth.isLoggedIn">
<span class="navbar-text"
>Prisijungta kaip {{ $store.state.msalAuth.displayName }}
</span>
<div class="nav-item">
<a href="#" @click="LogoutMsal" class="nav-link">Atsijungti</a>
</div>
</template>
<div v-else class="nav-item">
<a href="#" @click="LoginMsal" class="nav-link">Prisijungti</a>
</div>
</div>
</div>
</div>
</nav>
</template>
<script>
import { LoginMsal, LogoutMsal } from '@/msal'
export default {
name: 'NavMenu',
data() {
return {
isExpanded: false,
}
},
methods: {
LoginMsal,
LogoutMsal,
collapse() {
this.isExpanded = false
},
toggle() {
this.isExpanded = !this.isExpanded
},
},
}
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div class="mb-3">
<label for="feedbackLtTextArea" class="form-label"
>Lietuviškas Aprašymas</label
>
<textarea
v-model="feedbackLt"
class="form-control"
id="feedbackLtTextArea"
rows="4"
></textarea>
</div>
<div class="mb-3">
<label for="feedbackEnTextArea" class="form-label"
>Angliškas Aprašymas</label
>
<textarea
v-model="feedbackEn"
class="form-control"
id="feedbackEnTextArea"
rows="4"
></textarea>
</div>
</template>
<script>
import { mapModel } from './formHelpers'
export default {
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
computed: {
...mapModel(['feedbackLt', 'feedbackEn']),
},
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="mb-3">
<label for="nameInput" class="form-label">Lietuviškas pavadinimas</label>
<input v-model="name" type="text" class="form-control" id="nameInput" />
</div>
<div class="mb-3">
<label for="nameEnInput" class="form-label">Angliškas pavadinimas</label>
<input v-model="nameEn" type="text" class="form-control" id="nameEnInput" />
</div>
</template>
<script>
import { mapModel } from './formHelpers'
export default {
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
computed: {
...mapModel(['name', 'nameEn']),
},
}
</script>

View File

@@ -0,0 +1,27 @@
export function autoComputed(propName, defaultValue) {
return {
get() {
return this.modelValue[propName] || defaultValue
},
set(value) {
this.$emit('update:modelValue', {
...this.modelValue,
[propName]: value,
})
},
}
}
export function mapModel(props) {
const computedValues = {}
if (Array.isArray(props)) {
props.forEach((propName) => {
computedValues[propName] = autoComputed(propName, '')
})
} else {
Object.entries(props).forEach(([propName, defaultValue]) => {
computedValues[propName] = autoComputed(propName, defaultValue)
})
}
return computedValues
}

View File

@@ -4,6 +4,7 @@ import router from './router'
import store from './store'
import './assets/main.scss'
import 'bootstrap'
import './msal'
const app = createApp(App)
@@ -11,5 +12,3 @@ app.use(router)
app.use(store)
app.mount('#app')
window.r = router

View File

@@ -0,0 +1,169 @@
import * as msal from '@azure/msal-browser'
import axios from 'axios'
const ClientIdCookieName = 'ktusakacas'
const AuthorityCookieName = 'ktusakeksas'
const TenantCookieName = 'ktusalaimis'
const RequestedScopes = ['openid', 'email', 'profile']
const msalState = {
msal: null,
clientId: null, // 5931fda0-e9e0-4754-80c2-18bcb9d9561a
authority: null, // https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/v2.0
tenant: null, // 3415f2f7-f5a8-4092-b52a-003aaf844853,
stateChangeCallbacks: [],
isLoggedIn: false,
accessToken: null,
idToken: null,
email: null,
displayName: null,
debugFullTokenResponse: null,
msalRefreshTimer: null,
}
async function initializeMSAL() {
if (msalState.msal != null) {
throw new Error('MSAL was attempted to initialize second time')
}
await __loadAuthParameters()
const msalConfig = {
auth: {
clientId: msalState.clientId,
authority: `https://login.microsoftonline.com/${msalState.tenant}`,
redirectUri: window.location.protocol + '//' + window.location.host + '/',
},
}
msalState.msalRefreshTimer = setInterval(__refreshToken, 10 * 60 * 1000)
msalState.msal = new msal.PublicClientApplication(msalConfig)
msalState.msal.handleRedirectPromise().then(__handleResponse)
window.msalState = msalState
}
export function WatchMsalState(callback) {
msalState.stateChangeCallbacks.push(callback)
callback()
}
export function GetMsalState() {
return {
accessToken: msalState.accessToken,
idToken: msalState.idToken,
isLoggedIn: msalState.isLoggedIn,
debugFullTokenResponse: msalState.debugFullTokenResponse,
debugAccountInfo: msalState.debugAccountInfo,
email: msalState.email,
displayName: msalState.displayName,
}
}
export function LoginMsal() {
msalState.msal.loginRedirect({
scopes: RequestedScopes,
})
}
export function LogoutMsal() {
msalState.msal.logout()
}
async function __refreshToken() {
if (!msalState.isLoggedIn) return
msalState.debugFullTokenResponse = await msalState.msal
.acquireTokenSilent({ scopes: RequestedScopes })
.catch((error) => {
if (error instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return msalState.msal.acquireTokenRedirect({
scopes: RequestedScopes,
})
}
})
__responseObjectToMsalState()
__stateChanged()
}
async function __handleResponse(response) {
if (response !== null) {
if (__isAccountAceptable(response.account)) {
msalState.msal.setActiveAccount(response)
msalState.debugFullTokenResponse = response
__responseObjectToMsalState()
}
} else {
msalState.msal
.getAllAccounts()
.filter(__isAccountAceptable)
.forEach((account) => {
msalState.msal.setActiveAccount(account)
})
const account = msalState.msal.getActiveAccount()
if (account != null) {
msalState.debugFullTokenResponse = await msalState.msal
.acquireTokenSilent({ scopes: RequestedScopes })
.catch((error) => {
if (error instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return msalState.msal.acquireTokenRedirect({
scopes: RequestedScopes,
})
}
})
__responseObjectToMsalState()
}
}
__stateChanged()
}
function __responseObjectToMsalState() {
msalState.isLoggedIn = true
msalState.accessToken = msalState.debugFullTokenResponse.accessToken
msalState.idToken = msalState.debugFullTokenResponse.idToken
msalState.email = msalState.debugFullTokenResponse.idTokenClaims.email
msalState.displayName = msalState.debugFullTokenResponse.idTokenClaims.name
}
function __isAccountAceptable(account) {
if (account.tenantId != msalState.tenant) return false
return true
}
function __stateChanged() {
msalState.stateChangeCallbacks.forEach((cb) => cb())
}
async function __loadAuthParameters() {
await __loadAuthParametersLocalStorage()
}
async function __loadAuthParametersLocalStorage() {
const clientId = localStorage.getItem(ClientIdCookieName)
const authority = localStorage.getItem(AuthorityCookieName)
const tenant = localStorage.getItem(TenantCookieName)
if (clientId == null || authority == null || tenant == null) {
await __fetchAuthParameters()
localStorage.setItem(ClientIdCookieName, msalState.clientId)
localStorage.setItem(AuthorityCookieName, msalState.authority)
localStorage.setItem(TenantCookieName, msalState.tenant)
} else {
msalState.clientId = clientId
msalState.authority = authority
msalState.tenant = tenant
}
}
async function __fetchAuthParameters() {
var response = await axios.get('/api/AuthMetadata')
msalState.clientId = response.data.clientId
msalState.authority = response.data.authority
msalState.tenant = response.data.tenant
}
initializeMSAL()

View File

@@ -0,0 +1,34 @@
<template>
<div class="container">
<div class="card my-4" v-for="f in feedbacks" :key="f.id">
<div class="row g-0">
<div class="card-body">
<h6 class="card-text text-muted">Paskelbta: {{ f.created }}</h6>
<h5 class="card-title">Atsiliepimas:</h5>
<p class="card-text">{{ f.feedbackLt }}</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { axios } from '@/axios'
export default {
data() {
return {
feedbacks: [],
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await axios.get('/api/PublishedFeedbacks')
this.feedbacks = response.data
},
},
}
</script>

View File

@@ -0,0 +1,93 @@
<template>
<div class="container">
<h1>KTU SA Problemų sprendimo sistema</h1>
<template v-if="$store.state.msalAuth.isLoggedIn">
<div class="alert alert-success">
<h4 class="alert-heading">Tu esi prisijungęs</h4>
<span>
Kliento aplikacija turi tavo saugos raktą. Aplikacija žino, kad tavo
el. paštas yra: <b>{{ $store.state.msalAuth.email }}</b>
</span>
</div>
<template v-if="$store.state.msalAuth.isAdmin">
<div class="alert alert-warning">
<span> Tu esi administratorius. </span>
</div>
<h3>Prieigos raktas (Access token)</h3>
<a
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens"
>Dokumentacija</a
>
<pre>{{ $store.state.msalAuth.accessToken }}</pre>
<h3>Indentifikacijos raktas (ID Token)</h3>
<a
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens"
>Dokumentacija</a
>
<pre>{{ $store.state.msalAuth.idToken }}</pre>
<h4>Duomenys gaunami Indentifikacijos rakto (ID Token)</h4>
<pre>{{
$store.state.msalAuth.debugFullTokenResponse.idTokenClaims
}}</pre>
<h3>Autorizuotos sritys</h3>
<pre>{{ $store.state.msalAuth.debugFullTokenResponse.scopes }}</pre>
<h3>Serverio tokeno patikrinimas</h3>
<button type="button" class="btn btn-primary" @click="serverVerify">
Patikrinti
</button>
<button
type="button"
class="btn btn-primary"
@click="serverAdminVerify"
>
Patikrinti ar admin
</button>
<h5>Verifikacijos atsakas:</h5>
<pre>{{ verificationResult }}</pre>
</template>
</template>
<div v-else class="alert alert-danger" role="alert">
<h4 class="alert-heading">Tu neprisijungęs</h4>
<p>Prašom paspausti prisijungimo mygtuką navigacijos juostoje.</p>
</div>
</div>
</template>
<script>
import { authAxios } from '@/axios'
export default {
data() {
return {
verificationResult: null,
}
},
methods: {
serverVerify() {
this.verificationResult = null
authAxios
.get('/api/AuthMetadata/authed')
.then((response) => {
this.verificationResult = response.data
})
.catch(function (error) {
alert(error)
})
},
serverAdminVerify() {
this.verificationResult = null
authAxios
.get('/api/AuthMetadata/Admin')
.then((response) => {
this.verificationResult = response.data
})
.catch(function (error) {
alert(error)
})
},
},
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<div class="container">
<div class="alert alert-success" v-if="ok">{{ ok }}</div>
<div class="alert alert-danger" v-if="error">{{ error }}</div>
<div class="card my-4" v-for="i in issues" :key="i.id">
<div class="row g-0">
<div class="card-body">
<h6 class="card-text text-muted">Pateikta: {{ i.created }}</h6>
<h6 class="card-text text-muted">El paštas: {{ i.email }}</h6>
<h6 class="card-text text-muted">
Skelbtinas: {{ i.publishable ? '✔' : '❌' }}
</h6>
<h6 class="card-text text-muted">
Išsprestą: {{ i.solved ? '✔' : '❌' }}
</h6>
<h6 class="card-text text-muted">
Tipas: {{ issueTypes[i.issueTypeId]?.name }}
</h6>
<h5 class="card-title">Aprašymas:</h5>
<p class="card-text">{{ i.description }}</p>
<button class="btn btn-danger mx-1" @click="deleteIssue(i.id)">
<i class="bi bi-trash-fill"></i>
Ištrinti
</button>
<router-link
:to="{ name: 'IssueNewProblem', params: { id: i.id } }"
class="btn btn-primary mx-1"
>Paskelbti problemą</router-link
>
<router-link
:to="{ name: 'IssueNewFeedback', params: { id: i.id } }"
class="btn btn-primary mx-1"
>Paskelbti atsiliepimą</router-link
>
</div>
</div>
</div>
</div>
</template>
<script>
import { axios, authAxios } from '@/axios'
export default {
data() {
return {
issues: [],
issueTypes: {},
ok: null,
error: null,
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await authAxios.get('/api/Issues')
this.issues = response.data
const response2 = await axios.get('/api/IssueTypes')
response2.data.forEach((it) => {
this.issueTypes[it.id] = it
})
},
async deleteIssue(id) {
this.error = null
this.ok = null
try {
await this.$root.deleteConfirmation()
await authAxios.delete(`/api/Issues/${id}`)
this.ok = 'Sėkmingai ištrintą'
await this.fetchData()
} catch (error) {
this.error = error
}
},
},
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<div class="container">
<div class="alert alert-success" v-if="ok">{{ ok }}</div>
<div class="alert alert-danger" v-if="error">{{ error }}</div>
<h1>Naujas atsiliepimas</h1>
<div class="card my-4">
<div class="row g-0">
<div class="card-body">
<h6 class="card-text text-muted">Pateikta: {{ issue.created }}</h6>
<h6 class="card-text text-muted">El paštas: {{ issue.email }}</h6>
<h6 class="card-text text-muted">
Skelbtinas: {{ issue.publishable ? '✔' : '❌' }}
</h6>
<h6 class="card-text text-muted">
Išsprestą: {{ issue.solved ? '✔' : '❌' }}
</h6>
<h6 class="card-text text-muted">
Tipas: {{ issueTypes[issue.issueTypeId]?.name }}
</h6>
<h5 class="card-title">Aprašymas:</h5>
<p class="card-text">{{ issue.description }}</p>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="card col-lg-6 p-5">
<feedback-form v-model="feedback" />
<button @click="create" class="btn btn-primary btn-lg">
Sukurti naują atsiliepimą
</button>
</div>
</div>
</div>
</template>
<script>
import { axios, authAxios } from '@/axios'
import FeedbackForm from '@/components/forms/FeedbackForm.vue'
export default {
components: {
FeedbackForm,
},
data() {
return {
issue: null,
issueTypes: {},
feedback: {
feedbackEn: '',
feedbackLt: '',
issueId: null,
},
ok: null,
error: null,
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await authAxios.get(
`/api/Issues/${this.$route.params.id}`
)
this.issue = response.data
const response2 = await axios.get('/api/IssueTypes')
response2.data.forEach((it) => {
this.issueTypes[it.id] = it
})
},
async create() {
try {
this.feedback.issueId = this.issue.id
await authAxios.post('/api/PublishedFeedbacks', this.feedback)
this.ok = 'Sukurtą.'
} catch (error) {
this.error = error
}
},
},
watch: {
$route: 'fetchData',
},
}
</script>

View File

@@ -0,0 +1,102 @@
<template>
<div class="container">
<div class="alert alert-success" v-if="ok">{{ ok }}</div>
<div class="alert alert-danger" v-if="error">{{ error }}</div>
<h1>Nauja problema</h1>
<div class="card my-4">
<div class="row g-0">
<div class="card-body">
<h6 class="card-text text-muted">Pateikta: {{ issue.created }}</h6>
<h6 class="card-text text-muted">El paštas: {{ issue.email }}</h6>
<h6 class="card-text text-muted">
Skelbtinas: {{ issue.publishable ? '✔' : '❌' }}
</h6>
<h6 class="card-text text-muted">
Išsprestą: {{ issue.solved ? '✔' : '❌' }}
</h6>
<h6 class="card-text text-muted">
Tipas: {{ issueTypes[issue.issueTypeId]?.name }}
</h6>
<h5 class="card-title">Aprašymas:</h5>
<p class="card-text">{{ issue.description }}</p>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="card col-lg-6 p-5">
<div class="mb-3">
<label for="problemLtTextArea" class="form-label"
>Lietuviškas Aprašymas</label
>
<textarea
v-model="problem.problemLt"
class="form-control"
id="problemLtTextArea"
rows="4"
></textarea>
</div>
<div class="mb-3">
<label for="problemEnTextArea" class="form-label"
>Angliškas Aprašymas</label
>
<textarea
v-model="problem.problemEn"
class="form-control"
id="problemEnTextArea"
rows="4"
></textarea>
</div>
<button @click="create" class="btn btn-primary btn-lg">
Sukurti naują problemą
</button>
</div>
</div>
</div>
</template>
<script>
import { axios, authAxios } from '@/axios'
export default {
data() {
return {
issue: null,
issueTypes: {},
problem: {
problemLt: '',
problemEn: '',
issueId: null,
},
ok: null,
error: null,
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await authAxios.get(
`/api/Issues/${this.$route.params.id}`
)
this.issue = response.data
const response2 = await axios.get('/api/IssueTypes')
response2.data.forEach((it) => {
this.issueTypes[it.id] = it
})
},
async create() {
try {
this.problem.issueId = this.issue.id
await authAxios.post('/api/PublishedProblems', this.problem)
this.ok = 'Sukurtą.'
} catch (error) {
this.error = error
}
},
},
watch: {
$route: 'fetchData',
},
}
</script>

View File

@@ -0,0 +1,67 @@
<template>
<div class="container">
<div class="alert alert-success" v-if="ok">{{ ok }}</div>
<div class="alert alert-danger" v-if="error">{{ error }}</div>
<div class="card my-4" v-for="p in problems" :key="p.id">
<div class="row g-0">
<div class="col-lg-2 col-md-3">
<div class="card-body">
<span class="h3">{{ votes[p.id] }}</span> aktualumo&nbsp;balas
<button @click="vote(p.id)" class="btn btn-primary">
Balsuoti
</button>
</div>
</div>
<div class="col-lg col-md">
<div class="card-body">
<h6 class="card-text text-muted">Paskelbta: {{ p.created }}</h6>
<h5 class="card-title">Problema:</h5>
<p class="card-text">{{ p.problemLt }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { axios, authAxios } from '@/axios'
export default {
data() {
return {
problems: [],
votes: {},
ok: null,
error: null,
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await axios.get('/api/PublishedProblems')
this.problems = response.data
this.problems.forEach((p) => {
this.fetchVoteCount(p.id)
})
},
async fetchVoteCount(id) {
const response = await axios.get(`/api/PublishedProblems/${id}/Votes`)
this.votes[id] = response.data
},
async vote(id) {
this.ok = null
this.error = null
try {
await authAxios.post(`/api/PublishedProblems/${id}/Votes`)
this.ok = 'Sekmingai prabalsuotą.'
await this.fetchVoteCount(id)
} catch (error) {
this.error = error
}
},
},
}
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div class="container mt-2">
<div class="alert alert-danger" v-if="error">{{ error }}</div>
<div class="alert alert-success" v-if="ok">{{ ok }}</div>
<div class="row justify-content-center">
<div class="card col-lg-6 p-5">
<h1>Pateik savo problemą</h1>
<div class="mb-3">
<label for="emailInput" class="form-label">El. paštas</label>
<input
class="form-control"
type="email"
id="emailInput"
:value="$store.state.msalAuth.email"
disabled
readonly
/>
</div>
<div class="mb-3">
<label for="issueTypeSelect" class="form-label"
>Problemos tipas</label
>
<select
v-model="issue.issueTypeId"
id="issueTypeSelect"
class="form-select"
>
<option v-for="it in issueTypes" :key="it.id" :value="it.id">
{{ it.name }}
</option>
</select>
</div>
<div class="mb-3">
<label for="descriptionTextArea" class="form-label">Aprašymas</label>
<textarea
v-model="issue.description"
class="form-control"
id="descriptionTextArea"
rows="4"
aria-describedby="descriptionHelp"
></textarea>
<div id="descriptionHelp" class="form-text">
Nepamirškite paminėti kokiame modulyje susiduriate su šią problemą.
</div>
</div>
<div class="mb-3 form-check">
<input
class="form-check-input"
type="checkbox"
id="publishableCheckbox"
v-model="issue.publishable"
/>
<label class="form-check-label" for="publishable">
Ši problema turėtų būti viešai paskelbta, kad galėtų matyti kiti
studentai.
</label>
</div>
<button @click="submit" class="btn btn-primary btn-lg">Pateikti</button>
</div>
</div>
</div>
</template>
<script>
import { authAxios, axios } from '@/axios'
export default {
data() {
return {
issueTypes: [],
issue: {
description: '',
issueTypeId: null,
publishable: false,
},
error: null,
ok: null,
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await axios.get('/api/IssueTypes')
this.issueTypes = response.data
},
async submit() {
this.error = null
this.ok = null
try {
const response = await authAxios.post('/api/Issues', this.issue)
console.log(response)
this.ok = 'Problemą sekmingai pateikta.'
this.issue.description = ''
this.issue.publishable = false
this.issue.issueTypeId = null
} catch (error) {
this.error = error
}
},
},
}
</script>

View File

@@ -0,0 +1,15 @@
<template>
<div>
<div class="container">
<div class="alert alert-warning">
<b>Dėmesio!</b> Tu esi administracinėje skiltyje, visą informacija
pateikiama čia yra galimai konfidianciali.
</div>
</div>
<router-view v-slot="{ Component }">
<transition name="slide-fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>

View File

@@ -0,0 +1,62 @@
<template>
<div class="container">
<h1>Redaguoti problemos tipą</h1>
<div class="row justify-content-center">
<div class="card col-lg-6 p-5">
<issue-type-form v-model="issueType" />
<alerter ref="submitAlerter" />
<button @click="update" class="btn btn-primary btn-lg">
Išsaugoti
</button>
</div>
</div>
</div>
</template>
<script>
import { axios, authAxios } from '@/axios'
import IssueTypeForm from '@/components/forms/IssueTypeForm.vue'
import Alerter from '@/components/Alerter.vue'
export default {
components: {
IssueTypeForm,
Alerter,
},
data() {
return {
issueType: { name: '', nameEn: '' },
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await axios.get(
`/api/IssueTypes/${this.$route.params.id}`
)
this.issueType = response.data
},
async update() {
try {
if (!this.issueType.name || !this.issueType.nameEn) {
this.$refs.submitAlerter.alertTimed({
text: 'Problemos tipas turi turėti pavadinimus!!!',
})
return
}
await authAxios.patch(
`/api/IssueTypes/${this.$route.params.id}`,
this.issueType
)
this.$router.push({ name: 'AdminIssueTypes' })
} catch (error) {
this.$refs.submitAlerter.alert({
title: 'Įvyko klaidą sukuriant tipą.',
text: error,
})
}
},
},
}
</script>

View File

@@ -0,0 +1,54 @@
<template>
<div class="container">
<h1>Naujas problemos tipas</h1>
<div class="row justify-content-center">
<div class="card col-lg-6 p-5">
<issue-type-form v-model="issueType" />
<alerter ref="submitAlerter" />
<button @click="create" class="btn btn-primary btn-lg">
Sukurti naują problemos tipą
</button>
</div>
</div>
</div>
</template>
<script>
import { authAxios } from '@/axios'
import IssueTypeForm from '@/components/forms/IssueTypeForm.vue'
import Alerter from '@/components/Alerter.vue'
export default {
components: {
IssueTypeForm,
Alerter,
},
data() {
return {
issueType: { name: '', nameEn: '' },
}
},
methods: {
async create() {
try {
if (!this.issueType.name || !this.issueType.nameEn) {
this.$refs.submitAlerter.alertTimed({
text: 'Problemos tipas turi turėti pavadinimus!!!',
})
return
}
await authAxios.post('/api/IssueTypes', this.issueType)
this.$refs.submitAlerter.alertTimed({
text: 'Sėkmingai sukurtas problemos tipas.',
type: 'success',
})
this.issueType = {}
} catch (error) {
this.$refs.submitAlerter.alert({
title: 'Įvyko klaidą sukuriant tipą.',
text: error,
})
}
},
},
}
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div class="container">
<router-link
:to="{ name: 'AdminIssueTypeNew' }"
class="btn btn-primary mx-1"
>Sukurti naują problemos tipą</router-link
>
<div class="card my-4" v-for="it in issueTypes" :key="it.id">
<div class="row g-0">
<div class="card-body">
<h5 class="card-title">{{ it.name }}</h5>
<h5 class="card-title text-muted">{{ it.nameEn }}</h5>
<button class="btn btn-danger mx-1" @click="deleteIssueType(it.id)">
<i class="bi bi-trash-fill"></i>
Ištrinti
</button>
<router-link
:to="{ name: 'AdminIssueTypeEdit', params: { id: it.id } }"
class="btn btn-primary mx-1"
>Redaguoti</router-link
>
</div>
</div>
</div>
</div>
</template>
<script>
import { axios, authAxios } from '@/axios'
export default {
data() {
return {
issueTypes: [],
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await axios.get('/api/IssueTypes')
this.issueTypes = response.data
},
async deleteIssueType(id) {
try {
await this.$root.deleteConfirmation()
await authAxios.delete(`/api/IssueTypes/${id}`)
// TODO: Display some success info
await this.fetchData()
} catch (error) {
// TODO: Display error.
}
},
},
}
</script>

View File

@@ -0,0 +1,28 @@
import IssueTypes from '@/pages/admin/IssueTypes.vue'
import IssueTypeNew from '@/pages/admin/IssueTypeNew.vue'
import IssueTypeEdit from '@/pages/admin/IssueTypeEdit.vue'
import Admin from '@/pages/admin/Admin.vue'
export default [
{
path: '/admin',
component: Admin,
children: [
{
path: 'issuetypes',
name: 'AdminIssueTypes',
component: IssueTypes,
},
{
path: 'issuetypes/new',
name: 'AdminIssueTypeNew',
component: IssueTypeNew,
},
{
path: 'issuetypes/:id/edit',
name: 'AdminIssueTypeEdit',
component: IssueTypeEdit,
},
],
},
]

View File

@@ -0,0 +1,55 @@
import { createWebHistory, createRouter } from 'vue-router'
import Home from '@/pages/Home.vue'
import Submit from '@/pages/Submit.vue'
import Problems from '@/pages/Problems.vue'
import Feedbacks from '@/pages/Feedbacks.vue'
import Issues from '@/pages/Issues.vue'
import NewProblem from '@/pages/NewProblem.vue'
import NewFeedback from '@/pages/NewFeedback.vue'
import AdminRoutes from './admin'
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/new',
name: 'Submit',
component: Submit,
},
{
path: '/problems',
name: 'Problems',
component: Problems,
},
{
path: '/feedbacks',
name: 'Feedbacks',
component: Feedbacks,
},
{
path: '/issues',
name: 'Issues',
component: Issues,
},
{
path: '/issues/:id/newProblem',
name: 'IssueNewProblem',
component: NewProblem,
},
{
path: '/issues/:id/newFeedback',
name: 'IssueNewFeedback',
component: NewFeedback,
},
...AdminRoutes,
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router

View File

@@ -0,0 +1,12 @@
import { createStore, createLogger } from 'vuex'
import msalAuth from './modules/msalAuth'
const debug = process.env.NODE_ENV !== 'production'
export default createStore({
modules: {
msalAuth,
},
strict: debug,
plugins: debug ? [createLogger()] : [],
})

View File

@@ -0,0 +1,77 @@
import { WatchMsalState, GetMsalState } from '@/msal'
import axios from 'axios'
// initial state
const state = () => ({
isReady: false,
isLoggedIn: false,
isAdmin: false,
accessToken: null,
idToken: null,
email: null,
displayName: null,
debugFullTokenResponse: null,
// debugAccountInfo: null,
onReady: [],
})
// getters
const getters = {}
// actions
const actions = {
initialize({ commit }) {
WatchMsalState(async () => {
const state = GetMsalState()
let isAdmin = false
if (state.isLoggedIn && state.idToken) {
await axios
.get('/api/AuthMetadata/Admin', {
headers: {
Authorization: `Bearer ${state.idToken}`,
},
})
.then((res) => (isAdmin = res.data))
.catch((error) => console.error(error))
}
commit('setState', { ...state, isAdmin })
})
},
async waitTillReady({ commit, state }) {
if (!state.isReady) {
await new Promise((c) => {
commit('addReadyCalback', c)
})
}
},
}
// mutations
const mutations = {
setState(state, msalState) {
state.isLoggedIn = msalState.isLoggedIn
state.isAdmin = msalState.isAdmin
state.accessToken = msalState.accessToken
state.idToken = msalState.idToken
state.debugFullTokenResponse = msalState.debugFullTokenResponse
// state.debugAccountInfo = msalState.debugAccountInfo
state.email = msalState.email
state.displayName = msalState.displayName
if (!state.isReady && state.idToken) {
state.isReady = true
state.onReady.forEach((c) => c())
}
},
addReadyCalback(state, callback) {
state.onReady.push(callback)
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}

View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from "path"
// https://vitejs.dev/config/
export default defineConfig({
resolve:{
alias:{
'@' : path.resolve(__dirname, './src')
},
},
plugins: [vue()]
})

523
KTUSAPS/ClientApp/yarn.lock Normal file
View File

@@ -0,0 +1,523 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@azure/msal-browser@^2.21.0":
version "2.21.0"
resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-2.21.0.tgz#2ebe4b0874a61d64009419d4cfef1f6bb5e226b3"
integrity sha512-y+oAUJJ0m7gRUT505iGT4SDXktrRzOE3HZC0+nQyHDI+3O7BKK5NjQUwD/4V8FNNyrhxEtLk99iwJEo61epinQ==
dependencies:
"@azure/msal-common" "^6.0.0"
"@azure/msal-common@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-6.0.0.tgz#9a60c27967ae6f6678015eb6dfb0ea7cc999eb3b"
integrity sha512-Vva3snqmWPHJNDCBb4lz3D0rvZsi/0QikAxHvVFNwtNg5pP4NZE4U34tNiXN+m9KhlQFrZBPkE5rk7dIEOGcWw==
dependencies:
debug "^4.1.1"
"@babel/parser@^7.16.4":
version "7.16.12"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.12.tgz#9474794f9a650cf5e2f892444227f98e28cdf8b6"
integrity sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==
"@popperjs/core@^2.10.2":
version "2.11.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9"
integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==
"@vitejs/plugin-vue@^2.0.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.1.0.tgz#ddf5e0059f84f2ff649afc25ce5a59211e670542"
integrity sha512-AZ78WxvFMYd8JmM/GBV6a6SGGTU0GgN/0/4T+FnMMsLzFEzTeAUwuraapy50ifHZsC+G5SvWs86bvaCPTneFlA==
"@vue/compiler-core@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.29.tgz#b06097ab8ff0493177c68c5ea5b63d379a061097"
integrity sha512-RePZ/J4Ub3sb7atQw6V6Rez+/5LCRHGFlSetT3N4VMrejqJnNPXKUt5AVm/9F5MJriy2w/VudEIvgscCfCWqxw==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/shared" "3.2.29"
estree-walker "^2.0.2"
source-map "^0.6.1"
"@vue/compiler-dom@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.29.tgz#ad0ead405bd2f2754161335aad9758aa12430715"
integrity sha512-y26vK5khdNS9L3ckvkqJk/78qXwWb75Ci8iYLb67AkJuIgyKhIOcR1E8RIt4mswlVCIeI9gQ+fmtdhaiTAtrBQ==
dependencies:
"@vue/compiler-core" "3.2.29"
"@vue/shared" "3.2.29"
"@vue/compiler-sfc@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.29.tgz#f76d556cd5fca6a55a3ea84c88db1a2a53a36ead"
integrity sha512-X9+0dwsag2u6hSOP/XsMYqFti/edvYvxamgBgCcbSYuXx1xLZN+dS/GvQKM4AgGS4djqo0jQvWfIXdfZ2ET68g==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.29"
"@vue/compiler-dom" "3.2.29"
"@vue/compiler-ssr" "3.2.29"
"@vue/reactivity-transform" "3.2.29"
"@vue/shared" "3.2.29"
estree-walker "^2.0.2"
magic-string "^0.25.7"
postcss "^8.1.10"
source-map "^0.6.1"
"@vue/compiler-ssr@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.29.tgz#37b15b32dcd2f6b410bb61fca3f37b1a92b7eb1e"
integrity sha512-LrvQwXlx66uWsB9/VydaaqEpae9xtmlUkeSKF6aPDbzx8M1h7ukxaPjNCAXuFd3fUHblcri8k42lfimHfzMICA==
dependencies:
"@vue/compiler-dom" "3.2.29"
"@vue/shared" "3.2.29"
"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.0.0-beta.18":
version "6.0.0-beta.21.1"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.21.1.tgz#f1410f53c42aa67fa3b01ca7bdba891f69d7bc97"
integrity sha512-FqC4s3pm35qGVeXRGOjTsRzlkJjrBLriDS9YXbflHLsfA9FrcKzIyWnLXoNm+/7930E8rRakXuAc2QkC50swAw==
"@vue/reactivity-transform@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.29.tgz#a08d606e10016b7cf588d1a43dae4db2953f9354"
integrity sha512-YF6HdOuhdOw6KyRm59+3rML8USb9o8mYM1q+SH0G41K3/q/G7uhPnHGKvspzceD7h9J3VR1waOQ93CUZj7J7OA==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.29"
"@vue/shared" "3.2.29"
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/reactivity@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.29.tgz#afdc9c111d4139b14600be17ad80267212af6052"
integrity sha512-Ryhb6Gy62YolKXH1gv42pEqwx7zs3n8gacRVZICSgjQz8Qr8QeCcFygBKYfJm3o1SccR7U+bVBQDWZGOyG1k4g==
dependencies:
"@vue/shared" "3.2.29"
"@vue/runtime-core@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.29.tgz#fb8577b2fcf52e8d967bd91cdf49ab9fb91f9417"
integrity sha512-VMvQuLdzoTGmCwIKTKVwKmIL0qcODIqe74JtK1pVr5lnaE0l25hopodmPag3RcnIcIXe+Ye3B2olRCn7fTCgig==
dependencies:
"@vue/reactivity" "3.2.29"
"@vue/shared" "3.2.29"
"@vue/runtime-dom@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.29.tgz#35e9a2bf04ef80b86ac2ca0e7b2ceaccf1e18f01"
integrity sha512-YJgLQLwr+SQyORzTsBQLL5TT/5UiV83tEotqjL7F9aFDIQdFBTCwpkCFvX9jqwHoyi9sJqM9XtTrMcc8z/OjPA==
dependencies:
"@vue/runtime-core" "3.2.29"
"@vue/shared" "3.2.29"
csstype "^2.6.8"
"@vue/server-renderer@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.29.tgz#ea6afa361b9c781a868c8da18c761f9b7bc89102"
integrity sha512-lpiYx7ciV7rWfJ0tPkoSOlLmwqBZ9FTmQm33S+T4g0j1fO/LmhJ9b9Ctl1o5xvIFVDk9QkSUWANZn7H2pXuxVw==
dependencies:
"@vue/compiler-ssr" "3.2.29"
"@vue/shared" "3.2.29"
"@vue/shared@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.29.tgz#07dac7051117236431d2f737d16932aa38bbb925"
integrity sha512-BjNpU8OK6Z0LVzGUppEk0CMYm/hKDnZfYdjSmPOs0N+TR1cLKJAkDwW8ASZUvaaSLEi6d3hVM7jnWnX+6yWnHw==
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
axios@^0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a"
integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==
dependencies:
follow-redirects "^1.14.7"
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bootstrap-icons@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.7.2.tgz#4024e081e2c850602552e1fed6451e682d09322a"
integrity sha512-NiR2PqC73AQOPdVSu6GJfnk+hN2z6powcistXk1JgPnKuoV2FSdSl26w931Oz9HYbKCcKUSB6ncZTYJAYJl3QQ==
bootstrap@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.3.tgz#ba081b0c130f810fa70900acbc1c6d3c28fa8f34"
integrity sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
"chokidar@>=3.0.0 <4.0.0":
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
csstype@^2.6.8:
version "2.6.19"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa"
integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==
debug@^4.1.1:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
dependencies:
ms "2.1.2"
esbuild-android-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44"
integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==
esbuild-darwin-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72"
integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==
esbuild-darwin-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a"
integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==
esbuild-freebsd-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85"
integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==
esbuild-freebsd-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52"
integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==
esbuild-linux-32@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69"
integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==
esbuild-linux-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3"
integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==
esbuild-linux-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1"
integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==
esbuild-linux-arm@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe"
integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==
esbuild-linux-mips64le@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7"
integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==
esbuild-linux-ppc64le@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2"
integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==
esbuild-netbsd-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038"
integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==
esbuild-openbsd-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7"
integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==
esbuild-sunos-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4"
integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==
esbuild-windows-32@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7"
integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==
esbuild-windows-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294"
integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==
esbuild-windows-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3"
integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==
esbuild@^0.13.12:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf"
integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==
optionalDependencies:
esbuild-android-arm64 "0.13.15"
esbuild-darwin-64 "0.13.15"
esbuild-darwin-arm64 "0.13.15"
esbuild-freebsd-64 "0.13.15"
esbuild-freebsd-arm64 "0.13.15"
esbuild-linux-32 "0.13.15"
esbuild-linux-64 "0.13.15"
esbuild-linux-arm "0.13.15"
esbuild-linux-arm64 "0.13.15"
esbuild-linux-mips64le "0.13.15"
esbuild-linux-ppc64le "0.13.15"
esbuild-netbsd-64 "0.13.15"
esbuild-openbsd-64 "0.13.15"
esbuild-sunos-64 "0.13.15"
esbuild-windows-32 "0.13.15"
esbuild-windows-64 "0.13.15"
esbuild-windows-arm64 "0.13.15"
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
follow-redirects@^1.14.7:
version "1.14.7"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-core-module@^2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
dependencies:
has "^1.0.3"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
magic-string@^0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
dependencies:
sourcemap-codec "^1.4.4"
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nanoid@^3.1.30:
version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
postcss@^8.1.10, postcss@^8.4.5:
version "8.4.5"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95"
integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==
dependencies:
nanoid "^3.1.30"
picocolors "^1.0.0"
source-map-js "^1.0.1"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
resolve@^1.20.0:
version "1.22.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
dependencies:
is-core-module "^2.8.1"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
rollup@^2.59.0:
version "2.66.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.66.1.tgz#366b0404de353c4331d538c3ad2963934fcb4937"
integrity sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w==
optionalDependencies:
fsevents "~2.3.2"
sass@^1.49.0:
version "1.49.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.0.tgz#65ec1b1d9a6bc1bae8d2c9d4b392c13f5d32c078"
integrity sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sourcemap-codec@^1.4.4:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
vite@^2.7.2:
version "2.7.13"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.7.13.tgz#99b56e27dfb1e4399e407cf94648f5c7fb9d77f5"
integrity sha512-Mq8et7f3aK0SgSxjDNfOAimZGW9XryfHRa/uV0jseQSilg+KhYDSoNb9h1rknOy6SuMkvNDLKCYAYYUMCE+IgQ==
dependencies:
esbuild "^0.13.12"
postcss "^8.4.5"
resolve "^1.20.0"
rollup "^2.59.0"
optionalDependencies:
fsevents "~2.3.2"
vue-router@^4.0.12:
version "4.0.12"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.12.tgz#8dc792cddf5bb1abcc3908f9064136de7e13c460"
integrity sha512-CPXvfqe+mZLB1kBWssssTiWg4EQERyqJZes7USiqfW9B5N2x+nHlnsM1D3b5CaJ6qgCvMmYJnz+G0iWjNCvXrg==
dependencies:
"@vue/devtools-api" "^6.0.0-beta.18"
vue@^3.2.25:
version "3.2.29"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.29.tgz#3571b65dbd796d3a6347e2fd45a8e6e11c13d56a"
integrity sha512-cFIwr7LkbtCRanjNvh6r7wp2yUxfxeM2yPpDQpAfaaLIGZSrUmLbNiSze9nhBJt5MrZ68Iqt0O5scwAMEVxF+Q==
dependencies:
"@vue/compiler-dom" "3.2.29"
"@vue/compiler-sfc" "3.2.29"
"@vue/runtime-dom" "3.2.29"
"@vue/server-renderer" "3.2.29"
"@vue/shared" "3.2.29"
vuex@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.2.tgz#f896dbd5bf2a0e963f00c67e9b610de749ccacc9"
integrity sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==
dependencies:
"@vue/devtools-api" "^6.0.0-beta.11"

View File

@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace KTUSAPS.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Produces("application/json")]
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;
public AuthMetadataController(IConfiguration configuration)
{
_configuration = configuration;
}
/// <summary>
/// Get authethication metadata needed to obtain token.
/// </summary>
/// <returns></returns>
[HttpGet]
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.Status200OK)]
[HttpGet("Authed")]
public bool IsAuthed() => true;
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[HttpGet("Claims")]
public IEnumerable<object> Claims() => User.Claims.Select((c) => new { c.Type, c.Value });
[Authorize("admin")]
[ProducesResponseType(StatusCodes.Status200OK)]
[HttpGet("Admin")]
public bool IsAdmin() => true;
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using KTUSAPS.Data;
using KTUSAPS.Data.Model;
using KTUSAPS.Extensions;
using Microsoft.AspNetCore.Authorization;
namespace KTUSAPS.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class IssueTypesController : ControllerBase
{
private readonly SAPSDataContext _context;
public IssueTypesController(SAPSDataContext context)
{
_context = context;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<IssueType>>> GetIssueTypes()
{
return await _context.IssueTypes.ToListAsync();
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize("admin")]
public async Task<ActionResult<IssueType>> CreateIssueType([FromBody] IssueType issueType)
{
if (issueType == null)
return BadRequest("No data provided for object to be created.");
if (issueType.Id != default)
return BadRequest("Id has been set on create request, please do not do that, set id to 0 or ommit it.");
if (issueType.Issues != null)
return BadRequest("Do not privide navigation property values.");
_context.IssueTypes.Add(issueType);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetIssueType), new { id = issueType.Id }, issueType);
}
// GET: api/IssueTypes/5
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IssueType>> GetIssueType(int id)
{
var issueType = await _context.IssueTypes.FindAsync(id);
if (issueType == null)
return NotFound();
return Ok(issueType);
}
[HttpPatch("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize("admin")]
public async Task<IActionResult> UpdateIssueType(int id, IssueType issueType)
{
var databaseIssueType = await _context.IssueTypes.FindAsync(id);
if (databaseIssueType == default)
return NotFound();
var eIssueType = _context.Attach(databaseIssueType);
eIssueType.MovePropertyDataWhiteList(issueType, new string[] {
nameof(databaseIssueType.Name),
nameof(databaseIssueType.NameEn),
});
await _context.SaveChangesAsync();
return Ok(eIssueType.Entity);
}
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize("admin")]
public async Task<IActionResult> DeleteIssueType(int id)
{
var issueType = await _context.IssueTypes.FindAsync(id);
if (issueType == default)
return NotFound();
_context.IssueTypes.Remove(issueType);
await _context.SaveChangesAsync();
return NoContent();
}
}
}

View File

@@ -0,0 +1,121 @@
using KTUSAPS.Auth;
using KTUSAPS.Data.Model;
using KTUSAPS.Extensions;
using KTUSAPS.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace KTUSAPS.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class IssuesController : ControllerBase
{
private readonly Data.SAPSDataContext dataContext;
private readonly IAuthorizationService _authorizationService;
public IssuesController(Data.SAPSDataContext dataContext, IAuthorizationService authorizationService)
{
this.dataContext = dataContext;
_authorizationService = authorizationService;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize]
public async Task<ActionResult<IEnumerable<Issue>>> GetIssues([FromQuery] RequestScope requestScope = RequestScope.All)
{
if (requestScope == RequestScope.All)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, "admin");
if (!authorizationResult.Succeeded)
return Forbid();
return await dataContext.Issues.ToListAsync();
} else if (requestScope == RequestScope.My)
return await dataContext.Issues.Where(i => i.UserID == User.GetUserId()).ToListAsync();
throw new NotImplementedException();
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize]
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.");
if (issueToCreate.UserID != default || issueToCreate.Email != default)
return BadRequest("Do not provide indentity values.");
issueToCreate.UserID = User.GetUserId();
issueToCreate.Email = User.GetEmail();
var createdValue = await dataContext.AddAsync(issueToCreate);
await dataContext.SaveChangesAsync();
return CreatedAtAction(nameof(GetIssue), new { Id = createdValue.Entity.Id }, createdValue.Entity);
}
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize]
public async Task<ActionResult<Issue>> GetIssue(int id)
{
var issue = dataContext.Issues.AsQueryable().Where(i => i.Id == id).FirstOrDefault();
if(issue == default)
return NotFound();
var authorizationResult = await _authorizationService.AuthorizeAsync(User, issue, new MyIssueRequirement());
if(authorizationResult.Succeeded)
return Ok(issue);
return Forbid();
}
[HttpPatch("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize("admin")]
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)]
[Authorize("admin")]
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

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using KTUSAPS.Data;
using KTUSAPS.Data.Model;
using KTUSAPS.Extensions;
using Microsoft.AspNetCore.Authorization;
namespace KTUSAPS.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PublishedFeedbacksController : ControllerBase
{
private readonly SAPSDataContext _context;
public PublishedFeedbacksController(SAPSDataContext context)
{
_context = context;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<PublishedFeedback>>> GetPublishedFeedbacks()
{
return await _context.PublishedFeedbacks.ToListAsync();
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize("admin")]
public async Task<ActionResult<PublishedFeedback>> PostPublishedFeedback(PublishedFeedback publishedFeedback)
{
if (publishedFeedback == null)
return BadRequest("No data provided for object to be created.");
if (publishedFeedback.Id != default)
return BadRequest("Id has been set on create request, please do not do that, set id to 0 or ommit it.");
if (publishedFeedback.Issue != null)
return BadRequest("Do not privide navigation property values.");
_context.PublishedFeedbacks.Add(publishedFeedback);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetPublishedFeedback), new { id = publishedFeedback.Id }, publishedFeedback);
}
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PublishedFeedback>> GetPublishedFeedback(int id)
{
var publishedFeedback = await _context.PublishedFeedbacks.FindAsync(id);
if (publishedFeedback == null)
return NotFound();
return publishedFeedback;
}
[HttpPatch("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize("admin")]
public async Task<ActionResult<PublishedFeedback>> UpdatePublishedFeedback(int id, PublishedFeedback publishedFeedback)
{
var databasePublishedFeedback = await _context.PublishedFeedbacks.FindAsync(id);
if (databasePublishedFeedback == default)
return NotFound();
var ePublishedFeedback = _context.Attach(databasePublishedFeedback);
ePublishedFeedback.MovePropertyDataWhiteList(publishedFeedback, new string[]
{
nameof(databasePublishedFeedback.FeedbackLt),
nameof(databasePublishedFeedback.FeedbackEn),
nameof(databasePublishedFeedback.Created),
nameof(databasePublishedFeedback.IssueId),
});
await _context.SaveChangesAsync();
return Ok(ePublishedFeedback.Entity);
}
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize("admin")]
public async Task<IActionResult> DeletePublishedFeedback(int id)
{
var publishedFeedback = await _context.PublishedFeedbacks.FindAsync(id);
if (publishedFeedback == null)
return NotFound();
_context.PublishedFeedbacks.Remove(publishedFeedback);
await _context.SaveChangesAsync();
return NoContent();
}
}
}

View File

@@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using KTUSAPS.Data;
using KTUSAPS.Data.Model;
using KTUSAPS.Extensions;
using Microsoft.AspNetCore.Authorization;
namespace KTUSAPS.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PublishedProblemsController : ControllerBase
{
private readonly SAPSDataContext _context;
public PublishedProblemsController(SAPSDataContext context)
{
_context = context;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<PublishedProblem>>> GetPublishedProblems()
{
return await _context.PublishedProblems.ToListAsync();
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize("admin")]
public async Task<ActionResult<PublishedProblem>> CreatePublishedProblem([FromBody] PublishedProblem publishedProblem)
{
if (publishedProblem == null)
return BadRequest("No data provided for object to be created.");
if (publishedProblem.Id != default)
return BadRequest("Id has been set on create request, please do not do that, set id to 0 or ommit it.");
if (publishedProblem.Issue != null || publishedProblem.Solution != null || publishedProblem.Votes != null)
return BadRequest("Do not provide navigation property values.");
_context.PublishedProblems.Add(publishedProblem);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetPublishedProblem), new { id = publishedProblem.Id }, publishedProblem);
}
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PublishedProblem>> GetPublishedProblem(int id)
{
var publishedProblem = await _context.PublishedProblems.FindAsync(id);
if (publishedProblem == null)
return NotFound();
return publishedProblem;
}
[HttpPatch("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize("admin")]
public async Task<ActionResult<PublishedProblem>> UpdatePublishedProblem(int id, PublishedProblem publishedProblem)
{
var databasePublishedProblem = await _context.PublishedProblems.FindAsync(id);
if (databasePublishedProblem == default)
return NotFound();
var ePublishedProblem = _context.Attach(databasePublishedProblem);
ePublishedProblem.MovePropertyDataWhiteList(publishedProblem, new string[]
{
nameof(databasePublishedProblem.ProblemLt),
nameof(databasePublishedProblem.ProblemEn),
nameof(databasePublishedProblem.Created),
nameof(databasePublishedProblem.IssueId),
nameof(databasePublishedProblem.SolutionId),
});
await _context.SaveChangesAsync();
return Ok(ePublishedProblem.Entity);
}
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize("admin")]
public async Task<IActionResult> DeletePublishedProblem(int id)
{
var publishedProblem = await _context.PublishedProblems.FindAsync(id);
if (publishedProblem == null)
return NotFound();
_context.PublishedProblems.Remove(publishedProblem);
await _context.SaveChangesAsync();
return NoContent();
}
[HttpGet("{id}/Votes")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<int>> GetVoteCount(int id)
{
var publishedProblem = await _context.PublishedProblems.FindAsync(id);
if (publishedProblem == null)
return NotFound();
return (await _context.Votes
.Where(v => v.ProblemId == publishedProblem.Id)
.ToListAsync())
.Count();
}
[HttpPost("{id}/Votes")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize]
public async Task<ActionResult<Vote>> Vote(int id)
{
var publishedProblem = await _context.PublishedProblems.FindAsync(id);
if (publishedProblem == null)
return NotFound();
var vote = new Vote()
{
Problem = publishedProblem,
UserId = User.GetUserId(),
};
_context.Votes.Add(vote);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetVote), new { id = id, userId = vote.UserId }, vote);
}
[HttpPost("{id}/Votes/{userId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Vote>> GetVote(int id, string userId)
{
var publishedProblem = await _context.PublishedProblems.FindAsync(id);
if (publishedProblem == null)
return NotFound();
var vote = await _context.Votes
.Where(v => v.ProblemId == publishedProblem.Id && v.UserId == userId)
.FirstOrDefaultAsync();
if (vote == default)
return NotFound();
return vote;
}
}
}

View File

@@ -0,0 +1,41 @@
using System.Linq;
using System.Security.Claims;
namespace KTUSAPS.Extensions
{
public static class ClaimsPrincipalExtensions
{
private static string getClaimValue(ClaimsPrincipal claimsPrincipal, string claimType)
{
return claimsPrincipal.Claims.FirstOrDefault(c => c.Type == claimType)?.Value;
}
public static string GetUserId(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal == null)
return null;
return getClaimValue(claimsPrincipal, ClaimTypes.NameIdentifier);
}
public static string GetName(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal == null)
return null;
return getClaimValue(claimsPrincipal, "name");
}
public static string GetEmail(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal == null)
return null;
return getClaimValue(claimsPrincipal, ClaimTypes.Email);
}
public static string GetObjectId(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal == null)
return null;
return getClaimValue(claimsPrincipal, "http://schemas.microsoft.com/identity/claims/objectidentifier");
}
}
}

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

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>KTUSA_PS</RootNamespace>
<RootNamespace>KTUSAPS</RootNamespace>
</PropertyGroup>
<PropertyGroup>
@@ -10,9 +10,25 @@
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
</PropertyGroup>
<PropertyGroup>
<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.8" />
<PackageReference Include="VueCliMiddleware" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.6.3" />
<PackageReference Include="VueCliMiddleware" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
@@ -23,7 +39,7 @@
</ItemGroup>
<ItemGroup>
<None Remove="ClientApp\src\assets\NewFile.txt" />
<ProjectReference Include="..\KTUSAPS.Data\KTUSAPS.Data.csproj" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
@@ -40,12 +56,10 @@
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build -- --prod" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build:ssr -- --prod" Condition=" '$(BuildServerSideRenderer)' == 'true' " />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" />
<DistFiles Include="$(SpaRoot)node_modules\**" Condition="'$(BuildServerSideRenderer)' == 'true'" />
<DistFiles Include="$(SpaRoot)dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>

View File

@@ -0,0 +1,8 @@
namespace KTUSAPS.Models
{
public enum RequestScope
{
All,
My,
}
}

View File

@@ -7,7 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace KTUSA_PS
namespace KTUSAPS
{
public class Program
{

View File

@@ -16,10 +16,9 @@
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"KTUSA_PS": {
"KTUSAPS": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},

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;
}
}
}

128
KTUSAPS/Startup.cs Normal file
View File

@@ -0,0 +1,128 @@
using KTUSAPS.Auth;
using KTUSAPS.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using VueCliMiddleware;
namespace KTUSAPS
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
options.SuppressAsyncSuffixInActionNames = false
)
.AddControllersAsServices();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.MetadataAddress = "https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/v2.0/.well-known/openid-configuration";
options.Audience = Configuration["ClientId"];
//options.Authority = Configuration["Authority"];
});
services.AddAuthorization((configure) =>
{
configure.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
configure.AddPolicy("admin", new AuthorizationPolicyBuilder(configure.DefaultPolicy)
.AddRequirements(new AdminRequirement())
.Build());
});
var connectionString = Configuration.GetConnectionString("Main");
services.AddDbContext<Data.SAPSDataContext>((options) => options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
services.AddHostedService<DatabaseInitializationService>();
services.AddSingleton<IAuthorizationHandler, SaPsAuthorizationHandler>();
services.AddSwaggerGen(options =>
{
options.OperationFilter<AuthorizeCheckOperationFilter>();
options.AddSecurityDefinition("msad", new Microsoft.OpenApi.Models.OpenApiSecurityScheme()
{
Type = Microsoft.OpenApi.Models.SecuritySchemeType.OAuth2,
Flows = new Microsoft.OpenApi.Models.OpenApiOAuthFlows()
{
AuthorizationCode = new Microsoft.OpenApi.Models.OpenApiOAuthFlow()
{
AuthorizationUrl = new Uri("https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/oauth2/v2.0/authorize"),
TokenUrl = new Uri("https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/oauth2/v2.0/token"),
Scopes = new Dictionary<string, string>
{
{ "openid", "Access to user's id" },
{ "profile", "Access to user's name" },
{ "email", "Access to email" }
}
}
}
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API");
c.OAuthClientId(Configuration["ClientId"]);
c.OAuthAppName("KTUSA Problem<65> sistema");
c.OAuthUsePkce();
});
}
app.UseSwagger(c =>
{
});
app.UseRouting();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp/";
if (env.IsDevelopment())
{
spa.UseVueCli(npmScript: "serve");
}
});
}
}
}

View File

@@ -1,4 +1,7 @@
{
"ConnectionStrings": {
"Main": "Server=localhost;User=saps_dev;Password=;Database=saps_dev"
},
"Logging": {
"LogLevel": {
"Default": "Information",
@@ -6,7 +9,7 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ClientId": "5931fda0-e9e0-4754-80c2-18bcb9d9561a",
"ClientId": "411a8715-3d63-4a03-9be8-9370d920e36f",
"Authority": "https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/v2.0",
"Tenant": "3415f2f7-f5a8-4092-b52a-003aaf844853"
}

View File

@@ -1,4 +1,7 @@
{
"ConnectionStrings": {
"Main": "Server=localhost;User=saps;Password=FuhcMfapPkGOH8DjPSAw;Database=saps"
},
"Logging": {
"LogLevel": {
"Default": "Information",
@@ -7,7 +10,7 @@
}
},
"AllowedHosts": "*",
"ClientId": "5931fda0-e9e0-4754-80c2-18bcb9d9561a",
"ClientId": "411a8715-3d63-4a03-9be8-9370d920e36f",
"Authority": "https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/v2.0",
"Tenant": "3415f2f7-f5a8-4092-b52a-003aaf844853"
}