This commit is contained in:
115
Controllers/AuthController.cs
Normal file
115
Controllers/AuthController.cs
Normal 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
|
||||
456
Controllers/SyncController.cs
Normal file
456
Controllers/SyncController.cs
Normal 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
|
||||
|
||||
//-------------
|
||||
}
|
||||
}
|
||||
46
Controllers/ValuesController.cs
Normal file
46
Controllers/ValuesController.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user