feat: get, add, & delete user's inbox api tokens (#50)

* feat: get, add, & delete user's inbox api tokens

* inbox: generate inbox api key on the server

* inbox: use nanoid to generate api key && set created date on server

* inbox: set api key in constructor && increase default expiry date to 1 year
This commit is contained in:
01zulfi
2025-09-16 08:40:52 +05:00
committed by GitHub
parent 4a0aee1c44
commit 9b774d640c
9 changed files with 207 additions and 5 deletions

View File

@@ -43,6 +43,7 @@ namespace Notesnook.API.Accessors
public SyncItemsRepository Tags { get; }
public Repository<UserSettings> UsersSettings { get; }
public Repository<Monograph> Monographs { get; }
public Repository<InboxApiKey> InboxApiKey { get; }
public SyncItemsRepositoryAccessor(IDbContext dbContext,
@@ -71,10 +72,12 @@ namespace Notesnook.API.Accessors
[FromKeyedServices(Collections.TagsKey)]
IMongoCollection<SyncItem> tags,
Repository<UserSettings> usersSettings, Repository<Monograph> monographs)
Repository<UserSettings> usersSettings, Repository<Monograph> monographs,
Repository<InboxApiKey> inboxApiKey)
{
UsersSettings = usersSettings;
Monographs = monographs;
InboxApiKey = inboxApiKey;
Notebooks = new SyncItemsRepository(dbContext, notebooks);
Notes = new SyncItemsRepository(dbContext, notes);
Contents = new SyncItemsRepository(dbContext, content);

View File

@@ -14,5 +14,6 @@ namespace Notesnook.API
public const string TagsKey = "tags";
public const string ColorsKey = "colors";
public const string VaultsKey = "vaults";
public const string InboxApiKeysKey = "inbox_api_keys";
}
}

View File

@@ -0,0 +1,119 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Notesnook.API.Models;
using Streetwriters.Common;
using Streetwriters.Data.Repositories;
namespace Notesnook.API.Controllers
{
[ApiController]
[Authorize]
[Route("inbox")]
public class InboxController : ControllerBase
{
private readonly Repository<InboxApiKey> InboxApiKey;
public InboxController(Repository<InboxApiKey> inboxApiKeysRepository)
{
InboxApiKey = inboxApiKeysRepository;
}
[HttpGet("api-keys")]
public async Task<IActionResult> GetApiKeysAsync()
{
var userId = User.FindFirstValue("sub");
try
{
var apiKeys = await InboxApiKey.FindAsync(t => t.UserId == userId);
return Ok(apiKeys);
}
catch (Exception ex)
{
await Slogger<InboxController>.Error(nameof(GetApiKeysAsync), "Couldn't get inbox api keys.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpPost("api-keys")]
public async Task<IActionResult> CreateApiKeyAsync([FromBody] InboxApiKey request)
{
var userId = User.FindFirstValue("sub");
try
{
if (string.IsNullOrWhiteSpace(request.Name))
{
return BadRequest(new { error = "Api key name is required." });
}
if (request.ExpiryDate <= -1)
{
return BadRequest(new { error = "Valid expiry date is required." });
}
var count = await InboxApiKey.CountAsync(t => t.UserId == userId);
if (count >= 10)
{
return BadRequest(new { error = "Maximum of 10 inbox api keys allowed." });
}
var inboxApiKey = new InboxApiKey
{
UserId = userId,
Name = request.Name,
DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
ExpiryDate = request.ExpiryDate,
LastUsedAt = 0
};
await InboxApiKey.InsertAsync(inboxApiKey);
return Ok(inboxApiKey);
}
catch (Exception ex)
{
await Slogger<InboxController>.Error(nameof(CreateApiKeyAsync), "Couldn't create inbox api key.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpDelete("api-keys/{apiKey}")]
public async Task<IActionResult> DeleteApiKeyAsync(string apiKey)
{
var userId = User.FindFirstValue("sub");
try
{
if (string.IsNullOrWhiteSpace(apiKey))
{
return BadRequest(new { error = "Api key is required." });
}
await InboxApiKey.DeleteAsync(t => t.UserId == userId && t.Key == apiKey);
return Ok(new { message = "Api key deleted successfully." });
}
catch (Exception ex)
{
await Slogger<InboxController>.Error(nameof(DeleteApiKeyAsync), "Couldn't delete inbox api key.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
}
}

View File

@@ -40,5 +40,6 @@ namespace Notesnook.API.Interfaces
SyncItemsRepository Tags { get; }
Repository<UserSettings> UsersSettings { get; }
Repository<Monograph> Monographs { get; }
Repository<InboxApiKey> InboxApiKey { get; }
}
}

View File

@@ -0,0 +1,60 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using NanoidDotNet;
namespace Notesnook.API.Models
{
public class InboxApiKey
{
public InboxApiKey()
{
var random = Nanoid.Generate(size: 64);
Key = "nn__" + random;
}
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
[MessagePack.IgnoreMember]
public string Id { get; set; }
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("key")]
public string Key { get; set; }
[JsonPropertyName("dateCreated")]
public long DateCreated { get; set; }
[JsonPropertyName("expiryDate")]
public long ExpiryDate { get; set; }
[JsonPropertyName("lastUsedAt")]
public long LastUsedAt { get; set; }
}
}

View File

@@ -16,6 +16,7 @@
<PackageReference Include="AWSSDK.S3" Version="3.7.310.8" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.2.0" />
<PackageReference Include="Nanoid" Version="3.1.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-alpha.2" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageReference Include="Quartz" Version="3.5.0" />

View File

@@ -18,9 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -154,10 +152,20 @@ namespace Notesnook.API.Services
if (keys.InboxKeys.Public == null || keys.InboxKeys.Private == null)
{
userSettings.InboxKeys = null;
await Repositories.InboxApiKey.DeleteManyAsync(t => t.UserId == userId);
}
else
{
userSettings.InboxKeys = keys.InboxKeys;
var defaultInboxKey = new InboxApiKey
{
UserId = userId,
Name = "Default",
DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds(),
LastUsedAt = 0
};
await Repositories.InboxApiKey.InsertAsync(defaultInboxKey);
}
}
@@ -184,6 +192,7 @@ namespace Notesnook.API.Services
Repositories.Vaults.DeleteByUserId(userId);
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
var result = await unit.Commit();
await Slogger<UserService>.Info(nameof(DeleteUserAsync), "User data deleted", userId, result.ToString());
@@ -243,6 +252,7 @@ namespace Notesnook.API.Services
Repositories.Tags.DeleteByUserId(userId);
Repositories.Vaults.DeleteByUserId(userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
if (!await unit.Commit()) return false;
var userSettings = await Repositories.UsersSettings.FindOneAsync((s) => s.UserId == userId);

View File

@@ -169,7 +169,8 @@ namespace Notesnook.API
services.AddRepository<UserSettings>("user_settings", "notesnook")
.AddRepository<Monograph>("monographs", "notesnook")
.AddRepository<Announcement>("announcements", "notesnook");
.AddRepository<Announcement>("announcements", "notesnook")
.AddRepository<InboxApiKey>(Collections.InboxApiKeysKey, "notesnook");
services.AddMongoCollection(Collections.SettingsKey)
.AddMongoCollection(Collections.AttachmentsKey)
@@ -182,7 +183,8 @@ namespace Notesnook.API
.AddMongoCollection(Collections.ShortcutsKey)
.AddMongoCollection(Collections.TagsKey)
.AddMongoCollection(Collections.ColorsKey)
.AddMongoCollection(Collections.VaultsKey);
.AddMongoCollection(Collections.VaultsKey)
.AddMongoCollection(Collections.InboxApiKeysKey);
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
services.AddScoped<IUserService, UserService>();

View File

@@ -84,6 +84,11 @@ namespace Streetwriters.Data.Repositories
return all.ToList();
}
public virtual async Task<long> CountAsync(Expression<Func<TEntity, bool>> filterExpression)
{
return await Collection.CountDocumentsAsync(filterExpression);
}
public virtual void Update(string id, TEntity obj)
{
dbContext.AddCommand((handle, ct) => Collection.ReplaceOneAsync(handle, Builders<TEntity>.Filter.Eq("_id", ObjectId.Parse(id)), obj, cancellationToken: ct));