mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 19:22:45 +00:00
Compare commits
9 Commits
v1.0-beta.
...
v1.0-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
014c4e3b32 | ||
|
|
bf70a32b95 | ||
|
|
d047bd052e | ||
|
|
03f230dbca | ||
|
|
0a8c707387 | ||
|
|
8908b8c6ed | ||
|
|
da8df8973c | ||
|
|
7e50311c92 | ||
|
|
02ccd6cd06 |
@@ -27,8 +27,6 @@ FROM build AS publish
|
||||
RUN dotnet publish -c Release -o /app/publish \
|
||||
#--runtime alpine-x64 \
|
||||
--self-contained true \
|
||||
/p:TrimMode=partial \
|
||||
/p:PublishTrimmed=true \
|
||||
/p:PublishSingleFile=true \
|
||||
/p:JsonSerializerIsReflectionEnabledByDefault=true \
|
||||
-a $TARGETARCH
|
||||
|
||||
@@ -247,7 +247,7 @@ namespace Notesnook.API.Hubs
|
||||
var chunks = PrepareChunks(
|
||||
userId,
|
||||
ids,
|
||||
size: 1000,
|
||||
size: 100,
|
||||
resetSync: device.IsSyncReset,
|
||||
maxBytes: 7 * 1024 * 1024
|
||||
);
|
||||
|
||||
@@ -91,11 +91,7 @@ namespace Notesnook.API.Repositories
|
||||
public void DeleteByUserId(string userId)
|
||||
{
|
||||
var filter = Builders<SyncItem>.Filter.Eq("UserId", userId);
|
||||
var writes = new List<WriteModel<SyncItem>>
|
||||
{
|
||||
new DeleteManyModel<SyncItem>(filter)
|
||||
};
|
||||
dbContext.AddCommand((handle, ct) => Collection.BulkWriteAsync(handle, writes, options: null, ct));
|
||||
dbContext.AddCommand((handle, ct) => Collection.DeleteManyAsync(handle, filter, null, ct));
|
||||
}
|
||||
|
||||
public void Upsert(SyncItem item, string userId, long dateSynced)
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Notesnook.API.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
const int MaxIdsPerChunk = 400_000;
|
||||
const int MaxIdsPerChunk = 25_000;
|
||||
public async Task AppendIdsAsync(string userId, string deviceId, string key, IEnumerable<ItemKey> ids)
|
||||
{
|
||||
var filter = DeviceIdsChunkFilter(userId, deviceId, key) & Builders<DeviceIdsChunk>.Filter.Where(x => x.Ids.Length < MaxIdsPerChunk);
|
||||
@@ -81,7 +81,7 @@ namespace Notesnook.API.Services
|
||||
|
||||
if (chunk != null)
|
||||
{
|
||||
var update = Builders<DeviceIdsChunk>.Update.PushEach(x => x.Ids, ids.Select(i => i.ToString()));
|
||||
var update = Builders<DeviceIdsChunk>.Update.AddToSetEach(x => x.Ids, ids.Select(i => i.ToString()));
|
||||
await repositories.DeviceIdsChunks.Collection.UpdateOneAsync(
|
||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.Id, chunk.Id),
|
||||
update
|
||||
|
||||
@@ -182,6 +182,7 @@ namespace Notesnook.API.Services
|
||||
|
||||
public async Task DeleteUserAsync(string userId)
|
||||
{
|
||||
logger.LogInformation("Deleting user {UserId}", userId);
|
||||
var cc = new CancellationTokenSource();
|
||||
|
||||
Repositories.Notes.DeleteByUserId(userId);
|
||||
|
||||
@@ -33,9 +33,6 @@ namespace Streetwriters.Data.DbContexts
|
||||
public static IMongoClient CreateMongoDbClient(IDbSettings dbSettings)
|
||||
{
|
||||
var settings = MongoClientSettings.FromConnectionString(dbSettings.ConnectionString);
|
||||
settings.MaxConnectionPoolSize = 500;
|
||||
settings.MinConnectionPoolSize = 0;
|
||||
settings.HeartbeatInterval = TimeSpan.FromSeconds(60);
|
||||
return new MongoClient(settings);
|
||||
}
|
||||
|
||||
|
||||
@@ -97,12 +97,12 @@ namespace Streetwriters.Identity.Controllers
|
||||
}
|
||||
case TokenType.RESET_PASSWORD:
|
||||
{
|
||||
if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", code))
|
||||
return BadRequest("Invalid token.");
|
||||
// if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", code))
|
||||
return BadRequest("Password reset is temporarily disabled due to some issues. It should be back soon. We apologize for the inconvenience.");
|
||||
|
||||
var authorizationCode = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode");
|
||||
var redirectUrl = $"{client.AccountRecoveryRedirectURL}?userId={userId}&code={authorizationCode}";
|
||||
return RedirectPermanent(redirectUrl);
|
||||
// var authorizationCode = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode");
|
||||
// var redirectUrl = $"{client.AccountRecoveryRedirectURL}?userId={userId}&code={authorizationCode}";
|
||||
// return RedirectPermanent(redirectUrl);
|
||||
}
|
||||
default:
|
||||
return BadRequest("Invalid type.");
|
||||
@@ -149,21 +149,22 @@ namespace Streetwriters.Identity.Controllers
|
||||
[EnableRateLimiting("strict")]
|
||||
public async Task<IActionResult> ResetUserPassword([FromForm] ResetPasswordForm form)
|
||||
{
|
||||
var client = Clients.FindClientById(form.ClientId);
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
return BadRequest(new { error = "Password reset is temporarily disabled due to some issues. It should be back soon. We apologize for the inconvenience." });
|
||||
// var client = Clients.FindClientById(form.ClientId);
|
||||
// if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.FindByEmailAsync(form.Email) ?? throw new Exception("User not found.");
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok();
|
||||
// var user = await UserManager.FindByEmailAsync(form.Email) ?? throw new Exception("User not found.");
|
||||
// if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok();
|
||||
|
||||
var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
|
||||
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD);
|
||||
#if (DEBUG || STAGING)
|
||||
return Ok(callbackUrl);
|
||||
#else
|
||||
logger.LogInformation("Password reset email sent to: {Email}, callback URL: {CallbackUrl}", user.Email, callbackUrl);
|
||||
await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client);
|
||||
return Ok();
|
||||
#endif
|
||||
// var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
|
||||
// var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD);
|
||||
// #if (DEBUG || STAGING)
|
||||
// return Ok(callbackUrl);
|
||||
// #else
|
||||
// logger.LogInformation("Password reset email sent to: {Email}, callback URL: {CallbackUrl}", user.Email, callbackUrl);
|
||||
// await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client);
|
||||
// return Ok();
|
||||
// #endif
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
@@ -250,31 +251,33 @@ namespace Streetwriters.Identity.Controllers
|
||||
}
|
||||
case "change_password":
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(form.OldPassword);
|
||||
ArgumentNullException.ThrowIfNull(form.NewPassword);
|
||||
var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Password changed.");
|
||||
return Ok();
|
||||
}
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
return BadRequest(new { error = "Password change is temporarily disabled due to some issues. It should be back soon. We apologize for the inconvenience." });
|
||||
// ArgumentNullException.ThrowIfNull(form.OldPassword);
|
||||
// ArgumentNullException.ThrowIfNull(form.NewPassword);
|
||||
// var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword);
|
||||
// if (result.Succeeded)
|
||||
// {
|
||||
// await SendLogoutMessageAsync(user.Id.ToString(), "Password changed.");
|
||||
// return Ok();
|
||||
// }
|
||||
// return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
case "reset_password":
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(form.NewPassword);
|
||||
var result = await UserManager.RemovePasswordAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await MFAService.ResetMFAAsync(user);
|
||||
result = await UserManager.AddPasswordAsync(user, form.NewPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Password reset.");
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
return BadRequest(new { error = "Password reset is temporarily disabled due to some issues. It should be back soon. We apologize for the inconvenience." });
|
||||
// ArgumentNullException.ThrowIfNull(form.NewPassword);
|
||||
// var result = await UserManager.RemovePasswordAsync(user);
|
||||
// if (result.Succeeded)
|
||||
// {
|
||||
// await MFAService.ResetMFAAsync(user);
|
||||
// result = await UserManager.AddPasswordAsync(user, form.NewPassword);
|
||||
// if (result.Succeeded)
|
||||
// {
|
||||
// await SendLogoutMessageAsync(user.Id.ToString(), "Password reset.");
|
||||
// return Ok();
|
||||
// }
|
||||
// }
|
||||
// return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
case "change_marketing_consent":
|
||||
{
|
||||
|
||||
@@ -27,8 +27,6 @@ FROM build AS publish
|
||||
RUN dotnet publish -c Release -o /app/publish \
|
||||
#--runtime alpine-x64 \
|
||||
--self-contained true \
|
||||
/p:TrimMode=partial \
|
||||
/p:PublishTrimmed=true \
|
||||
/p:PublishSingleFile=true \
|
||||
/p:JsonSerializerIsReflectionEnabledByDefault=true \
|
||||
-a $TARGETARCH
|
||||
|
||||
@@ -53,6 +53,7 @@ using Streetwriters.Identity.Interfaces;
|
||||
using Streetwriters.Identity.Jobs;
|
||||
using Streetwriters.Identity.Services;
|
||||
using Streetwriters.Identity.Validation;
|
||||
using IdentityServer4.MongoDB.Configuration;
|
||||
|
||||
namespace Streetwriters.Identity
|
||||
{
|
||||
@@ -107,11 +108,6 @@ namespace Streetwriters.Identity
|
||||
options.UsersCollection = "users";
|
||||
// options.MigrationCollection = "migration";
|
||||
options.ConnectionString = connectionString;
|
||||
options.ClusterConfigurator = builder =>
|
||||
{
|
||||
builder.ConfigureConnectionPool((c) => c.With(maxConnections: 500, minConnections: 0));
|
||||
builder.ConfigureServer(s => s.With(heartbeatInterval: TimeSpan.FromSeconds(60)));
|
||||
};
|
||||
}).AddDefaultTokenProviders();
|
||||
|
||||
services.AddIdentityServer(
|
||||
@@ -137,6 +133,11 @@ namespace Streetwriters.Identity
|
||||
.AddKeyManagement()
|
||||
.AddFileSystemPersistence(Path.Combine(WebHostEnvironment.ContentRootPath, @"keystore"));
|
||||
|
||||
services.Configure<MongoDBConfiguration>(options =>
|
||||
{
|
||||
options.ConnectionString = connectionString;
|
||||
});
|
||||
|
||||
services.Configure<DataProtectionTokenProviderOptions>(options =>
|
||||
{
|
||||
options.TokenLifespan = TimeSpan.FromHours(2);
|
||||
|
||||
@@ -27,8 +27,6 @@ FROM build AS publish
|
||||
RUN dotnet publish -c Release -o /app/publish \
|
||||
#--runtime alpine-x64 \
|
||||
--self-contained true \
|
||||
/p:TrimMode=partial \
|
||||
/p:PublishTrimmed=true \
|
||||
/p:PublishSingleFile=true \
|
||||
/p:JsonSerializerIsReflectionEnabledByDefault=true \
|
||||
-a $TARGETARCH
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
const PORT = Bun.env.PORT || 3000;
|
||||
const HOST = Bun.env.HOST || "localhost";
|
||||
const ALLOWED_ORIGINS = Bun.env.ALLOWED_ORIGINS?.split(",") || ["*"];
|
||||
const MAX_REDIRECTS = 5;
|
||||
|
||||
@@ -114,6 +115,7 @@ async function proxyRequest(
|
||||
// Main server
|
||||
const server = Bun.serve({
|
||||
port: PORT,
|
||||
hostname: HOST,
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
|
||||
@@ -208,7 +210,21 @@ const server = Bun.serve({
|
||||
});
|
||||
}
|
||||
|
||||
// Proxy the request
|
||||
// Check if it's a YouTube URL and redirect instead of proxying
|
||||
if (isYouTubeEmbed(targetUrl)) {
|
||||
// YouTube URL detected, redirect to youtube-nocookie.com
|
||||
logRequest(req.method, targetUrl, 200);
|
||||
return new Response(serveYouTubeEmbed(targetUrl), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Content-Security-Policy": "frame-ancestors *",
|
||||
"X-Frame-Options": "ALLOWALL",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Proxy the request for non-YouTube URLs
|
||||
const response = await proxyRequest(targetUrl);
|
||||
logRequest(req.method, targetUrl, response.status);
|
||||
return response;
|
||||
@@ -222,6 +238,85 @@ const server = Bun.serve({
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`🚀 CORS Proxy Server running on http://localhost:${server.port}`);
|
||||
console.log(`📋 Health check: http://localhost:${server.port}/health`);
|
||||
console.log(
|
||||
`🚀 CORS Proxy Server running on http://${server.hostname}:${server.port}`
|
||||
);
|
||||
console.log(`📋 Health check: http://${server.hostname}:${server.port}/health`);
|
||||
console.log(`🌍 Environment: ${Bun.env.NODE_ENV || "development"}`);
|
||||
|
||||
/**
|
||||
* This is required to bypass YouTube's Referrer Policy restrictions when
|
||||
* embedding videos on the mobile app. It basically "proxies" the Referrer and
|
||||
* allows any YouTube video to be embedded anywhere without restrictions.
|
||||
*/
|
||||
function serveYouTubeEmbed(url: string) {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="referrer" content="strict-origin-when-cross-origin">
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<title>YouTube Video Embed</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing:border-box
|
||||
}
|
||||
|
||||
body, html {
|
||||
overflow: hidden;
|
||||
background:#000
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="${transformYouTubeUrl(
|
||||
url
|
||||
)}" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture;web-share" allowfullscreen referrerpolicy="strict-origin-when-cross-origin" title="Video player"></iframe>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// Check if URL is a YouTube embed (including youtube-nocookie.com)
|
||||
function isYouTubeEmbed(urlString: string) {
|
||||
const url = new URL(urlString);
|
||||
return (
|
||||
(url.hostname === "www.youtube.com" ||
|
||||
url.hostname === "youtube.com" ||
|
||||
url.hostname === "m.youtube.com" ||
|
||||
url.hostname === "www.youtube-nocookie.com" ||
|
||||
url.hostname === "youtube-nocookie.com") &&
|
||||
url.pathname.startsWith("/embed/")
|
||||
);
|
||||
}
|
||||
|
||||
// Transform YouTube URLs to use youtube-nocookie.com for enhanced privacy
|
||||
function transformYouTubeUrl(urlString: string): string {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
|
||||
// Check if it's a YouTube domain
|
||||
if (
|
||||
url.hostname === "www.youtube.com" ||
|
||||
url.hostname === "youtube.com" ||
|
||||
url.hostname === "m.youtube.com"
|
||||
) {
|
||||
// Replace with youtube-nocookie.com
|
||||
url.hostname = "www.youtube-nocookie.com";
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
return urlString;
|
||||
} catch {
|
||||
return urlString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"types": [],
|
||||
"types": [
|
||||
"bun-types"
|
||||
],
|
||||
// For nodejs:
|
||||
// "lib": ["esnext"],
|
||||
// "types": ["node"],
|
||||
|
||||
Reference in New Issue
Block a user