This commit is contained in:
2018-06-28 23:08:13 +00:00
commit 877637f1e5
69 changed files with 13521 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using GZTW.Pecklist.Models;
using GZTW.Pecklist.Util;
using System.Linq;
using System;
//required to inject configuration in constructor
using Microsoft.Extensions.Configuration;
namespace GZTW.Pecklist.Controllers
{
//Authentication controller
public class AuthController : Controller
{
private readonly PecklistContext _context;
private readonly IConfiguration _configuration;
public AuthController(PecklistContext context, IConfiguration configuration)//these two are injected, see startup.cs
{
_context = context;
//_configuration = configuration;
//guard clause
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
//AUTHENTICATE CREDS
//RETURN JWT
[HttpPost("/authenticate")]
public JsonResult PostCreds(string login, string password)
{
int nFailedAuthDelay = 10000;
if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password))
{
//Make a failed pw wait
System.Threading.Thread.Sleep(nFailedAuthDelay);
return Json(new { msg = "authentication failed", error = 1 });
}
var user = _context.User.SingleOrDefault(m => m.Login == login);
if (user == null)
{
//Make a failed pw wait
System.Threading.Thread.Sleep(nFailedAuthDelay);
return Json(new { msg = "authentication failed", error = 1 });
}
//TODO: do a test login from postman, login as john, then copy pw into db
//string pwnewJohn=Hasher.hash(user.Salt,"XXX");
//then login as Joyce and copy pw into db
// string pwnewJoyce=Hasher.hash(user.Salt,"XXX");
string hashed = Hasher.hash(user.Salt, password);
if (hashed == user.Password)
{
//get teh secret from appsettings.json
var secret = _configuration.GetSection("JWT").GetValue<string>("secret");
byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(secret);
var iat = new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds();
//NOTE: pecklist is appropriate for a really long expiry
var exp = new DateTimeOffset(DateTime.Now.AddDays(365)).ToUnixTimeSeconds();
//Generate a download token and store it with the user account and return it for the client
Guid g = Guid.NewGuid();
string dlkey = Convert.ToBase64String(g.ToByteArray());
dlkey = dlkey.Replace("=", "");
dlkey = dlkey.Replace("+", "");
user.DlKey = dlkey;
user.DlKeyExp = exp;
_context.User.Update(user);
_context.SaveChanges();
var payload = new Dictionary<string, object>()
{
{ "iat", iat.ToString() },
{ "exp", exp.ToString() },
{ "iss", "GZTW_Pecklist" },
{ "id", user.Id.ToString() }
};
//NOTE: probably don't need Jose.JWT as am using Microsoft jwt stuff to validate routes so it should also be able to
//issue tokens as well, but it looked cmplex and this works so unless need to remove in future keeping it.
string token = Jose.JWT.Encode(payload, secretKey, Jose.JwsAlgorithm.HS256);
//string jsonDecoded = Jose.JWT.Decode(token, secretKey);
return Json(new
{
ok = 1,
issued = iat,
expires = exp,
token = token,
dlkey = dlkey,
name = user.Name,
id = user.Id
});
}
else
{
//Make a failed pw wait
System.Threading.Thread.Sleep(nFailedAuthDelay);
return Json(new { msg = "authentication failed", error = 1 });
}
}
//------------------------------------------------------
}//eoc
}//eons

View File

@@ -0,0 +1,456 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using GZTW.Pecklist.Models;
using GZTW.Pecklist.Util;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
namespace GZTW.Pecklist.Controllers
{
[Produces("application/json")]//forces all responses to be json
[Route("api/sync")]
[Authorize]
public class SyncController : Controller
{
private readonly PecklistContext _ct;
private readonly ILogger _logger;
public SyncController(PecklistContext context, ILogger<SyncController> logger)
{
_ct = context;
//guard clause
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
// POST: api/sync
[HttpPost]
public ActionResult PostList([FromBody] JArray jclient)
{
//TODO: Might need a lock flag in db here just in case of duelling updates
//Get list from server
ListData l = _ct.ListData.First(c=>c.Id == 1);
//client has no data, send them the latest data
if (jclient == null)
{
return Ok(l.TheList);
}
JArray jserver = JArray.Parse(l.TheList);
JArray jnew = doSync(jclient, jserver);
l.TheList = jnew.ToString();
_ct.ListData.Update(l);
_ct.SaveChanges();
return Ok(l.TheList);
}
////////////////////////////////////////////////////////////////////////
// SYNCHRONIZE
////////////////////////////////////////////////////////////////////////
/*
SYNC STRATEGY
=-=-=-=-=-=-=
NEW ITEMS:
client record is iterated looking for id that starts with the string "NEW_". If any are found the master rev is incremented and new items
are added with a proper ID.
DELETED ITEMS:
Server record is itereated looking for deleted items (items with priority -1), if any are
found master rev is incremented (unless it was already) then they are removed from the master list.
UPDATE
Compare client record to master record, compare list name for a change, then find all items that have the same ID but different user editable values,
if any found update master rev if not done already, make master changed items match client changed items, update master.
RETURN:
Finally, if master list has changed, it is saved to db. Master list is then returned returned to client.
*/
/////////////////////////////////////////////
// Synchronize the two lists, return the new
//
private JArray doSync(JArray jclient, JArray jserver)
{
_logger.LogInformation(LoggingEvents.SyncList, "Synchronize list");
//Iterate the client's lists looking for changes
foreach (JObject l in jclient)
{
//track if master v is incremented (non zero)
long newListVersion = 0;
var listId = (string)l["id"];
//Is this an entirely new list?
if (listId.StartsWith("NEW_"))
{
//yes this is a new list, add it and go to the next
addNewList(l, jserver);
continue;
}
//bugbug: this will bomb if client syncs with a list that was deleted (which should be entirely ignored)
ListHeaderData oldListHeader = getListHeaderData(listId, jserver);
if (oldListHeader.v == -999)
{
//list isn't at the server, was deleted so go to next list and ignore it
continue;
}
//Has this list been deleted?
long newListV = (long)l["v"];
if (l["deleted"] != null && newListV == oldListHeader.v)
{
removeList(listId, jserver);
continue;
}
//Has the name changed?
string newListName = (string)l["name"];
long newListNameV = (long)l["name_v"];
//if names differ but name_v are the same then the client changed the name
if (newListName != oldListHeader.name && oldListHeader.name_v == newListNameV)
{
if (0 == newListVersion)
newListVersion = incrementListVersion(listId, jserver);
renameList(listId, newListName, jserver);
}
//Existing list, lets see if there are any changes required
//iterate this client list's items
foreach (JObject citem in l["items"])
{
string itemId = (string)citem["id"];
int itemPriority = (int)citem["priority"];
string itemText = (string)citem["text"];
bool itemCompleted = (bool)citem["completed"];
//New item and not deleted?
if (itemId.StartsWith("NEW_") && itemPriority != -1)
{
if (0 == newListVersion)
newListVersion = incrementListVersion(listId, jserver);
addNewListItem(listId, citem, jserver);
continue;
}
//check if deleted
if (itemPriority == -1)
{
if (0 == newListVersion)
newListVersion = incrementListVersion(listId, jserver);
removeListItem(listId, itemId, jserver);
continue;
}
//check if changed or removed (sitem=null)
JObject sitem = getListItem(listId, itemId, jserver);
if (sitem != null && ((string)sitem["text"] != itemText || (int)sitem["priority"] != itemPriority || (bool)sitem["completed"] != itemCompleted))
{
//replace server item with client item
if (0 == newListVersion)
newListVersion = incrementListVersion(listId, jserver);
//make a replacement item
JObject ritem = new JObject();
ritem["id"] = itemId;
ritem["completed"] = itemCompleted;
ritem["text"] = itemText;
ritem["priority"] = itemPriority;
ritem["v"] = newListVersion;
//replace it
replaceListItem(listId, itemId, ritem, jserver);
continue;
}
}
}
//completed, return the synced list
return jserver;
}
////////////////////////////////////////////////////////////////////////
// SYNC UTILITIES
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////
// Increment a server list's version number
//
private long incrementListVersion(string listId, JArray jserver)
{
foreach (JObject slist in jserver)
{
if (((string)slist["id"]) == listId)
{
long listversion = (long)slist["v"];
listversion++;
slist["v"] = listversion;
return listversion;
}
}
throw new System.ArgumentNullException("listId[" + listId + "]", "SyncController::incrementListVersion -> List id not found in server docs lists");
}
////////////////////////////////////////////
// Get a list item from the server
//
private JObject getListItem(string listId, string itemId, JArray jserver)
{
foreach (JObject slist in jserver)
{
if (((string)slist["id"]) == listId)
{
foreach (JObject sitem in slist["items"])
{
if ((string)sitem["id"] == itemId)
{
return sitem;
}
}
}
}
return null;//not exceptional, client has item that other user deleted
//throw new System.ArgumentException("SyncController::getListItem -> List item[" + listId + "][" + itemId + "] not found in servers lists");
}
////////////////////////////////////////////
// Get a list "header" from the server
//
private ListHeaderData getListHeaderData(string listId, JArray jserver)
{
foreach (JObject slist in jserver)
{
if (((string)slist["id"]) == listId)
{
ListHeaderData lnd = new ListHeaderData();
lnd.name = (string)slist["name"];
lnd.name_v = (long)slist["name_v"];
lnd.v = (long)slist["v"];
return lnd;
}
}
//Doesn't exist, probably deleted previously and attempting to sync a deleted list
return new ListHeaderData() { v = -999 };
}
private class ListHeaderData
{
public long name_v { get; set; }
public string name { get; set; }
public long v { get; set; }
}
////////////////////////////////////////////
//Rename a list
//
private void renameList(string listId, string newName, JArray jserver)
{
_logger.LogInformation(LoggingEvents.RenameList, $"Rename list id {listId} to {newName}");
foreach (JObject slist in jserver)
{
if (((string)slist["id"]) == listId)
{
slist["name"] = newName;
slist["name_v"] = slist["v"];
}
}
}
////////////////////////////////////////////
// Clean up and ID a client list item
//
private JObject createServerReadyListItemFromClientListItem(JObject citem)
{
JObject sitem = new JObject();
sitem["id"] = Util.ShortId.Generate();
sitem["completed"] = (bool)citem["completed"];
sitem["text"] = (string)citem["text"];
sitem["v"] = 1;
sitem["priority"] = (int)citem["priority"];
return sitem;
}
////////////////////////////////////////////
//Add a new list item from the client
//
private void addNewListItem(string listId, JObject citem, JArray jserver)
{
_logger.LogInformation(LoggingEvents.InsertItem, "addNewListItem");
//NOTE: new list item should have starting version number equal to
//server list's master v value
JObject sitem = createServerReadyListItemFromClientListItem(citem);
foreach (JObject slist in jserver)
{
if (((string)slist["id"]) == listId)
{
((JArray)slist["items"]).Add(sitem);
}
}
}
////////////////////////////////////////////
// Replace a list item from the client
//
private void replaceListItem(string listId, string itemId, JObject citem, JArray jserver)
{
removeListItem(listId, itemId, jserver);
addNewListItem(listId, citem, jserver);
}
////////////////////////////////////////////
//Remove list item
//
private void removeListItem(string listId, string listItemId, JArray jserver)
{
_logger.LogInformation(LoggingEvents.DeleteItem, $"Remove list item - list id {listId} listItemId {listItemId}");
foreach (JObject slist in jserver)
{
if (((string)slist["id"]) == listId)
{
int nRemoveAt = -1;
JArray sitems = (JArray)slist["items"];
for (int i = 0; i < sitems.Count; i++)
{
if ((string)sitems[i]["id"] == listItemId)
{
nRemoveAt = i;
break;
}
}
if (nRemoveAt == -1)
{
_logger.LogWarning(LoggingEvents.DeleteItem, $"Remove list item NOT FOUND - list id {listId} listItemId {listItemId}");
}else{
sitems.RemoveAt(nRemoveAt);
}
return;
}
}
}
///////////////////////////////////////////
//Add a new list from the client
//
private void addNewList(JObject clist, JArray jserver)
{
_logger.LogInformation(LoggingEvents.AddList, "Add new list");
//NOTE: New lists start at version 1 for everything
JObject slist = new JObject();
//set list headers
slist["id"] = Util.ShortId.Generate();
slist["name"] = clist["name"];
slist["name_v"] = 1;
slist["v"] = 1;
//copy over the list data
JArray sitems = new JArray();
foreach (JObject citem in clist["items"])
{
sitems.Add(createServerReadyListItemFromClientListItem(citem));
}
slist["items"] = sitems;
jserver.Add(slist);
}
////////////////////////////////////////////
// Remove list
//
private void removeList(string listId, JArray jserver)
{
_logger.LogInformation(LoggingEvents.DeleteList, $"Remove list {listId}");
int nRemoveAt = -1;
for (int i = 0; i < jserver.Count; i++)
{
if ((string)jserver[i]["id"] == listId)
{
nRemoveAt = i;
break;
}
}
jserver.RemoveAt(nRemoveAt);
return;
}
//-----------------
#region LoggingEvents
public class LoggingEvents
{
public const int SyncList = 1000;
public const int RenameList = 1001;
public const int GetItem = 1002;
public const int InsertItem = 1003;
public const int UpdateItem = 1004;
public const int DeleteItem = 1005;
public const int AddList = 1006;
public const int DeleteList = 1007;
public const int GetItemNotFound = 4000;
public const int UpdateItemNotFound = 4001;
}
#endregion
//-------------
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace GZTW.Pecklist.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
[Authorize]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody]string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}