Upload project.

This commit is contained in:
StevenJW 2020-06-09 21:28:47 +01:00
parent 36a582d89d
commit 00bf32859f
70 changed files with 38746 additions and 0 deletions

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Aya_Backend.Tests</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aya-Backend\Aya-Backend.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,36 @@
using Aya_Backend.Controllers;
using Aya_Backend.Data;
using Aya_Backend.Data.Repositories.UserRepositories;
using Aya_Backend.Tests.Data;
using Microsoft.AspNetCore.Mvc;
using NUnit.Framework;
using System.Linq;
using System.Threading.Tasks;
namespace Aya_Backend.Tests
{
[TestFixture]
public class UserControllerTests
{
private readonly int validUserID = 2;
[Test]
public async Task GetUser_CorrectUserReturned()
{
var expected = MockData.Users.FirstOrDefault(u => u.ID == validUserID);
var mockContext = new MockRepository(MockData.Users, null, null);
var controller = new UserController(mockContext);
var result = await controller.GetUser(validUserID);
Assert.IsNotNull(result);
var objectResult = result as OkObjectResult;
Assert.IsNotNull(objectResult);
var modelResult = objectResult.Value as User;
Assert.IsNotNull(modelResult);
Assert.AreEqual(expected.ID, modelResult.ID);
Assert.AreEqual(expected.Username, modelResult.Username);
}
}
}

View File

@ -0,0 +1,14 @@
using NUnit.Framework;
namespace Aya_Backend.Tests
{
[TestFixture]
public class WorkbookControllerTests
{
[Test]
public void Test1()
{
Assert.Pass();
}
}
}

View File

@ -0,0 +1,14 @@
using NUnit.Framework;
namespace Aya_Backend.Tests
{
[TestFixture]
public class WorkpageControllerTests
{
[Test]
public void Test1()
{
Assert.Pass();
}
}
}

View File

@ -0,0 +1,17 @@
using Aya_Backend.Data;
using System;
using System.Collections.Generic;
using System.Text;
namespace Aya_Backend.Tests.Data
{
public static class MockData
{
public static List<User> Users = new List<User>
{
new User() { ID = 1, Username = "User 1" },
new User() { ID = 2, Username = "User 2" },
new User() { ID = 3, Username = "User 3" }
};
}
}

View File

@ -0,0 +1,23 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Aya-Backend.Tests")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("Aya-Backend.Tests")]
[assembly: System.Reflection.AssemblyTitleAttribute("Aya-Backend.Tests")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.

View File

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29728.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aya-Backend", "Aya-Backend\Aya-Backend.csproj", "{150093C3-BCCB-49E6-B39E-74E2BC7831E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aya-Backend.Tests", "Aya-Backend.Tests\Aya-Backend.Tests.csproj", "{ADE9EB7C-FBCE-4498-B9F2-8164115F0492}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{150093C3-BCCB-49E6-B39E-74E2BC7831E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{150093C3-BCCB-49E6-B39E-74E2BC7831E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{150093C3-BCCB-49E6-B39E-74E2BC7831E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{150093C3-BCCB-49E6-B39E-74E2BC7831E5}.Release|Any CPU.Build.0 = Release|Any CPU
{ADE9EB7C-FBCE-4498-B9F2-8164115F0492}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ADE9EB7C-FBCE-4498-B9F2-8164115F0492}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ADE9EB7C-FBCE-4498-B9F2-8164115F0492}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ADE9EB7C-FBCE-4498-B9F2-8164115F0492}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {288D4B34-3BEA-4CF7-8E3F-7F7AED944F18}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Aya_Backend</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Data\Migrations\20200503195721_InitialCreate.cs" />
<Compile Remove="Data\Migrations\20200503195721_InitialCreate.Designer.cs" />
<Compile Remove="Data\Migrations\20200503223253_AddContentToWorkpage.cs" />
<Compile Remove="Data\Migrations\20200503223253_AddContentToWorkpage.Designer.cs" />
<Compile Remove="Data\Migrations\20200504225546_BetterWorkpageSeedData.cs" />
<Compile Remove="Data\Migrations\20200504225546_BetterWorkpageSeedData.Designer.cs" />
<Compile Remove="Data\Migrations\20200505163516_PrimaryKeysIdToInt.cs" />
<Compile Remove="Data\Migrations\20200505163516_PrimaryKeysIdToInt.Designer.cs" />
<Compile Remove="Data\Migrations\20200505223715_User-Store.cs" />
<Compile Remove="Data\Migrations\20200505223715_User-Store.Designer.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.3" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Aya_Backend.Data;
using Aya_Backend.Data.Repositories.UserRepositories;
using Aya_Backend.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Aya_Backend.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly IRepository _context;
private readonly IUserService _userService;
public UserController(IRepository context, IUserService userService)
{
_context = context;
_userService = userService;
}
//Returns the user object of the user currently logged in (from token passed).
[Route("get")]
[HttpGet]
public async Task<IActionResult> GetUser()
{
string userId = User.FindFirst(ClaimTypes.Name)?.Value;
var user = _context.GetUser(Int32.Parse(userId));
if (user == null)
return NotFound();
return Ok(user);
}
//Returns the user object if login credentials are valid (user object includes jwt token).
[AllowAnonymous]
[Route("login")]
[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginDTO login)
{
var user = _userService.Authenticate(login.Username, login.Password);
if (user == null)
return BadRequest();
return Ok(user);
}
//Creates a new user object with the credentials passed and authenticates them.
[AllowAnonymous]
[Route("create")]
[HttpPost]
public async Task<IActionResult> CreateAccount([FromBody] LoginDTO account)
{
var user = _context.AddUser(new User() { Username = account.Username, Password = account.Password });
return Ok(user);
}
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Aya_Backend.Data.Repositories.UserRepositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Aya_Backend.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class WorkbookController : ControllerBase
{
private readonly IRepository _context;
public WorkbookController(IRepository context)
{
_context = context;
}
//Gets a workbook if the logged in user is owner of the workbook.
[Route("get")]
[HttpGet]
public async Task<IActionResult> GetWorkbook(int id)
{
string userId = User.FindFirst(ClaimTypes.Name)?.Value;
var workbook = _context.GetWorkbook(id);
if (workbook.OwnerID != Int32.Parse(userId))
return BadRequest();
return Ok(workbook);
}
//Adds a workbook to the user currently logged in.
[Route("add")]
[HttpPost]
public async Task<IActionResult> AddWorkbook(string name)
{
string userId = User.FindFirst(ClaimTypes.Name)?.Value;
return Ok(_context.AddWorkbook(name, Int32.Parse(userId)));
}
//Gets a list of all of the workbooks that belong to that user.
[Route("userworkbooks")]
[HttpGet]
public async Task<IActionResult> GetUserWorkbooks()
{
Console.WriteLine("workbook");
string userId = User.FindFirst(ClaimTypes.Name)?.Value;
return Ok(_context.GetUserWorkbooks(Int32.Parse(userId)));
}
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Aya_Backend.Data;
using Aya_Backend.Data.Repositories.UserRepositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Aya_Backend.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class WorkpageController : ControllerBase
{
private readonly IRepository _context;
public WorkpageController(IRepository context)
{
_context = context;
}
//Returns a workpage, aslong as the user is owner.
[Route("get")]
[HttpGet]
public async Task<IActionResult> GetWorkpage(int id)
{
string userId = User.FindFirst(ClaimTypes.Name)?.Value;
int workpageOwner = _context.GetWorkpageOwnerID(id);
if (workpageOwner != Int32.Parse(userId))
return BadRequest();
return Ok(_context.GetWorkpage(id));
}
//Returns a list of the workpages from a workbook (given a workbook id).
//Only returns them if the workbook belongs to the user.
[Route("fromworkbook")]
[HttpGet]
public async Task<IActionResult> GetFromWorkbook(int id)
{
string userId = User.FindFirst(ClaimTypes.Name)?.Value;
var workbook = _context.GetWorkbook(id);
if (workbook.OwnerID != Int32.Parse(userId))
return BadRequest();
return Ok(_context.GetWorkbookWorkpages(id));
}
//Adds a workpage with the given name and workbookid, aslong as they are the owner of the workbook.
[Route("add")]
[HttpPost]
public async Task<IActionResult> AddWorkpage(string name, int workbookid)
{
string userId = User.FindFirst(ClaimTypes.Name)?.Value;
var workbook = _context.GetWorkbook(workbookid);
if (workbook.OwnerID != Int32.Parse(userId))
return BadRequest();
var wp = new Workpage() { Name = name, WorkbookID = workbookid, Content = String.Empty };
_context.AddWorkpage(wp);
return Ok("true");
}
//Updates the content of a workpage from the request body, as long as they are the owner of the workbook.
[Route("updateworkpagecontent")]
[HttpPost]
public async Task<IActionResult> UpdateWorkpageContent(int id, [FromBody] string content)
{
string userId = User.FindFirst(ClaimTypes.Name)?.Value;
var workbook = _context.GetWorkbook(id);
if (workbook.OwnerID != Int32.Parse(userId))
return BadRequest();
_context.UpdateWorkbookContent(id, content);
return Ok("true");
}
}
}

View File

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Aya_Backend.Data.Repositories
{
public class AyaContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Workbook> Workbooks { get; set; }
public DbSet<Workpage> Workpages { get; set; }
private IHostingEnvironment HostEnv { get; }
public AyaContext(DbContextOptions<AyaContext> options, IHostingEnvironment env) : base(options)
{
HostEnv = env;
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite("Data Source=aya.db");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
if (HostEnv != null && HostEnv.IsDevelopment())
{
modelBuilder.Entity<User>()
.HasData(
new User() { ID = 1, Username = "User1", Password = "Password1_" },
new User() { ID = 2, Username = "User2", Password = "Password2_" },
new User() { ID = 3, Username = "User3", Password = "Password3_" }
);
modelBuilder.Entity<Workbook>()
.HasData(
new Workbook() { ID = 1, Name = "Workbook 1", OwnerID = 1 },
new Workbook() { ID = 2, Name = "Workbook 2", OwnerID = 1 },
new Workbook() { ID = 3, Name = "Workbook 3", OwnerID = 2 },
new Workbook() { ID = 4, Name = "Workbook 4", OwnerID = 2 },
new Workbook() { ID = 5, Name = "Workbook 5", OwnerID = 3 }
);
modelBuilder.Entity<Workpage>()
.HasData(
new Workpage() { ID = 1, Name = "Workpage 1", WorkbookID = 1, Content = "Content 1" },
new Workpage() { ID = 2, Name = "Workpage 2", WorkbookID = 1, Content = "Content 2" },
new Workpage() { ID = 3, Name = "Workpage 3", WorkbookID = 1, Content = "Content 3" },
new Workpage() { ID = 4, Name = "Workpage 4", WorkbookID = 1, Content = "Content 4" },
new Workpage() { ID = 5, Name = "Workpage 5", WorkbookID = 2, Content = "Content 5" },
new Workpage() { ID = 6, Name = "Workpage 6", WorkbookID = 2, Content = "Content 6" },
new Workpage() { ID = 7, Name = "Workpage 7", WorkbookID = 3, Content = "Content 7" }
);
}
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Aya_Backend.Data
{
public class LoginDTO
{
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@ -0,0 +1,195 @@
// <auto-generated />
using Aya_Backend.Data.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Aya_Backend.Data.Migrations
{
[DbContext(typeof(AyaContext))]
[Migration("20200505164438_InitialFix")]
partial class InitialFix
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.3");
modelBuilder.Entity("Aya_Backend.Data.User", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("ID");
b.ToTable("Users");
b.HasData(
new
{
ID = 1,
Username = "User1"
},
new
{
ID = 2,
Username = "User2"
},
new
{
ID = 3,
Username = "User3"
});
});
modelBuilder.Entity("Aya_Backend.Data.Workbook", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("OwnerID")
.HasColumnType("INTEGER");
b.HasKey("ID");
b.HasIndex("OwnerID");
b.ToTable("Workbooks");
b.HasData(
new
{
ID = 1,
Name = "Workbook 1",
OwnerID = 1
},
new
{
ID = 2,
Name = "Workbook 2",
OwnerID = 1
},
new
{
ID = 3,
Name = "Workbook 3",
OwnerID = 2
},
new
{
ID = 4,
Name = "Workbook 4",
OwnerID = 2
},
new
{
ID = 5,
Name = "Workbook 5",
OwnerID = 3
});
});
modelBuilder.Entity("Aya_Backend.Data.Workpage", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("WorkbookID")
.HasColumnType("INTEGER");
b.HasKey("ID");
b.HasIndex("WorkbookID");
b.ToTable("Workpages");
b.HasData(
new
{
ID = 1,
Content = "Content 1",
Name = "Workpage 1",
WorkbookID = 1
},
new
{
ID = 2,
Content = "Content 2",
Name = "Workpage 2",
WorkbookID = 1
},
new
{
ID = 3,
Content = "Content 3",
Name = "Workpage 3",
WorkbookID = 1
},
new
{
ID = 4,
Content = "Content 4",
Name = "Workpage 4",
WorkbookID = 1
},
new
{
ID = 5,
Content = "Content 5",
Name = "Workpage 5",
WorkbookID = 2
},
new
{
ID = 6,
Content = "Content 6",
Name = "Workpage 6",
WorkbookID = 2
},
new
{
ID = 7,
Content = "Content 7",
Name = "Workpage 7",
WorkbookID = 3
});
});
modelBuilder.Entity("Aya_Backend.Data.Workbook", b =>
{
b.HasOne("Aya_Backend.Data.User", "Owner")
.WithMany()
.HasForeignKey("OwnerID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aya_Backend.Data.Workpage", b =>
{
b.HasOne("Aya_Backend.Data.Workbook", "Workbook")
.WithMany()
.HasForeignKey("WorkbookID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,161 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Aya_Backend.Data.Migrations
{
public partial class InitialFix : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
ID = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Username = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.ID);
});
migrationBuilder.CreateTable(
name: "Workbooks",
columns: table => new
{
ID = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(nullable: true),
OwnerID = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Workbooks", x => x.ID);
table.ForeignKey(
name: "FK_Workbooks_Users_OwnerID",
column: x => x.OwnerID,
principalTable: "Users",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Workpages",
columns: table => new
{
ID = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(nullable: true),
Content = table.Column<string>(nullable: true),
WorkbookID = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Workpages", x => x.ID);
table.ForeignKey(
name: "FK_Workpages_Workbooks_WorkbookID",
column: x => x.WorkbookID,
principalTable: "Workbooks",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
table: "Users",
columns: new[] { "ID", "Username" },
values: new object[] { 1, "User1" });
migrationBuilder.InsertData(
table: "Users",
columns: new[] { "ID", "Username" },
values: new object[] { 2, "User2" });
migrationBuilder.InsertData(
table: "Users",
columns: new[] { "ID", "Username" },
values: new object[] { 3, "User3" });
migrationBuilder.InsertData(
table: "Workbooks",
columns: new[] { "ID", "Name", "OwnerID" },
values: new object[] { 1, "Workbook 1", 1 });
migrationBuilder.InsertData(
table: "Workbooks",
columns: new[] { "ID", "Name", "OwnerID" },
values: new object[] { 2, "Workbook 2", 1 });
migrationBuilder.InsertData(
table: "Workbooks",
columns: new[] { "ID", "Name", "OwnerID" },
values: new object[] { 3, "Workbook 3", 2 });
migrationBuilder.InsertData(
table: "Workbooks",
columns: new[] { "ID", "Name", "OwnerID" },
values: new object[] { 4, "Workbook 4", 2 });
migrationBuilder.InsertData(
table: "Workbooks",
columns: new[] { "ID", "Name", "OwnerID" },
values: new object[] { 5, "Workbook 5", 3 });
migrationBuilder.InsertData(
table: "Workpages",
columns: new[] { "ID", "Content", "Name", "WorkbookID" },
values: new object[] { 1, "Content 1", "Workpage 1", 1 });
migrationBuilder.InsertData(
table: "Workpages",
columns: new[] { "ID", "Content", "Name", "WorkbookID" },
values: new object[] { 2, "Content 2", "Workpage 2", 1 });
migrationBuilder.InsertData(
table: "Workpages",
columns: new[] { "ID", "Content", "Name", "WorkbookID" },
values: new object[] { 3, "Content 3", "Workpage 3", 1 });
migrationBuilder.InsertData(
table: "Workpages",
columns: new[] { "ID", "Content", "Name", "WorkbookID" },
values: new object[] { 4, "Content 4", "Workpage 4", 1 });
migrationBuilder.InsertData(
table: "Workpages",
columns: new[] { "ID", "Content", "Name", "WorkbookID" },
values: new object[] { 5, "Content 5", "Workpage 5", 2 });
migrationBuilder.InsertData(
table: "Workpages",
columns: new[] { "ID", "Content", "Name", "WorkbookID" },
values: new object[] { 6, "Content 6", "Workpage 6", 2 });
migrationBuilder.InsertData(
table: "Workpages",
columns: new[] { "ID", "Content", "Name", "WorkbookID" },
values: new object[] { 7, "Content 7", "Workpage 7", 3 });
migrationBuilder.CreateIndex(
name: "IX_Workbooks_OwnerID",
table: "Workbooks",
column: "OwnerID");
migrationBuilder.CreateIndex(
name: "IX_Workpages_WorkbookID",
table: "Workpages",
column: "WorkbookID");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Workpages");
migrationBuilder.DropTable(
name: "Workbooks");
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@ -0,0 +1,204 @@
// <auto-generated />
using Aya_Backend.Data.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Aya_Backend.Data.Migrations
{
[DbContext(typeof(AyaContext))]
[Migration("20200505223849_User-Store")]
partial class UserStore
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.3");
modelBuilder.Entity("Aya_Backend.Data.User", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasColumnType("TEXT");
b.Property<string>("Token")
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("ID");
b.ToTable("Users");
b.HasData(
new
{
ID = 1,
Password = "Password1_",
Username = "User1"
},
new
{
ID = 2,
Password = "Password2_",
Username = "User2"
},
new
{
ID = 3,
Password = "Password3_",
Username = "User3"
});
});
modelBuilder.Entity("Aya_Backend.Data.Workbook", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("OwnerID")
.HasColumnType("INTEGER");
b.HasKey("ID");
b.HasIndex("OwnerID");
b.ToTable("Workbooks");
b.HasData(
new
{
ID = 1,
Name = "Workbook 1",
OwnerID = 1
},
new
{
ID = 2,
Name = "Workbook 2",
OwnerID = 1
},
new
{
ID = 3,
Name = "Workbook 3",
OwnerID = 2
},
new
{
ID = 4,
Name = "Workbook 4",
OwnerID = 2
},
new
{
ID = 5,
Name = "Workbook 5",
OwnerID = 3
});
});
modelBuilder.Entity("Aya_Backend.Data.Workpage", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("WorkbookID")
.HasColumnType("INTEGER");
b.HasKey("ID");
b.HasIndex("WorkbookID");
b.ToTable("Workpages");
b.HasData(
new
{
ID = 1,
Content = "Content 1",
Name = "Workpage 1",
WorkbookID = 1
},
new
{
ID = 2,
Content = "Content 2",
Name = "Workpage 2",
WorkbookID = 1
},
new
{
ID = 3,
Content = "Content 3",
Name = "Workpage 3",
WorkbookID = 1
},
new
{
ID = 4,
Content = "Content 4",
Name = "Workpage 4",
WorkbookID = 1
},
new
{
ID = 5,
Content = "Content 5",
Name = "Workpage 5",
WorkbookID = 2
},
new
{
ID = 6,
Content = "Content 6",
Name = "Workpage 6",
WorkbookID = 2
},
new
{
ID = 7,
Content = "Content 7",
Name = "Workpage 7",
WorkbookID = 3
});
});
modelBuilder.Entity("Aya_Backend.Data.Workbook", b =>
{
b.HasOne("Aya_Backend.Data.User", "Owner")
.WithMany()
.HasForeignKey("OwnerID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aya_Backend.Data.Workpage", b =>
{
b.HasOne("Aya_Backend.Data.Workbook", "Workbook")
.WithMany()
.HasForeignKey("WorkbookID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Aya_Backend.Data.Migrations
{
public partial class UserStore : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Password",
table: "Users",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Token",
table: "Users",
nullable: true);
migrationBuilder.UpdateData(
table: "Users",
keyColumn: "ID",
keyValue: 1,
column: "Password",
value: "Password1_");
migrationBuilder.UpdateData(
table: "Users",
keyColumn: "ID",
keyValue: 2,
column: "Password",
value: "Password2_");
migrationBuilder.UpdateData(
table: "Users",
keyColumn: "ID",
keyValue: 3,
column: "Password",
value: "Password3_");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Password",
table: "Users");
migrationBuilder.DropColumn(
name: "Token",
table: "Users");
}
}
}

View File

@ -0,0 +1,202 @@
// <auto-generated />
using Aya_Backend.Data.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Aya_Backend.Data.Migrations
{
[DbContext(typeof(AyaContext))]
partial class AyaContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.3");
modelBuilder.Entity("Aya_Backend.Data.User", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasColumnType("TEXT");
b.Property<string>("Token")
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("ID");
b.ToTable("Users");
b.HasData(
new
{
ID = 1,
Password = "Password1_",
Username = "User1"
},
new
{
ID = 2,
Password = "Password2_",
Username = "User2"
},
new
{
ID = 3,
Password = "Password3_",
Username = "User3"
});
});
modelBuilder.Entity("Aya_Backend.Data.Workbook", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("OwnerID")
.HasColumnType("INTEGER");
b.HasKey("ID");
b.HasIndex("OwnerID");
b.ToTable("Workbooks");
b.HasData(
new
{
ID = 1,
Name = "Workbook 1",
OwnerID = 1
},
new
{
ID = 2,
Name = "Workbook 2",
OwnerID = 1
},
new
{
ID = 3,
Name = "Workbook 3",
OwnerID = 2
},
new
{
ID = 4,
Name = "Workbook 4",
OwnerID = 2
},
new
{
ID = 5,
Name = "Workbook 5",
OwnerID = 3
});
});
modelBuilder.Entity("Aya_Backend.Data.Workpage", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("WorkbookID")
.HasColumnType("INTEGER");
b.HasKey("ID");
b.HasIndex("WorkbookID");
b.ToTable("Workpages");
b.HasData(
new
{
ID = 1,
Content = "Content 1",
Name = "Workpage 1",
WorkbookID = 1
},
new
{
ID = 2,
Content = "Content 2",
Name = "Workpage 2",
WorkbookID = 1
},
new
{
ID = 3,
Content = "Content 3",
Name = "Workpage 3",
WorkbookID = 1
},
new
{
ID = 4,
Content = "Content 4",
Name = "Workpage 4",
WorkbookID = 1
},
new
{
ID = 5,
Content = "Content 5",
Name = "Workpage 5",
WorkbookID = 2
},
new
{
ID = 6,
Content = "Content 6",
Name = "Workpage 6",
WorkbookID = 2
},
new
{
ID = 7,
Content = "Content 7",
Name = "Workpage 7",
WorkbookID = 3
});
});
modelBuilder.Entity("Aya_Backend.Data.Workbook", b =>
{
b.HasOne("Aya_Backend.Data.User", "Owner")
.WithMany()
.HasForeignKey("OwnerID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aya_Backend.Data.Workpage", b =>
{
b.HasOne("Aya_Backend.Data.Workbook", "Workbook")
.WithMany()
.HasForeignKey("WorkbookID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Aya_Backend.Data.Repositories.UserRepositories
{
public interface IRepository
{
//Interface for getting values from a dataset (Database/Mocked data).
//User
public User GetUser(int id);
public User GetUserAuth(string username, string password);
public User AddUser(User user);
public void UpdateUser(User user);
//Workbook
public Workbook GetWorkbook(int id);
public Workbook AddWorkbook(string name, int ownerid);
public List<Workbook> GetUserWorkbooks(int id);
//Workpage
public Workpage GetWorkpage(int id);
public void AddWorkpage(Workpage wp);
public List<Workpage> GetWorkbookWorkpages(int id);
public void UpdateWorkbookContent(int id, string content);
public int GetWorkpageOwnerID(int id);
}
}

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Aya_Backend.Data.Repositories.UserRepositories
{
//Mock repository to use for testing. Gets passed mock data in the form of lists upon construction.
public class MockRepository : IRepository
{
private List<User> MockUsers { get; }
private List<Workbook> MockWorkbooks { get; }
private List<Workpage> MockWorkpages { get; }
public MockRepository(List<User> users, List<Workbook> workbooks, List<Workpage> workpages)
{
MockUsers = users;
MockWorkbooks = workbooks;
MockWorkpages = workpages;
}
//User
public User GetUser(int id)
{
return MockUsers.FirstOrDefault(user => user.ID == id);
}
public User AddUser(User user)
{
MockUsers.Add(user);
return user;
}
//Workbook
public Workbook GetWorkbook(int id)
{
return MockWorkbooks.FirstOrDefault(wb => wb.ID == id);
}
public Workbook AddWorkbook(string name, int ownerid)
{
var workbook = new Workbook() { ID = 10, Name = name, OwnerID = ownerid };
MockWorkbooks.Add(workbook);
return workbook;
}
public List<Workbook> GetUserWorkbooks(int id)
{
return MockWorkbooks.Where(wb => wb.OwnerID == id).ToList();
}
public Workpage GetWorkpage(int id)
{
return MockWorkpages.FirstOrDefault(wp => wp.ID == id);
}
public void AddWorkpage(Workpage wp)
{
MockWorkpages.Add(wp);
}
public List<Workpage> GetWorkbookWorkpages(int id)
{
return MockWorkpages.Where(wp => wp.WorkbookID == id).ToList();
}
public void UpdateWorkbookContent(int id, string content)
{
var workpage = MockWorkpages.FirstOrDefault(wp => wp.ID == id);
workpage.Content = content;
}
public Workbook AddWorkbook(string name, string ownerid)
{
throw new NotImplementedException();
}
public User GetUserAuth(string username, string password)
{
throw new NotImplementedException();
}
public void UpdateUser(User user)
{
throw new NotImplementedException();
}
public int GetWorkpageOwnerID(int id)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,94 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Aya_Backend.Data.Repositories.UserRepositories
{
public class Repository : IRepository
{
private readonly AyaContext _context;
public Repository(AyaContext context)
{
_context = context;
}
//User
public User GetUser(int id)
{
return _context.Users.FirstOrDefault(u => u.ID == id);
}
public User GetUserAuth(string username, string password)
{
return _context.Users.FirstOrDefault(u => u.Username == username && u.Password == password);
}
public User AddUser(User user)
{
var u = _context.Add(user);
_context.SaveChanges();
return u.Entity;
}
public void UpdateUser(User user)
{
_context.Update(user);
_context.SaveChanges();
}
//Workbook
public Workbook GetWorkbook(int id)
{
return _context.Workbooks.FirstOrDefault(u => u.ID == id);
}
public Workbook AddWorkbook(string name, int ownerid)
{
var added = _context.Add(new Workbook { Name = name, OwnerID = ownerid });
_context.SaveChangesAsync();
return added.Entity;
}
public List<Workbook> GetUserWorkbooks(int id)
{
return _context.Workbooks.Where(wb => wb.OwnerID == id).ToList();
}
//Workpage
public Workpage GetWorkpage(int id)
{
return _context.Workpages.FirstOrDefault(wp => wp.ID == id);
}
public void AddWorkpage(Workpage wp)
{
_context.Add(wp);
_context.SaveChanges();
}
public List<Workpage> GetWorkbookWorkpages(int id)
{
return _context.Workpages.Where(wp => wp.WorkbookID == id).ToList();
}
public void UpdateWorkbookContent(int id, string content)
{
var original = _context.Workpages.FirstOrDefault(wp => wp.ID == id);
if (original == null)
return;
original.Content = content;
_context.Update(original);
_context.SaveChanges();
}
public int GetWorkpageOwnerID(int id)
{
var workbook = _context.Workpages.Include(wp => wp.Workbook).FirstOrDefault(wp => wp.ID == id);
return workbook.Workbook.OwnerID;
}
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Aya_Backend.Data
{
public class TokenDTO
{
public string Token { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Aya_Backend.Data
{
public class User
{
public int ID { get; set; }
public string Username { get; set; }
[JsonIgnore]
public string Password { get; set; }
public string Token { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Aya_Backend.Data
{
public class Workbook
{
public int ID { get; set; }
public string Name { get; set; }
public int OwnerID { get; set; }
public User Owner { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Aya_Backend.Data
{
public class Workpage
{
public int ID { get; set; }
public string Name { get; set; }
public string Content { get; set; }
public int WorkbookID { get; set; }
public Workbook Workbook { get; set; }
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Aya_Backend.Data.Repositories;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment;
namespace Aya_Backend
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var env = services.GetRequiredService<IHostingEnvironment>();
if (env.IsDevelopment())
{
var context = services.GetRequiredService<AyaContext>();
context.Database.EnsureDeleted();
context.Database.Migrate();
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:63581",
"sslPort": 44394
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Aya_Backend": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Text.Json.Serialization;
using Aya_Backend.Data;
using Aya_Backend.Data.Repositories;
using Aya_Backend.Data.Repositories.UserRepositories;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace Aya_Backend.Services
{
public interface IUserService
{
User Authenticate(string username, string password);
}
public class UserService : IUserService
{
private readonly IRepository _context;
public UserService(IRepository repository)
{
_context = repository;
}
public User Authenticate(string username, string password)
{
var user = _context.GetUserAuth(username, password);
// return null if user not found
if (user == null)
return null;
// authentication successful so generate jwt token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("VmdjieSvw6co7huHZtyn");
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.ID.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
return user;
}
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Aya_Backend.Data.Repositories;
using Aya_Backend.Data.Repositories.UserRepositories;
using Aya_Backend.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
namespace Aya_Backend
{
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.AddDbContext<AyaContext>();
var key = Encoding.ASCII.GetBytes("VmdjieSvw6co7huHZtyn");
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddScoped<IRepository, Repository>();
services.AddScoped<IUserService, UserService>();
}
// 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.UseCors(builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

Binary file not shown.

View File

@ -0,0 +1,23 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Aya-Backend")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("Aya-Backend")]
[assembly: System.Reflection.AssemblyTitleAttribute("Aya-Backend")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.

File diff suppressed because it is too large Load Diff

2
Aya-Backend/README.md Normal file
View File

@ -0,0 +1,2 @@
# Aya-Backend
The backend/API for Aya, written in C# ASP.NET.

41
Aya-Frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.swp
pids
logs
results
tmp
# Build
public/css/main.css
# Coverage reports
coverage
# API keys and secrets
.env
# Dependency directory
node_modules
bower_components
# Editors
.idea
*.iml
# OS metadata
.DS_Store
Thumbs.db
# Ignore built ts files
dist/**/*
# ignore yarn.lock
yarn.lock

2
Aya-Frontend/README.md Normal file
View File

@ -0,0 +1,2 @@
# Aya-Frontend
The main frontend to the Aya language learning webapp, using the VueJS framework.

View File

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

15245
Aya-Frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

88
Aya-Frontend/package.json Normal file
View File

@ -0,0 +1,88 @@
{
"name": "aya-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"@types/showdown": "^1.9.3",
"@types/simplemde": "^1.11.7",
"axios": "^0.19.2",
"bootstrap": "^4.4.1",
"bootstrap-vue": "^2.12.0",
"core-js": "^3.6.4",
"easymde": "^2.10.1",
"showdown": "^1.9.1",
"simplemde": "^1.11.2",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-js-modal": "^2.0.0-rc.3",
"vue-property-decorator": "^8.4.1",
"vue-router": "^3.1.6",
"vue-simplemde": "^1.0.4",
"vuex": "^3.3.0"
},
"devDependencies": {
"@types/jest": "^24.0.19",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-eslint": "~4.3.0",
"@vue/cli-plugin-router": "~4.3.0",
"@vue/cli-plugin-typescript": "~4.3.0",
"@vue/cli-plugin-unit-jest": "~4.3.0",
"@vue/cli-service": "~4.3.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "1.0.0-beta.31",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^1.19.1",
"typescript": "~3.8.3",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint"
],
"parserOptions": {
"ecmaVersion": 2020
},
"rules": {},
"overrides": [
{
"files": [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)"
],
"env": {
"jest": true
}
}
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"jest": {
"preset": "@vue/cli-plugin-unit-jest/presets/typescript-and-babel"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@ -0,0 +1,17 @@
<!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>

34
Aya-Frontend/src/App.vue Normal file
View File

@ -0,0 +1,34 @@
<template>
<div id="app">
<div id="nav">
<HeaderPanel />
</div>
<router-view />
</div>
</template>
<script>
// @ is an alias to /src
import HeaderPanel from "@/components/HeaderPanel.vue";
export default {
name: "Home",
components: {
HeaderPanel
}
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: left;
color: #2c3e50;
}
html, body
{
height: 100%; overflow: hidden;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book-open" class="svg-inline--fa fa-book-open fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M542.22 32.05c-54.8 3.11-163.72 14.43-230.96 55.59-4.64 2.84-7.27 7.89-7.27 13.17v363.87c0 11.55 12.63 18.85 23.28 13.49 69.18-34.82 169.23-44.32 218.7-46.92 16.89-.89 30.02-14.43 30.02-30.66V62.75c.01-17.71-15.35-31.74-33.77-30.7zM264.73 87.64C197.5 46.48 88.58 35.17 33.78 32.05 15.36 31.01 0 45.04 0 62.75V400.6c0 16.24 13.13 29.78 30.02 30.66 49.49 2.6 149.59 12.11 218.77 46.95 10.62 5.35 23.21-1.94 23.21-13.46V100.63c0-5.29-2.62-10.14-7.27-12.99z"></path></svg>

After

Width:  |  Height:  |  Size: 691 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z"></path></svg>

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="plus-square" class="svg-inline--fa fa-plus-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-32 252c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92H92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"></path></svg>

After

Width:  |  Height:  |  Size: 551 B

View File

@ -0,0 +1,97 @@
<template>
<div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import Workpage from "../types/Workpage";
import API from '../data/API';
import UserAPI from "../data/UserAPI"
import User from '../types/User';
@Component
export default class WorkpageExplorer extends Vue {
@Prop() api!: API;
//Watch for a login request, if a request is made (value is set to true), begin login procedure.
@Watch('$store.state.loginRequested')
onLoginRequest(newVal: any, oldVal: any)
{
if (newVal == true)
this.loginDialog()
}
//Check whether a token exists in localstorage, if it does then login with that token.
created()
{
document.title = "Aya";
if (localStorage.token == null || localStorage.token == "")
this.loginDialog();
else
this.loginWithToken();
}
//Show login dialog.
loginDialog()
{
this.$store.state.loginRequested = false;
this.$modal.show("dialog", {
title: "Please Login",
text: "Username: <input type=\"text\" id=\"username\"><br>" +
"Password: <input type=\"password\" id=\"password\">",
buttons: [
{
title: "Create",
default: false,
handler: () => { this.createAccount((document.getElementById("username") as HTMLInputElement).value, (document.getElementById("password") as HTMLInputElement).value); this.$modal.hide("dialog"); }
},
{
title: "Login",
default: true,
handler: () => { this.login((document.getElementById("username") as HTMLInputElement).value, (document.getElementById("password") as HTMLInputElement).value); this.$modal.hide("dialog"); }
}
]
},{ clickToClose: false });
}
//Send request to API to login (returning user object with token).
async login(username: string, password: string)
{
if (this.api == undefined)
return;
const uAPI = new UserAPI(this.api.axios, this.api.rootURL);
const response = await uAPI.login(username, password);
this.$store.state.currentUser = new User(response.data.id, response.data.username, response.data.token);
localStorage.token = response.data.token;
this.$store.state.authed = true;
}
//Send request to API to create a new account (returns the new user with the token).
async createAccount(username: string, password: string)
{
if (this.api == undefined)
return;
const uAPI = new UserAPI(this.api.axios, this.api.rootURL);
const response = await uAPI.create(username, password);
console.log(response);
this.$store.state.currentUser = new User(response.data.id, response.data.username, localStorage.token);
this.$store.state.authed = true;
}
//Send request to API to login with a given token.
async loginWithToken()
{
if (this.api == undefined)
return;
const uAPI = new UserAPI(this.api.axios, this.api.rootURL);
const response = await uAPI.getUser(localStorage.token);
this.$store.state.currentUser = new User(response.data.id, response.data.username, localStorage.token);
this.$store.state.authed = true;
}
}
</script>

View File

@ -0,0 +1,92 @@
<template>
<div class="topnav">
<img class="logo" src="../assets/Aya_JP_With_Line.png" width="84" height="49">
<a class="username" v-if="logString == 'Logout'" href="#">Logged in as: {{ $store.state.currentUser.Username }}</a>
<a class="Log-Link" href="#" @click="handleButton()"> {{ logString }} </a>
</div>
</template>
<script lang="ts">
//Formatting in this script is odd for some reason, blame VSCode.
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import Workpage from "../types/Workpage";
import API from '../data/API';
import UserAPI from "../data/UserAPI"
import User from '../types/User';
@Component
export default class WorkpageExplorer extends Vue
{
@Prop() private logString = "Login";
//Check if authed has changed, and change the text based on whether authed or not.
@Watch('$store.state.authed')
onAuthChange(newVal: any, oldVal: any)
{
if (newVal == true)
this.logString = "Logout";
else
this.logString = "Login"
}
//Decides whether to request a login or logout, depending on the current state.
handleButton()
{
if (this.logString == "Login")
this.requestLogin();
else
{
this.logout();
}
}
//Sets the global loginRequested state to be true, saying that the login prompt should open.
requestLogin()
{
this.$store.state.loginRequested = true;
}
//Removes the token from the local storage and sets global authed to false. (Simple logout technique).
logout()
{
this.$store.state.authed = false;
localStorage.token = null;
}
}
</script>
<style scoped>
.topnav {
background-color: #333;
overflow: hidden;
}
.topnav a {
float: left;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 17px;
}
.topnav a:hover {
background-color: #ddd;
color: black;
}
.logo {
float: left;
color: #f2f2f2;
text-align: center;
text-decoration: none;
font-size: 17px;
}
.Log-Link
{
position: absolute;
right: 0px;
}
.username
{
position: absolute;
right: 100px;
}
</style>

View File

@ -0,0 +1,138 @@
<template>
<div>
<ul class="workbookList">
<li v-for="workbook in workbookArray" :key="workbook.id">
<img @click="changeActiveWorkbook(workbook.id)" :src="getBookImage(workbook.id)" width="50" height="50">
</li>
</ul>
<div class="addWorkbook">
<img @click="addWorkbookDialog()" src="../assets/plus-square-solid.svg" width="20" height="20"> Add Workbook
</div>
<v-dialog />
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import Workbook from "../types/Workbook";
import API from '../data/API';
import WorkbookAPI from "../data/WorkbookAPI";
import { findByKey } from "../utils/collections";
@Component
export default class WorkbookSidebar extends Vue {
//Local workbook variable, these are from the API.
@Prop() private workbookArray!: Workbook[];
@Prop() private api!: API;
//When dynamic images are done in this way, they must be required by the compiler.
@Prop() private readonly closedBookSolidImage = require('../assets/book-solid.svg');
@Prop() private readonly openBookSolidImage = require('../assets/book-open-solid.svg');
//When the user is authed, get all workbooks from that user from the api and assign them to the local variable.
//When the user is not authed, empty the array.
@Watch('$store.state.authed')
onAuthed(newVal: any, oldVal: any)
{
if (newVal == true)
this.getAllUserWorkbooksFromAPI();
else
this.emptyWorkbookArray();
}
//Assign the response from the api to the local workbook variable.
private async getAllUserWorkbooksFromAPI()
{
if (this.api == undefined)
return;
const wbAPI = new WorkbookAPI(this.api.axios, this.api.rootURL);
const response = await wbAPI.getAllUserWorkbooks(localStorage.token);
this.workbookArray = response.data;
}
//Get the book image (open/closed) for the specified ID.
//(If the current workbook is the workbook ID passed, the open book image will be used, otherwise the closed image will be used).
private getBookImage(id: string): any
{
if (this.$store.state.workbookActive == id)
return this.openBookSolidImage;
return this.closedBookSolidImage;
};
//Change the store's active workbook to the ID passed.
private changeActiveWorkbook(id: string)
{
this.$store.commit("changeActiveWorkbook", id);
}
//Show the dialog for adding a new workbook.
private addWorkbookDialog()
{
if (this.$store.state.authed == false)
return;
this.$modal.show("dialog", {
title: "Add Workbook",
text: "Please input the name of the new Workbook: <input id=\"new-workbook-name\" type=\"text\" style=\"width: 370px\">",
buttons: [
{
title: "Cancel",
hander: () => { this.$modal.hide("dialog") }
},
{
title: "Create",
default: true,
handler: () => { this.addWorkbook((document.getElementById("new-workbook-name") as HTMLInputElement).value); this.$modal.hide("dialog"); }
}
]
});
}
//Adds a workbook by posting to the API. Then, retrieves all of the workbooks again to update the local version.
private async addWorkbook(name: string)
{
if (this.api == undefined)
return;
const wbAPI = new WorkbookAPI(this.api.axios, this.api.rootURL);
const response = await wbAPI.addWorkbook(name, localStorage.token);
await this.getAllUserWorkbooksFromAPI();
}
//Empties the local workbook array.
private emptyWorkbookArray()
{
this.workbookArray = [];
}
}
</script>
<style scoped>
body
{
background-color: darkslategray;
}
.addWorkbook
{
position : fixed;
bottom : 1vh;
cursor: pointer;
}
.workbookList
{
position : fixed;
left: 0px;
overflow : auto;
height : 90vh;
list-style-type : none;
cursor: pointer;
}
.workbookList ul, .workbookList li
{
margin: 0; padding: 0;
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div class="editor">
<h1>{{ workpageTitle }}</h1>
<vue-simplemde ref="markdownEditor" />
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch, Ref } from "vue-property-decorator";
import VueSimplemde from 'vue-simplemde';
import API from "../data/API";
import WorkpageAPI from "../data/WorkpageAPI";
import AutoSave from "../utils/autosave";
@Component
export default class WorkpageEditor extends Vue {
//Local variables for editor.
@Prop() private workpageTitle!: string;
@Prop() private workpageContent!: string;
@Prop() private api!: API;
@Prop() private autoSave!: any;
//Reference to the markdown editor.
@Ref("markdownEditor") readonly mde!: any;
//Watch for authed changing, if the user gets unauthed, then reset/empty all values.
@Watch('$store.state.authed')
onAuthChange(newVal: any, oldVal: any)
{
if (newVal == false)
{
this.workpageContent = "";
this.updateEditorText();
this.workpageTitle = "";
}
}
//Watch for the active workpage changing, as this is when the text changes need to be applied.
@Watch('$store.state.workpageActive')
onWorkPageChange(newVal: any, oldVal: any)
{
this.getWorkpage(newVal);
}
//Create a loop for the autosave function. Creates the object on created (5 seconds per post).
created()
{
this.autoSave = new AutoSave(() => this.updateWorkpage(), 5000);
}
//Creates a hook for when a change is made (for example, text changes), create the timer. If another change is made within the 5 seconds, restart the timer.
//This means the save only starts after 5 seconds of inactivity after a change.
mounted()
{
const self = this.startAutosaveTimer;
this.mde.simplemde.codemirror.on("change", function(){
self();
});
}
//Gets a workpage from the api with the specified ID.
private async getWorkpage(id: string)
{
if (this.api == undefined)
return;
const wpAPI = new WorkpageAPI(this.api.axios, this.api.rootURL);
const response = await wpAPI.getWorkpage(id, localStorage.token);
this.workpageTitle = response.data.name;
this.workpageContent = response.data.content;
this.updateEditorText();
}
//Updates the Editor's text with the workpageContent variable.
private updateEditorText()
{
this.mde.simplemde.value(this.workpageContent);
}
//Updates a workpage to the API based on the content in the editor and the active workpage.
private async updateWorkpage()
{
if (this.api == undefined)
return;
const wpAPI = new WorkpageAPI(this.api.axios, this.api.rootURL);
const response = await wpAPI.updateWorkpage(this.$store.state.workpageActive, this.mde.simplemde.value(), localStorage.token);
}
//Starts the autosave timer.
private startAutosaveTimer()
{
clearInterval(this.autoSave);
this.autoSave = setInterval(this.updateWorkpage, 5000);
}
}
</script>
<style>
@import '~simplemde/dist/simplemde.min.css';
.editor
{
height: 90vh;
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<div class="workpageList">
<ul style="list-style-type : none;">
<li v-for="workpage in workpages" :key="workpage.id">
<a href="#" @click="changeSelectedWorkpage(workpage.id)">{{ workpage.name }}</a>
</li>
</ul>
<div class="addWorkpage">
<img @click="addWorkpageDialog()" src="../assets/plus-square-solid.svg" width="20" height="20"> Add Workpage
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import Workpage from "../types/Workpage";
import API from '../data/API';
import WorkpageAPI from "../data/WorkpageAPI"
@Component
export default class WorkpageExplorer extends Vue {
//The local variable for the workpages displayed.
@Prop() private workpages!: Workpage[];
@Prop() private api!: API;
//Checks of the workbook changes. When the workbook changes, it gets all pages from that workbook.
@Watch('$store.state.workbookActive')
onWorkbookChange(newVal: any, oldVal: any)
{
this.getAllWorkpagesFromWorkbook(newVal);
}
//Checks if the auth gets changed. If the user gets unauthed, all workpages should be removed from the view.
@Watch('$store.state.authed')
onAuthChange(newVal: any, oldVal: any)
{
if (newVal == false)
{
this.workpages = [];
}
}
//Gets all of the workpages from the workbook id passed.
private async getAllWorkpagesFromWorkbook(id: string)
{
if (this.api == undefined)
return;
const wpAPI = new WorkpageAPI(this.api.axios, this.api.rootURL);
const response = await wpAPI.getAllWorkpagesFromWorkbook(id, localStorage.token);
this.workpages = response.data;
}
//Commits the new selected workpage to the store.
private changeSelectedWorkpage(id: string)
{
this.$store.commit("changeActiveWorkpage", id);
}
//Shows the dialog to add a workpage.
private addWorkpageDialog()
{
this.$modal.show("dialog", {
title: "Add Workpage",
text: "Please input the name of the new Workpage: <input id=\"new-workpage-name\" type=\"text\" style=\"width: 370px\">",
buttons: [
{
title: "Cancel",
hander: () => { this.$modal.hide("dialog") }
},
{
title: "Create",
default: true,
handler: () => { this.addWorkpage((document.getElementById("new-workpage-name") as HTMLInputElement).value); this.$modal.hide("dialog"); }
}
]
});
}
//Adds a workpage to the API with the given name.
private async addWorkpage(name: string)
{
if (this.api == undefined)
return;
const activeWB = this.$store.state.workbookActive;
const wpAPI = new WorkpageAPI(this.api.axios, this.api.rootURL);
const response = await wpAPI.addWorkpage(name, activeWB, localStorage.token);
await this.getAllWorkpagesFromWorkbook(activeWB);
};
}
</script>
<style scoped>
.workpageList
{
text-align: left;
white-space : nowrap;
overflow: auto;
list-style-type : none;
}
.addWorkpage
{
position : fixed;
bottom : 1vh;
cursor: pointer;
}
.workpageList ul, .workpageList li
{
margin: 0; padding: 0;
}
</style>

View File

@ -0,0 +1,47 @@
export default class API
{
readonly rootURL: string;
readonly axios: any;
constructor(ax: any, root: string)
{
this.axios = ax;
this.rootURL = root;
}
async post(path: string, bodyContent: string)
{
const res = await this.axios.post(this.rootURL + path, bodyContent, { headers: { "Content-Type": "application/json" }});
return res;
}
async postAuthed(path: string, token: string)
{
const res = await this.axios.post(this.rootURL + path, "", { headers: { "Authorization": "Bearer " + token }});
return res;
}
async postAuthedWithBody(path: string, content: string, token: string)
{
const res = await this.axios.post(this.rootURL + path, content, { headers: { "Authorization": "Bearer " + token, "Content-Type": "application/json" }, });
return res;
}
async postWithHeaders(path: string, bodyContent: string, options: any)
{
const res = await this.axios.post(this.rootURL + path, bodyContent, options);
return res;
}
async get(path: string)
{
const res = await this.axios.get(this.rootURL + path);
return res;
}
async getAuthed(path: string, token: string)
{
const res = await this.axios.get(this.rootURL + path, { headers: { "Authorization": "Bearer " + token }});
return res;
}
}

View File

@ -0,0 +1,26 @@
import API from "./API";
export default class UserAPI extends API
{
private readonly apiPath = "api/user/";
constructor(ax: any, root: string)
{
super(ax, root);
}
async login(username: string, password: string)
{
return await super.post(this.apiPath + "login", JSON.stringify({ Username: username, Password: password }));
}
async getUser(token: string)
{
return await super.getAuthed(this.apiPath + "get", token);
}
async create(username: string, password: string)
{
return await super.post(this.apiPath + "create", JSON.stringify({ Username: username, Password: password }));
}
}

View File

@ -0,0 +1,22 @@
import API from './API';
export default class WorkbookAPI extends API
{
private readonly apiPath = "api/workbook/";
constructor(ax: any, root: string)
{
super(ax, root);
}
async getAllUserWorkbooks(token: string)
{
return await super.getAuthed(this.apiPath + "userworkbooks", token);
}
async addWorkbook(name: string, token: string)
{
console.log(name + " " + token);
return await super.postAuthed(this.apiPath + "add?name=" + name, token);
}
}

View File

@ -0,0 +1,31 @@
import API from './API';
export default class WorkpageAPI extends API
{
private readonly apiPath = "api/workpage/";
constructor(ax: any, root: string)
{
super(ax, root);
}
async getAllWorkpagesFromWorkbook(id: string, token: string)
{
return await super.getAuthed(this.apiPath + "fromworkbook?id=" + id, token);
}
async getWorkpage(id: string, token: string)
{
return await super.getAuthed(this.apiPath + "get?id=" + id, token)
}
async addWorkpage(name: string, workbookid: number, token)
{
return await super.postAuthed(this.apiPath + "add?name=" + name + "&workbookid=" + workbookid, token);
}
async updateWorkpage(id: string, content: string, token: string)
{
return await super.postAuthedWithBody(this.apiPath + "updateworkpagecontent?id=" + id, '"' + content + '"', token);
}
}

30
Aya-Frontend/src/main.ts Normal file
View File

@ -0,0 +1,30 @@
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store/store";
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import { library } from '@fortawesome/fontawesome-svg-core';
import { faUserSecret } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import VueSimplemde from 'vue-simplemde';
import 'simplemde/dist/simplemde.min.css';
import VModal from 'vue-js-modal';
library.add(faUserSecret);
Vue.use(BootstrapVue);
Vue.use(IconsPlugin);
Vue.use(VModal, { dialog: true });
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.component('vue-simplemde', VueSimplemde)
Vue.config.productionTip = false;
new Vue({
store,
router,
render: h => h(App)
}).$mount("#app");

View File

@ -0,0 +1,20 @@
import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import Home from "../views/Home.vue";
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
{
path: "/",
name: "Home",
component: Home
}];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
export default router;

13
Aya-Frontend/src/shims-tsx.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import Vue, { VNode } from "vue";
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

4
Aya-Frontend/src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}

View File

@ -0,0 +1,27 @@
import Vue from 'vue'
import Vuex from 'vuex'
import User from '@/types/User';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
workbookActive: 0,
workpageActive: 0,
authed: false,
loginRequested: false,
currentUser: User
},
getters: {},
mutations: {
changeActiveWorkbook(state, payload)
{
state.workbookActive = payload;
},
changeActiveWorkpage(state, payload)
{
state.workpageActive = payload;
}
},
actions: {}
});

View File

@ -0,0 +1,13 @@
export default class User
{
ID!: string;
Username!: string;
Token!: string;
constructor(id: string, username: string, token: string)
{
this.ID = id;
this.Username = username;
this.Token = token;
}
}

View File

@ -0,0 +1,9 @@
import User from "../types/User";
import Workpage from "../types/Workpage";
export default class Workbook
{
id!: string;
name!: string;
ownerID!: string;
}

View File

@ -0,0 +1,14 @@
import showdown from "showdown";
export default class Workpage
{
WorkpageID!: string;
WorkpageName!: string;
WorkpageContent!: string;
GetWorkpageContentMarkdown(): string
{
const converter = new showdown.Converter;
return converter.makeHtml(this.WorkpageContent);
}
}

View File

@ -0,0 +1,22 @@
export default class AutoSave
{
private currentTimeout!: any;
private delayTime!: number;
private func!: Function;
constructor(f: Function, time: number)
{
this.func = f;
this.delayTime = time;
}
public start(): void
{
this.currentTimeout = setTimeout(this.func, this.delayTime);
}
public stop(): void
{
clearTimeout(this.currentTimeout);
}
}

View File

@ -0,0 +1,8 @@
//Searches through the array and finds the value with the key, else returns null.
export function findByKey(arr: Array<any>, value: any, key = "id"): any
{
for (const val of arr)
if (val[key] == value)
return val;
return null;
}

View File

@ -0,0 +1,70 @@
<template>
<div class="container">
<div class="row">
<div class="col-sm WorkbookSidebar">
<WorkbookSidebar :api="api" />
</div>
<div class="col-sm WorkpageExplorer">
<WorkpageExplorer :api="api" />
</div>
<div class="col-sm WorkpageEditor">
<WorkpageEditor :api="api" />
</div>
</div>
<AccountDialog :api="api" />
</div>
</template>
<script>
// @ is an alias to /src
import WorkbookSidebar from "@/components/WorkbookSidebar.vue";
import WorkpageExplorer from "@/components/WorkpageExplorer.vue";
import WorkpageEditor from "@/components/WorkpageEditor.vue";
import AccountDialog from "@/components/AccountDialog.vue";
import API from "@/data/API";
export default {
name: "Home",
components: {
WorkbookSidebar,
WorkpageExplorer,
WorkpageEditor,
AccountDialog
},
data: function() {
return {
api: new API(require("axios"), "https://localhost:5001/")
};
}
};
</script>
<style scoped>
.WorkbookSidebar
{
position: absolute;
left: 0px;
width: 150px;
height: 90vh;
resize : horizontal;
background-color: lightslategray;
}
.WorkpageExplorer
{
text-align: left;
position: absolute;
left: 125px;
width: 300px;
height: 90vh;
resize : horizontal;
background-color: rgb(183, 188, 200);
}
.WorkpageEditor
{
text-align: left;
position: absolute;
height: 90vh;
left: 425px;
background-color: rgb(231, 231, 231);
}
</style>

View File

@ -0,0 +1,12 @@
import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
describe("HelloWorld.vue", () => {
it("renders props.msg when passed", () => {
const msg = "new message";
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
});
expect(wrapper.text()).toMatch(msg);
});
});

View File

@ -0,0 +1,40 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": false,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}