Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/org/vss/KVStore.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,11 +4,11 @@ public interface KVStore {

String GLOBAL_VERSION_KEY = "vss_global_version";

GetObjectResponse get(GetObjectRequest request);
GetObjectResponse get(String userToken, GetObjectRequest request);

PutObjectResponse put(PutObjectRequest request);
PutObjectResponse put(String userToken, PutObjectRequest request);

DeleteObjectResponse delete(DeleteObjectRequest request);
DeleteObjectResponse delete(String userToken, DeleteObjectRequest request);

ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request);
}
5 changes: 4 additions & 1 deletion app/src/main/java/org/vss/api/AbstractVssApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,16 +7,19 @@
import org.vss.ErrorCode;
import org.vss.ErrorResponse;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.exception.AuthException;
import org.vss.exception.ConflictException;
import org.vss.exception.NoSuchKeyException;

public abstract class AbstractVssApi {
final KVStore kvStore;
final Authorizer authorizer;

@Inject
public AbstractVssApi(KVStore kvStore) {
public AbstractVssApi(KVStore kvStore, Authorizer authorizer) {
this.kvStore = kvStore;
this.authorizer = authorizer;
}

Response toResponse(GeneratedMessageV3 protoResponse) {
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/DeleteObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,27 +4,32 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.DeleteObjectRequest;
import org.vss.DeleteObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.DELETE_OBJECT)
@Slf4j
public class DeleteObjectApi extends AbstractVssApi {
@Inject
public DeleteObjectApi(KVStore kvstore) {
super(kvstore);
public DeleteObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
DeleteObjectResponse response = kvStore.delete(request);
DeleteObjectResponse response = kvStore.delete(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in DeleteObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/GetObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.GetObjectRequest;
import org.vss.GetObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.GET_OBJECT)
@Slf4j
public class GetObjectApi extends AbstractVssApi {

@Inject
public GetObjectApi(KVStore kvstore) {
super(kvstore);
public GetObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
GetObjectRequest request = GetObjectRequest.parseFrom(payload);
GetObjectResponse response = kvStore.get(request);
GetObjectResponse response = kvStore.get(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in GetObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/ListKeyVersionsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.ListKeyVersionsRequest;
import org.vss.ListKeyVersionsResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.LIST_KEY_VERSIONS)
@Slf4j
public class ListKeyVersionsApi extends AbstractVssApi {

@Inject
public ListKeyVersionsApi(KVStore kvStore) {
super(kvStore);
public ListKeyVersionsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload);
ListKeyVersionsResponse response = kvStore.listKeyVersions(request);
ListKeyVersionsResponse response = kvStore.listKeyVersions(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in ListKeyVersionsApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/PutObjectsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.PutObjectRequest;
import org.vss.PutObjectResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.PUT_OBJECTS)
@Slf4j
public class PutObjectsApi extends AbstractVssApi {

@Inject
public PutObjectsApi(KVStore kvStore) {
super(kvStore);
public PutObjectsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload);
PutObjectResponse response = kvStore.put(putObjectRequest);
PutObjectResponse response = kvStore.put(authResponse.getUserToken(), putObjectRequest);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in PutObjectsApi: ", e);
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/vss/auth/AuthResponse.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
package org.vss.auth;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
private String userToken;
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/vss/auth/Authorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// Interface for authorizer that is run before every request.
public interface Authorizer {
AuthResponse verify(HttpHeaders headers) throws AuthException;
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/vss/auth/NoopAuthorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// A no-operation authorizer, that lets any user-request go through.
public class NoopAuthorizer implements Authorizer {
private static String UNAUTHENTICATED_USER = "unauth-user";

@Override
public AuthResponse verify(HttpHeaders headers) throws AuthException {
return new AuthResponse(UNAUTHENTICATED_USER);
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/vss/guice/BaseModule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,10 @@
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.tools.StringUtils;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.auth.NoopAuthorizer;
import org.vss.impl.postgres.PostgresBackendImpl;

public class BaseModule extends AbstractModule {
Expand All@@ -20,6 +23,9 @@ public class BaseModule extends AbstractModule {
protected void configure() {
// Provide PostgresBackend as default implementation for KVStore.
bind(KVStore.class).to(PostgresBackendImpl.class).in(Singleton.class);

// Default to Noop Authorizer.
bind(Authorizer.class).to(NoopAuthorizer.class).in(Singleton.class);
}

@Provides
Expand Down
52 changes: 29 additions & 23 deletions app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,11 +48,12 @@ public PostgresBackendImpl(DSLContext context) {
}

@Override
public GetObjectResponse get(GetObjectRequest request) {
public GetObjectResponse get(String userToken, GetObjectRequest request) {

VssDbRecord vssDbRecord = context.selectFrom(VSS_DB)
.where(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey())))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey()))))
.fetchOne();

final KeyValue keyValue;
Expand All@@ -77,18 +78,18 @@ public GetObjectResponse get(GetObjectRequest request) {
}

@Override
public PutObjectResponse put(PutObjectRequest request) {
public PutObjectResponse put(String userToken, PutObjectRequest request) {

String storeId = request.getStoreId();

List<VssDbRecord> vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

List<VssDbRecord> vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

if (request.hasGlobalVersion()) {
VssDbRecord globalVersionRecord = buildVssRecord(storeId,
VssDbRecord globalVersionRecord = buildVssRecord(userToken, storeId,
KeyValue.newBuilder()
.setKey(GLOBAL_VERSION_KEY)
.setVersion(request.getGlobalVersion())
Expand DownExpand Up@@ -130,15 +131,17 @@ private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private static DeleteConditionStep<VssDbRecord> buildNonConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey())));
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))));
}

private static DeleteConditionStep<VssDbRecord> buildConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
Expand All@@ -153,9 +156,9 @@ private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onConflict(VSS_DB.STORE_ID, VSS_DB.KEY)
.onConflict(VSS_DB.USER_TOKEN, VSS_DB.STORE_ID, VSS_DB.KEY)
.doUpdate()
.set(VSS_DB.VALUE, vssRecord.getValue())
.set(VSS_DB.VERSION, 1L)
Expand All@@ -165,7 +168,7 @@ private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord v
private Insert<VssDbRecord> buildConditionalInsertRecordQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onDuplicateKeyIgnore();
}
Expand All@@ -175,14 +178,16 @@ private Update<VssDbRecord> buildConditionalUpdateRecordQuery(DSLContext dsl, Vs
.set(Map.of(VSS_DB.VALUE, vssRecord.getValue(),
VSS_DB.VERSION, vssRecord.getVersion() + 1,
VSS_DB.LAST_UPDATED_AT, vssRecord.getLastUpdatedAt()))
.where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
private VssDbRecord buildVssRecord(String userToken, String storeId, KeyValue kv) {
OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS);
return new VssDbRecord()
.setUserToken(userToken)
.setStoreId(storeId)
.setKey(kv.getKey())
.setValue(kv.getValue().toByteArray())
Expand All@@ -192,9 +197,9 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
}

@Override
public DeleteObjectResponse delete(DeleteObjectRequest request) {
public DeleteObjectResponse delete(String userToken, DeleteObjectRequest request) {
String storeId = request.getStoreId();
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
VssDbRecord vssDbRecord = buildVssRecord(userToken, storeId, request.getKeyValue());

context.transaction((ctx) -> {
DSLContext dsl = ctx.dsl();
Expand All@@ -206,7 +211,7 @@ public DeleteObjectResponse delete(DeleteObjectRequest request) {
}

@Override
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
public ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request) {
String storeId = request.getStoreId();
String keyPrefix = request.getKeyPrefix();
String pageToken = request.getPageToken();
Expand All@@ -221,12 +226,13 @@ public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
.setStoreId(storeId)
.setKey(GLOBAL_VERSION_KEY)
.build();
globalVersion = get(getGlobalVersionRequest).getValue().getVersion();
globalVersion = get(userToken, getGlobalVersionRequest).getValue().getVersion();
}

List<VssDbRecord> vssDbRecords = context.select(VSS_DB.KEY, VSS_DB.VERSION).from(VSS_DB)
.where(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix)))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix))))
.orderBy(VSS_DB.KEY)
.seek(pageToken)
.limit(Math.min(pageSize, LIST_KEY_VERSIONS_MAX_PAGE_SIZE))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
CREATE TABLE vss_db (
user_token character varying(120) NOT NULL CHECK (user_token <> ''),
store_id character varying(120) NOT NULL CHECK (store_id <> ''),
key character varying(600) NOT NULL,
value bytea NULL,
version bigint NOT NULL,
created_at TIMESTAMP WITH TIME ZONE,
last_updated_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (store_id, key)
PRIMARY KEY (user_token, store_id, key)
);
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/org/vss/KVStore.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,11 +4,11 @@ public interface KVStore {

String GLOBAL_VERSION_KEY = "vss_global_version";

GetObjectResponse get(GetObjectRequest request);
GetObjectResponse get(String userToken, GetObjectRequest request);

PutObjectResponse put(PutObjectRequest request);
PutObjectResponse put(String userToken, PutObjectRequest request);

DeleteObjectResponse delete(DeleteObjectRequest request);
DeleteObjectResponse delete(String userToken, DeleteObjectRequest request);

ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request);
}
5 changes: 4 additions & 1 deletion app/src/main/java/org/vss/api/AbstractVssApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,16 +7,19 @@
import org.vss.ErrorCode;
import org.vss.ErrorResponse;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.exception.AuthException;
import org.vss.exception.ConflictException;
import org.vss.exception.NoSuchKeyException;

public abstract class AbstractVssApi {
final KVStore kvStore;
final Authorizer authorizer;

@Inject
public AbstractVssApi(KVStore kvStore) {
public AbstractVssApi(KVStore kvStore, Authorizer authorizer) {
this.kvStore = kvStore;
this.authorizer = authorizer;
}

Response toResponse(GeneratedMessageV3 protoResponse) {
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/DeleteObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,27 +4,32 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.DeleteObjectRequest;
import org.vss.DeleteObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.DELETE_OBJECT)
@Slf4j
public class DeleteObjectApi extends AbstractVssApi {
@Inject
public DeleteObjectApi(KVStore kvstore) {
super(kvstore);
public DeleteObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
DeleteObjectResponse response = kvStore.delete(request);
DeleteObjectResponse response = kvStore.delete(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in DeleteObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/GetObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.GetObjectRequest;
import org.vss.GetObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.GET_OBJECT)
@Slf4j
public class GetObjectApi extends AbstractVssApi {

@Inject
public GetObjectApi(KVStore kvstore) {
super(kvstore);
public GetObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
GetObjectRequest request = GetObjectRequest.parseFrom(payload);
GetObjectResponse response = kvStore.get(request);
GetObjectResponse response = kvStore.get(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in GetObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/ListKeyVersionsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.ListKeyVersionsRequest;
import org.vss.ListKeyVersionsResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.LIST_KEY_VERSIONS)
@Slf4j
public class ListKeyVersionsApi extends AbstractVssApi {

@Inject
public ListKeyVersionsApi(KVStore kvStore) {
super(kvStore);
public ListKeyVersionsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload);
ListKeyVersionsResponse response = kvStore.listKeyVersions(request);
ListKeyVersionsResponse response = kvStore.listKeyVersions(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in ListKeyVersionsApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/PutObjectsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.PutObjectRequest;
import org.vss.PutObjectResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.PUT_OBJECTS)
@Slf4j
public class PutObjectsApi extends AbstractVssApi {

@Inject
public PutObjectsApi(KVStore kvStore) {
super(kvStore);
public PutObjectsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload);
PutObjectResponse response = kvStore.put(putObjectRequest);
PutObjectResponse response = kvStore.put(authResponse.getUserToken(), putObjectRequest);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in PutObjectsApi: ", e);
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/vss/auth/AuthResponse.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
package org.vss.auth;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
private String userToken;
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/vss/auth/Authorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// Interface for authorizer that is run before every request.
public interface Authorizer {
AuthResponse verify(HttpHeaders headers) throws AuthException;
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/vss/auth/NoopAuthorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// A no-operation authorizer, that lets any user-request go through.
public class NoopAuthorizer implements Authorizer {
private static String UNAUTHENTICATED_USER = "unauth-user";

@Override
public AuthResponse verify(HttpHeaders headers) throws AuthException {
return new AuthResponse(UNAUTHENTICATED_USER);
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/vss/guice/BaseModule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,10 @@
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.tools.StringUtils;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.auth.NoopAuthorizer;
import org.vss.impl.postgres.PostgresBackendImpl;

public class BaseModule extends AbstractModule {
Expand All@@ -20,6 +23,9 @@ public class BaseModule extends AbstractModule {
protected void configure() {
// Provide PostgresBackend as default implementation for KVStore.
bind(KVStore.class).to(PostgresBackendImpl.class).in(Singleton.class);

// Default to Noop Authorizer.
bind(Authorizer.class).to(NoopAuthorizer.class).in(Singleton.class);
}

@Provides
Expand Down
52 changes: 29 additions & 23 deletions app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,11 +48,12 @@ public PostgresBackendImpl(DSLContext context) {
}

@Override
public GetObjectResponse get(GetObjectRequest request) {
public GetObjectResponse get(String userToken, GetObjectRequest request) {

VssDbRecord vssDbRecord = context.selectFrom(VSS_DB)
.where(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey())))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey()))))
.fetchOne();

final KeyValue keyValue;
Expand All@@ -77,18 +78,18 @@ public GetObjectResponse get(GetObjectRequest request) {
}

@Override
public PutObjectResponse put(PutObjectRequest request) {
public PutObjectResponse put(String userToken, PutObjectRequest request) {

String storeId = request.getStoreId();

List<VssDbRecord> vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

List<VssDbRecord> vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

if (request.hasGlobalVersion()) {
VssDbRecord globalVersionRecord = buildVssRecord(storeId,
VssDbRecord globalVersionRecord = buildVssRecord(userToken, storeId,
KeyValue.newBuilder()
.setKey(GLOBAL_VERSION_KEY)
.setVersion(request.getGlobalVersion())
Expand DownExpand Up@@ -130,15 +131,17 @@ private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private static DeleteConditionStep<VssDbRecord> buildNonConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey())));
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))));
}

private static DeleteConditionStep<VssDbRecord> buildConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
Expand All@@ -153,9 +156,9 @@ private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onConflict(VSS_DB.STORE_ID, VSS_DB.KEY)
.onConflict(VSS_DB.USER_TOKEN, VSS_DB.STORE_ID, VSS_DB.KEY)
.doUpdate()
.set(VSS_DB.VALUE, vssRecord.getValue())
.set(VSS_DB.VERSION, 1L)
Expand All@@ -165,7 +168,7 @@ private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord v
private Insert<VssDbRecord> buildConditionalInsertRecordQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onDuplicateKeyIgnore();
}
Expand All@@ -175,14 +178,16 @@ private Update<VssDbRecord> buildConditionalUpdateRecordQuery(DSLContext dsl, Vs
.set(Map.of(VSS_DB.VALUE, vssRecord.getValue(),
VSS_DB.VERSION, vssRecord.getVersion() + 1,
VSS_DB.LAST_UPDATED_AT, vssRecord.getLastUpdatedAt()))
.where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
private VssDbRecord buildVssRecord(String userToken, String storeId, KeyValue kv) {
OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS);
return new VssDbRecord()
.setUserToken(userToken)
.setStoreId(storeId)
.setKey(kv.getKey())
.setValue(kv.getValue().toByteArray())
Expand All@@ -192,9 +197,9 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
}

@Override
public DeleteObjectResponse delete(DeleteObjectRequest request) {
public DeleteObjectResponse delete(String userToken, DeleteObjectRequest request) {
String storeId = request.getStoreId();
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
VssDbRecord vssDbRecord = buildVssRecord(userToken, storeId, request.getKeyValue());

context.transaction((ctx) -> {
DSLContext dsl = ctx.dsl();
Expand All@@ -206,7 +211,7 @@ public DeleteObjectResponse delete(DeleteObjectRequest request) {
}

@Override
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
public ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request) {
String storeId = request.getStoreId();
String keyPrefix = request.getKeyPrefix();
String pageToken = request.getPageToken();
Expand All@@ -221,12 +226,13 @@ public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
.setStoreId(storeId)
.setKey(GLOBAL_VERSION_KEY)
.build();
globalVersion = get(getGlobalVersionRequest).getValue().getVersion();
globalVersion = get(userToken, getGlobalVersionRequest).getValue().getVersion();
}

List<VssDbRecord> vssDbRecords = context.select(VSS_DB.KEY, VSS_DB.VERSION).from(VSS_DB)
.where(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix)))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix))))
.orderBy(VSS_DB.KEY)
.seek(pageToken)
.limit(Math.min(pageSize, LIST_KEY_VERSIONS_MAX_PAGE_SIZE))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
CREATE TABLE vss_db (
user_token character varying(120) NOT NULL CHECK (user_token <> ''),
store_id character varying(120) NOT NULL CHECK (store_id <> ''),
key character varying(600) NOT NULL,
value bytea NULL,
version bigint NOT NULL,
created_at TIMESTAMP WITH TIME ZONE,
last_updated_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (store_id, key)
PRIMARY KEY (user_token, store_id, key)
);
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/org/vss/KVStore.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,11 +4,11 @@ public interface KVStore {

String GLOBAL_VERSION_KEY = "vss_global_version";

GetObjectResponse get(GetObjectRequest request);
GetObjectResponse get(String userToken, GetObjectRequest request);

PutObjectResponse put(PutObjectRequest request);
PutObjectResponse put(String userToken, PutObjectRequest request);

DeleteObjectResponse delete(DeleteObjectRequest request);
DeleteObjectResponse delete(String userToken, DeleteObjectRequest request);

ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request);
}
5 changes: 4 additions & 1 deletion app/src/main/java/org/vss/api/AbstractVssApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,16 +7,19 @@
import org.vss.ErrorCode;
import org.vss.ErrorResponse;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.exception.AuthException;
import org.vss.exception.ConflictException;
import org.vss.exception.NoSuchKeyException;

public abstract class AbstractVssApi {
final KVStore kvStore;
final Authorizer authorizer;

@Inject
public AbstractVssApi(KVStore kvStore) {
public AbstractVssApi(KVStore kvStore, Authorizer authorizer) {
this.kvStore = kvStore;
this.authorizer = authorizer;
}

Response toResponse(GeneratedMessageV3 protoResponse) {
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/DeleteObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,27 +4,32 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.DeleteObjectRequest;
import org.vss.DeleteObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.DELETE_OBJECT)
@Slf4j
public class DeleteObjectApi extends AbstractVssApi {
@Inject
public DeleteObjectApi(KVStore kvstore) {
super(kvstore);
public DeleteObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
DeleteObjectResponse response = kvStore.delete(request);
DeleteObjectResponse response = kvStore.delete(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in DeleteObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/GetObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.GetObjectRequest;
import org.vss.GetObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.GET_OBJECT)
@Slf4j
public class GetObjectApi extends AbstractVssApi {

@Inject
public GetObjectApi(KVStore kvstore) {
super(kvstore);
public GetObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
GetObjectRequest request = GetObjectRequest.parseFrom(payload);
GetObjectResponse response = kvStore.get(request);
GetObjectResponse response = kvStore.get(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in GetObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/ListKeyVersionsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.ListKeyVersionsRequest;
import org.vss.ListKeyVersionsResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.LIST_KEY_VERSIONS)
@Slf4j
public class ListKeyVersionsApi extends AbstractVssApi {

@Inject
public ListKeyVersionsApi(KVStore kvStore) {
super(kvStore);
public ListKeyVersionsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload);
ListKeyVersionsResponse response = kvStore.listKeyVersions(request);
ListKeyVersionsResponse response = kvStore.listKeyVersions(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in ListKeyVersionsApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/PutObjectsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.PutObjectRequest;
import org.vss.PutObjectResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.PUT_OBJECTS)
@Slf4j
public class PutObjectsApi extends AbstractVssApi {

@Inject
public PutObjectsApi(KVStore kvStore) {
super(kvStore);
public PutObjectsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload);
PutObjectResponse response = kvStore.put(putObjectRequest);
PutObjectResponse response = kvStore.put(authResponse.getUserToken(), putObjectRequest);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in PutObjectsApi: ", e);
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/vss/auth/AuthResponse.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
package org.vss.auth;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
private String userToken;
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/vss/auth/Authorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// Interface for authorizer that is run before every request.
public interface Authorizer {
AuthResponse verify(HttpHeaders headers) throws AuthException;
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/vss/auth/NoopAuthorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// A no-operation authorizer, that lets any user-request go through.
public class NoopAuthorizer implements Authorizer {
private static String UNAUTHENTICATED_USER = "unauth-user";

@Override
public AuthResponse verify(HttpHeaders headers) throws AuthException {
return new AuthResponse(UNAUTHENTICATED_USER);
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/vss/guice/BaseModule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,10 @@
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.tools.StringUtils;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.auth.NoopAuthorizer;
import org.vss.impl.postgres.PostgresBackendImpl;

public class BaseModule extends AbstractModule {
Expand All@@ -20,6 +23,9 @@ public class BaseModule extends AbstractModule {
protected void configure() {
// Provide PostgresBackend as default implementation for KVStore.
bind(KVStore.class).to(PostgresBackendImpl.class).in(Singleton.class);

// Default to Noop Authorizer.
bind(Authorizer.class).to(NoopAuthorizer.class).in(Singleton.class);
}

@Provides
Expand Down
52 changes: 29 additions & 23 deletions app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,11 +48,12 @@ public PostgresBackendImpl(DSLContext context) {
}

@Override
public GetObjectResponse get(GetObjectRequest request) {
public GetObjectResponse get(String userToken, GetObjectRequest request) {

VssDbRecord vssDbRecord = context.selectFrom(VSS_DB)
.where(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey())))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey()))))
.fetchOne();

final KeyValue keyValue;
Expand All@@ -77,18 +78,18 @@ public GetObjectResponse get(GetObjectRequest request) {
}

@Override
public PutObjectResponse put(PutObjectRequest request) {
public PutObjectResponse put(String userToken, PutObjectRequest request) {

String storeId = request.getStoreId();

List<VssDbRecord> vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

List<VssDbRecord> vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

if (request.hasGlobalVersion()) {
VssDbRecord globalVersionRecord = buildVssRecord(storeId,
VssDbRecord globalVersionRecord = buildVssRecord(userToken, storeId,
KeyValue.newBuilder()
.setKey(GLOBAL_VERSION_KEY)
.setVersion(request.getGlobalVersion())
Expand DownExpand Up@@ -130,15 +131,17 @@ private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private static DeleteConditionStep<VssDbRecord> buildNonConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey())));
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))));
}

private static DeleteConditionStep<VssDbRecord> buildConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
Expand All@@ -153,9 +156,9 @@ private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onConflict(VSS_DB.STORE_ID, VSS_DB.KEY)
.onConflict(VSS_DB.USER_TOKEN, VSS_DB.STORE_ID, VSS_DB.KEY)
.doUpdate()
.set(VSS_DB.VALUE, vssRecord.getValue())
.set(VSS_DB.VERSION, 1L)
Expand All@@ -165,7 +168,7 @@ private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord v
private Insert<VssDbRecord> buildConditionalInsertRecordQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onDuplicateKeyIgnore();
}
Expand All@@ -175,14 +178,16 @@ private Update<VssDbRecord> buildConditionalUpdateRecordQuery(DSLContext dsl, Vs
.set(Map.of(VSS_DB.VALUE, vssRecord.getValue(),
VSS_DB.VERSION, vssRecord.getVersion() + 1,
VSS_DB.LAST_UPDATED_AT, vssRecord.getLastUpdatedAt()))
.where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
private VssDbRecord buildVssRecord(String userToken, String storeId, KeyValue kv) {
OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS);
return new VssDbRecord()
.setUserToken(userToken)
.setStoreId(storeId)
.setKey(kv.getKey())
.setValue(kv.getValue().toByteArray())
Expand All@@ -192,9 +197,9 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
}

@Override
public DeleteObjectResponse delete(DeleteObjectRequest request) {
public DeleteObjectResponse delete(String userToken, DeleteObjectRequest request) {
String storeId = request.getStoreId();
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
VssDbRecord vssDbRecord = buildVssRecord(userToken, storeId, request.getKeyValue());

context.transaction((ctx) -> {
DSLContext dsl = ctx.dsl();
Expand All@@ -206,7 +211,7 @@ public DeleteObjectResponse delete(DeleteObjectRequest request) {
}

@Override
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
public ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request) {
String storeId = request.getStoreId();
String keyPrefix = request.getKeyPrefix();
String pageToken = request.getPageToken();
Expand All@@ -221,12 +226,13 @@ public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
.setStoreId(storeId)
.setKey(GLOBAL_VERSION_KEY)
.build();
globalVersion = get(getGlobalVersionRequest).getValue().getVersion();
globalVersion = get(userToken, getGlobalVersionRequest).getValue().getVersion();
}

List<VssDbRecord> vssDbRecords = context.select(VSS_DB.KEY, VSS_DB.VERSION).from(VSS_DB)
.where(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix)))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix))))
.orderBy(VSS_DB.KEY)
.seek(pageToken)
.limit(Math.min(pageSize, LIST_KEY_VERSIONS_MAX_PAGE_SIZE))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
CREATE TABLE vss_db (
user_token character varying(120) NOT NULL CHECK (user_token <> ''),
store_id character varying(120) NOT NULL CHECK (store_id <> ''),
key character varying(600) NOT NULL,
value bytea NULL,
version bigint NOT NULL,
created_at TIMESTAMP WITH TIME ZONE,
last_updated_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (store_id, key)
PRIMARY KEY (user_token, store_id, key)
);
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/org/vss/KVStore.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,11 +4,11 @@ public interface KVStore {

String GLOBAL_VERSION_KEY = "vss_global_version";

GetObjectResponse get(GetObjectRequest request);
GetObjectResponse get(String userToken, GetObjectRequest request);

PutObjectResponse put(PutObjectRequest request);
PutObjectResponse put(String userToken, PutObjectRequest request);

DeleteObjectResponse delete(DeleteObjectRequest request);
DeleteObjectResponse delete(String userToken, DeleteObjectRequest request);

ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request);
}
5 changes: 4 additions & 1 deletion app/src/main/java/org/vss/api/AbstractVssApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,16 +7,19 @@
import org.vss.ErrorCode;
import org.vss.ErrorResponse;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.exception.AuthException;
import org.vss.exception.ConflictException;
import org.vss.exception.NoSuchKeyException;

public abstract class AbstractVssApi {
final KVStore kvStore;
final Authorizer authorizer;

@Inject
public AbstractVssApi(KVStore kvStore) {
public AbstractVssApi(KVStore kvStore, Authorizer authorizer) {
this.kvStore = kvStore;
this.authorizer = authorizer;
}

Response toResponse(GeneratedMessageV3 protoResponse) {
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/DeleteObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,27 +4,32 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.DeleteObjectRequest;
import org.vss.DeleteObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.DELETE_OBJECT)
@Slf4j
public class DeleteObjectApi extends AbstractVssApi {
@Inject
public DeleteObjectApi(KVStore kvstore) {
super(kvstore);
public DeleteObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
DeleteObjectResponse response = kvStore.delete(request);
DeleteObjectResponse response = kvStore.delete(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in DeleteObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/GetObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.GetObjectRequest;
import org.vss.GetObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.GET_OBJECT)
@Slf4j
public class GetObjectApi extends AbstractVssApi {

@Inject
public GetObjectApi(KVStore kvstore) {
super(kvstore);
public GetObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
GetObjectRequest request = GetObjectRequest.parseFrom(payload);
GetObjectResponse response = kvStore.get(request);
GetObjectResponse response = kvStore.get(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in GetObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/ListKeyVersionsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.ListKeyVersionsRequest;
import org.vss.ListKeyVersionsResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.LIST_KEY_VERSIONS)
@Slf4j
public class ListKeyVersionsApi extends AbstractVssApi {

@Inject
public ListKeyVersionsApi(KVStore kvStore) {
super(kvStore);
public ListKeyVersionsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload);
ListKeyVersionsResponse response = kvStore.listKeyVersions(request);
ListKeyVersionsResponse response = kvStore.listKeyVersions(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in ListKeyVersionsApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/PutObjectsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.PutObjectRequest;
import org.vss.PutObjectResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.PUT_OBJECTS)
@Slf4j
public class PutObjectsApi extends AbstractVssApi {

@Inject
public PutObjectsApi(KVStore kvStore) {
super(kvStore);
public PutObjectsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload);
PutObjectResponse response = kvStore.put(putObjectRequest);
PutObjectResponse response = kvStore.put(authResponse.getUserToken(), putObjectRequest);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in PutObjectsApi: ", e);
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/vss/auth/AuthResponse.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
package org.vss.auth;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
private String userToken;
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/vss/auth/Authorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// Interface for authorizer that is run before every request.
public interface Authorizer {
AuthResponse verify(HttpHeaders headers) throws AuthException;
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/vss/auth/NoopAuthorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// A no-operation authorizer, that lets any user-request go through.
public class NoopAuthorizer implements Authorizer {
private static String UNAUTHENTICATED_USER = "unauth-user";

@Override
public AuthResponse verify(HttpHeaders headers) throws AuthException {
return new AuthResponse(UNAUTHENTICATED_USER);
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/vss/guice/BaseModule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,10 @@
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.tools.StringUtils;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.auth.NoopAuthorizer;
import org.vss.impl.postgres.PostgresBackendImpl;

public class BaseModule extends AbstractModule {
Expand All@@ -20,6 +23,9 @@ public class BaseModule extends AbstractModule {
protected void configure() {
// Provide PostgresBackend as default implementation for KVStore.
bind(KVStore.class).to(PostgresBackendImpl.class).in(Singleton.class);

// Default to Noop Authorizer.
bind(Authorizer.class).to(NoopAuthorizer.class).in(Singleton.class);
}

@Provides
Expand Down
52 changes: 29 additions & 23 deletions app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,11 +48,12 @@ public PostgresBackendImpl(DSLContext context) {
}

@Override
public GetObjectResponse get(GetObjectRequest request) {
public GetObjectResponse get(String userToken, GetObjectRequest request) {

VssDbRecord vssDbRecord = context.selectFrom(VSS_DB)
.where(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey())))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey()))))
.fetchOne();

final KeyValue keyValue;
Expand All@@ -77,18 +78,18 @@ public GetObjectResponse get(GetObjectRequest request) {
}

@Override
public PutObjectResponse put(PutObjectRequest request) {
public PutObjectResponse put(String userToken, PutObjectRequest request) {

String storeId = request.getStoreId();

List<VssDbRecord> vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

List<VssDbRecord> vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

if (request.hasGlobalVersion()) {
VssDbRecord globalVersionRecord = buildVssRecord(storeId,
VssDbRecord globalVersionRecord = buildVssRecord(userToken, storeId,
KeyValue.newBuilder()
.setKey(GLOBAL_VERSION_KEY)
.setVersion(request.getGlobalVersion())
Expand DownExpand Up@@ -130,15 +131,17 @@ private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private static DeleteConditionStep<VssDbRecord> buildNonConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey())));
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))));
}

private static DeleteConditionStep<VssDbRecord> buildConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
Expand All@@ -153,9 +156,9 @@ private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onConflict(VSS_DB.STORE_ID, VSS_DB.KEY)
.onConflict(VSS_DB.USER_TOKEN, VSS_DB.STORE_ID, VSS_DB.KEY)
.doUpdate()
.set(VSS_DB.VALUE, vssRecord.getValue())
.set(VSS_DB.VERSION, 1L)
Expand All@@ -165,7 +168,7 @@ private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord v
private Insert<VssDbRecord> buildConditionalInsertRecordQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onDuplicateKeyIgnore();
}
Expand All@@ -175,14 +178,16 @@ private Update<VssDbRecord> buildConditionalUpdateRecordQuery(DSLContext dsl, Vs
.set(Map.of(VSS_DB.VALUE, vssRecord.getValue(),
VSS_DB.VERSION, vssRecord.getVersion() + 1,
VSS_DB.LAST_UPDATED_AT, vssRecord.getLastUpdatedAt()))
.where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
private VssDbRecord buildVssRecord(String userToken, String storeId, KeyValue kv) {
OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS);
return new VssDbRecord()
.setUserToken(userToken)
.setStoreId(storeId)
.setKey(kv.getKey())
.setValue(kv.getValue().toByteArray())
Expand All@@ -192,9 +197,9 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
}

@Override
public DeleteObjectResponse delete(DeleteObjectRequest request) {
public DeleteObjectResponse delete(String userToken, DeleteObjectRequest request) {
String storeId = request.getStoreId();
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
VssDbRecord vssDbRecord = buildVssRecord(userToken, storeId, request.getKeyValue());

context.transaction((ctx) -> {
DSLContext dsl = ctx.dsl();
Expand All@@ -206,7 +211,7 @@ public DeleteObjectResponse delete(DeleteObjectRequest request) {
}

@Override
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
public ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request) {
String storeId = request.getStoreId();
String keyPrefix = request.getKeyPrefix();
String pageToken = request.getPageToken();
Expand All@@ -221,12 +226,13 @@ public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
.setStoreId(storeId)
.setKey(GLOBAL_VERSION_KEY)
.build();
globalVersion = get(getGlobalVersionRequest).getValue().getVersion();
globalVersion = get(userToken, getGlobalVersionRequest).getValue().getVersion();
}

List<VssDbRecord> vssDbRecords = context.select(VSS_DB.KEY, VSS_DB.VERSION).from(VSS_DB)
.where(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix)))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix))))
.orderBy(VSS_DB.KEY)
.seek(pageToken)
.limit(Math.min(pageSize, LIST_KEY_VERSIONS_MAX_PAGE_SIZE))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
CREATE TABLE vss_db (
user_token character varying(120) NOT NULL CHECK (user_token <> ''),
store_id character varying(120) NOT NULL CHECK (store_id <> ''),
key character varying(600) NOT NULL,
value bytea NULL,
version bigint NOT NULL,
created_at TIMESTAMP WITH TIME ZONE,
last_updated_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (store_id, key)
PRIMARY KEY (user_token, store_id, key)
);
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/org/vss/KVStore.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,11 +4,11 @@ public interface KVStore {

String GLOBAL_VERSION_KEY = "vss_global_version";

GetObjectResponse get(GetObjectRequest request);
GetObjectResponse get(String userToken, GetObjectRequest request);

PutObjectResponse put(PutObjectRequest request);
PutObjectResponse put(String userToken, PutObjectRequest request);

DeleteObjectResponse delete(DeleteObjectRequest request);
DeleteObjectResponse delete(String userToken, DeleteObjectRequest request);

ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request);
}
5 changes: 4 additions & 1 deletion app/src/main/java/org/vss/api/AbstractVssApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,16 +7,19 @@
import org.vss.ErrorCode;
import org.vss.ErrorResponse;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.exception.AuthException;
import org.vss.exception.ConflictException;
import org.vss.exception.NoSuchKeyException;

public abstract class AbstractVssApi {
final KVStore kvStore;
final Authorizer authorizer;

@Inject
public AbstractVssApi(KVStore kvStore) {
public AbstractVssApi(KVStore kvStore, Authorizer authorizer) {
this.kvStore = kvStore;
this.authorizer = authorizer;
}

Response toResponse(GeneratedMessageV3 protoResponse) {
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/DeleteObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,27 +4,32 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.DeleteObjectRequest;
import org.vss.DeleteObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.DELETE_OBJECT)
@Slf4j
public class DeleteObjectApi extends AbstractVssApi {
@Inject
public DeleteObjectApi(KVStore kvstore) {
super(kvstore);
public DeleteObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
DeleteObjectResponse response = kvStore.delete(request);
DeleteObjectResponse response = kvStore.delete(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in DeleteObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/GetObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.GetObjectRequest;
import org.vss.GetObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.GET_OBJECT)
@Slf4j
public class GetObjectApi extends AbstractVssApi {

@Inject
public GetObjectApi(KVStore kvstore) {
super(kvstore);
public GetObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
GetObjectRequest request = GetObjectRequest.parseFrom(payload);
GetObjectResponse response = kvStore.get(request);
GetObjectResponse response = kvStore.get(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in GetObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/ListKeyVersionsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.ListKeyVersionsRequest;
import org.vss.ListKeyVersionsResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.LIST_KEY_VERSIONS)
@Slf4j
public class ListKeyVersionsApi extends AbstractVssApi {

@Inject
public ListKeyVersionsApi(KVStore kvStore) {
super(kvStore);
public ListKeyVersionsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload);
ListKeyVersionsResponse response = kvStore.listKeyVersions(request);
ListKeyVersionsResponse response = kvStore.listKeyVersions(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in ListKeyVersionsApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/PutObjectsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.PutObjectRequest;
import org.vss.PutObjectResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.PUT_OBJECTS)
@Slf4j
public class PutObjectsApi extends AbstractVssApi {

@Inject
public PutObjectsApi(KVStore kvStore) {
super(kvStore);
public PutObjectsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload);
PutObjectResponse response = kvStore.put(putObjectRequest);
PutObjectResponse response = kvStore.put(authResponse.getUserToken(), putObjectRequest);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in PutObjectsApi: ", e);
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/vss/auth/AuthResponse.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
package org.vss.auth;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
private String userToken;
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/vss/auth/Authorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// Interface for authorizer that is run before every request.
public interface Authorizer {
AuthResponse verify(HttpHeaders headers) throws AuthException;
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/vss/auth/NoopAuthorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// A no-operation authorizer, that lets any user-request go through.
public class NoopAuthorizer implements Authorizer {
private static String UNAUTHENTICATED_USER = "unauth-user";

@Override
public AuthResponse verify(HttpHeaders headers) throws AuthException {
return new AuthResponse(UNAUTHENTICATED_USER);
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/vss/guice/BaseModule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,10 @@
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.tools.StringUtils;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.auth.NoopAuthorizer;
import org.vss.impl.postgres.PostgresBackendImpl;

public class BaseModule extends AbstractModule {
Expand All@@ -20,6 +23,9 @@ public class BaseModule extends AbstractModule {
protected void configure() {
// Provide PostgresBackend as default implementation for KVStore.
bind(KVStore.class).to(PostgresBackendImpl.class).in(Singleton.class);

// Default to Noop Authorizer.
bind(Authorizer.class).to(NoopAuthorizer.class).in(Singleton.class);
}

@Provides
Expand Down
52 changes: 29 additions & 23 deletions app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,11 +48,12 @@ public PostgresBackendImpl(DSLContext context) {
}

@Override
public GetObjectResponse get(GetObjectRequest request) {
public GetObjectResponse get(String userToken, GetObjectRequest request) {

VssDbRecord vssDbRecord = context.selectFrom(VSS_DB)
.where(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey())))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey()))))
.fetchOne();

final KeyValue keyValue;
Expand All@@ -77,18 +78,18 @@ public GetObjectResponse get(GetObjectRequest request) {
}

@Override
public PutObjectResponse put(PutObjectRequest request) {
public PutObjectResponse put(String userToken, PutObjectRequest request) {

String storeId = request.getStoreId();

List<VssDbRecord> vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

List<VssDbRecord> vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

if (request.hasGlobalVersion()) {
VssDbRecord globalVersionRecord = buildVssRecord(storeId,
VssDbRecord globalVersionRecord = buildVssRecord(userToken, storeId,
KeyValue.newBuilder()
.setKey(GLOBAL_VERSION_KEY)
.setVersion(request.getGlobalVersion())
Expand DownExpand Up@@ -130,15 +131,17 @@ private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private static DeleteConditionStep<VssDbRecord> buildNonConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey())));
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))));
}

private static DeleteConditionStep<VssDbRecord> buildConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
Expand All@@ -153,9 +156,9 @@ private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onConflict(VSS_DB.STORE_ID, VSS_DB.KEY)
.onConflict(VSS_DB.USER_TOKEN, VSS_DB.STORE_ID, VSS_DB.KEY)
.doUpdate()
.set(VSS_DB.VALUE, vssRecord.getValue())
.set(VSS_DB.VERSION, 1L)
Expand All@@ -165,7 +168,7 @@ private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord v
private Insert<VssDbRecord> buildConditionalInsertRecordQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onDuplicateKeyIgnore();
}
Expand All@@ -175,14 +178,16 @@ private Update<VssDbRecord> buildConditionalUpdateRecordQuery(DSLContext dsl, Vs
.set(Map.of(VSS_DB.VALUE, vssRecord.getValue(),
VSS_DB.VERSION, vssRecord.getVersion() + 1,
VSS_DB.LAST_UPDATED_AT, vssRecord.getLastUpdatedAt()))
.where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
private VssDbRecord buildVssRecord(String userToken, String storeId, KeyValue kv) {
OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS);
return new VssDbRecord()
.setUserToken(userToken)
.setStoreId(storeId)
.setKey(kv.getKey())
.setValue(kv.getValue().toByteArray())
Expand All@@ -192,9 +197,9 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
}

@Override
public DeleteObjectResponse delete(DeleteObjectRequest request) {
public DeleteObjectResponse delete(String userToken, DeleteObjectRequest request) {
String storeId = request.getStoreId();
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
VssDbRecord vssDbRecord = buildVssRecord(userToken, storeId, request.getKeyValue());

context.transaction((ctx) -> {
DSLContext dsl = ctx.dsl();
Expand All@@ -206,7 +211,7 @@ public DeleteObjectResponse delete(DeleteObjectRequest request) {
}

@Override
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
public ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request) {
String storeId = request.getStoreId();
String keyPrefix = request.getKeyPrefix();
String pageToken = request.getPageToken();
Expand All@@ -221,12 +226,13 @@ public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
.setStoreId(storeId)
.setKey(GLOBAL_VERSION_KEY)
.build();
globalVersion = get(getGlobalVersionRequest).getValue().getVersion();
globalVersion = get(userToken, getGlobalVersionRequest).getValue().getVersion();
}

List<VssDbRecord> vssDbRecords = context.select(VSS_DB.KEY, VSS_DB.VERSION).from(VSS_DB)
.where(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix)))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix))))
.orderBy(VSS_DB.KEY)
.seek(pageToken)
.limit(Math.min(pageSize, LIST_KEY_VERSIONS_MAX_PAGE_SIZE))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
CREATE TABLE vss_db (
user_token character varying(120) NOT NULL CHECK (user_token <> ''),
store_id character varying(120) NOT NULL CHECK (store_id <> ''),
key character varying(600) NOT NULL,
value bytea NULL,
version bigint NOT NULL,
created_at TIMESTAMP WITH TIME ZONE,
last_updated_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (store_id, key)
PRIMARY KEY (user_token, store_id, key)
);
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/org/vss/KVStore.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,11 +4,11 @@ public interface KVStore {

String GLOBAL_VERSION_KEY = "vss_global_version";

GetObjectResponse get(GetObjectRequest request);
GetObjectResponse get(String userToken, GetObjectRequest request);

PutObjectResponse put(PutObjectRequest request);
PutObjectResponse put(String userToken, PutObjectRequest request);

DeleteObjectResponse delete(DeleteObjectRequest request);
DeleteObjectResponse delete(String userToken, DeleteObjectRequest request);

ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request);
}
5 changes: 4 additions & 1 deletion app/src/main/java/org/vss/api/AbstractVssApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,16 +7,19 @@
import org.vss.ErrorCode;
import org.vss.ErrorResponse;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.exception.AuthException;
import org.vss.exception.ConflictException;
import org.vss.exception.NoSuchKeyException;

public abstract class AbstractVssApi {
final KVStore kvStore;
final Authorizer authorizer;

@Inject
public AbstractVssApi(KVStore kvStore) {
public AbstractVssApi(KVStore kvStore, Authorizer authorizer) {
this.kvStore = kvStore;
this.authorizer = authorizer;
}

Response toResponse(GeneratedMessageV3 protoResponse) {
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/DeleteObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,27 +4,32 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.DeleteObjectRequest;
import org.vss.DeleteObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.DELETE_OBJECT)
@Slf4j
public class DeleteObjectApi extends AbstractVssApi {
@Inject
public DeleteObjectApi(KVStore kvstore) {
super(kvstore);
public DeleteObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
DeleteObjectResponse response = kvStore.delete(request);
DeleteObjectResponse response = kvStore.delete(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in DeleteObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/GetObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.GetObjectRequest;
import org.vss.GetObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.GET_OBJECT)
@Slf4j
public class GetObjectApi extends AbstractVssApi {

@Inject
public GetObjectApi(KVStore kvstore) {
super(kvstore);
public GetObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
GetObjectRequest request = GetObjectRequest.parseFrom(payload);
GetObjectResponse response = kvStore.get(request);
GetObjectResponse response = kvStore.get(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in GetObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/ListKeyVersionsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.ListKeyVersionsRequest;
import org.vss.ListKeyVersionsResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.LIST_KEY_VERSIONS)
@Slf4j
public class ListKeyVersionsApi extends AbstractVssApi {

@Inject
public ListKeyVersionsApi(KVStore kvStore) {
super(kvStore);
public ListKeyVersionsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload);
ListKeyVersionsResponse response = kvStore.listKeyVersions(request);
ListKeyVersionsResponse response = kvStore.listKeyVersions(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in ListKeyVersionsApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/PutObjectsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.PutObjectRequest;
import org.vss.PutObjectResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.PUT_OBJECTS)
@Slf4j
public class PutObjectsApi extends AbstractVssApi {

@Inject
public PutObjectsApi(KVStore kvStore) {
super(kvStore);
public PutObjectsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload);
PutObjectResponse response = kvStore.put(putObjectRequest);
PutObjectResponse response = kvStore.put(authResponse.getUserToken(), putObjectRequest);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in PutObjectsApi: ", e);
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/vss/auth/AuthResponse.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
package org.vss.auth;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
private String userToken;
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/vss/auth/Authorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// Interface for authorizer that is run before every request.
public interface Authorizer {
AuthResponse verify(HttpHeaders headers) throws AuthException;
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/vss/auth/NoopAuthorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// A no-operation authorizer, that lets any user-request go through.
public class NoopAuthorizer implements Authorizer {
private static String UNAUTHENTICATED_USER = "unauth-user";

@Override
public AuthResponse verify(HttpHeaders headers) throws AuthException {
return new AuthResponse(UNAUTHENTICATED_USER);
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/vss/guice/BaseModule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,10 @@
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.tools.StringUtils;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.auth.NoopAuthorizer;
import org.vss.impl.postgres.PostgresBackendImpl;

public class BaseModule extends AbstractModule {
Expand All@@ -20,6 +23,9 @@ public class BaseModule extends AbstractModule {
protected void configure() {
// Provide PostgresBackend as default implementation for KVStore.
bind(KVStore.class).to(PostgresBackendImpl.class).in(Singleton.class);

// Default to Noop Authorizer.
bind(Authorizer.class).to(NoopAuthorizer.class).in(Singleton.class);
}

@Provides
Expand Down
52 changes: 29 additions & 23 deletions app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,11 +48,12 @@ public PostgresBackendImpl(DSLContext context) {
}

@Override
public GetObjectResponse get(GetObjectRequest request) {
public GetObjectResponse get(String userToken, GetObjectRequest request) {

VssDbRecord vssDbRecord = context.selectFrom(VSS_DB)
.where(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey())))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey()))))
.fetchOne();

final KeyValue keyValue;
Expand All@@ -77,18 +78,18 @@ public GetObjectResponse get(GetObjectRequest request) {
}

@Override
public PutObjectResponse put(PutObjectRequest request) {
public PutObjectResponse put(String userToken, PutObjectRequest request) {

String storeId = request.getStoreId();

List<VssDbRecord> vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

List<VssDbRecord> vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

if (request.hasGlobalVersion()) {
VssDbRecord globalVersionRecord = buildVssRecord(storeId,
VssDbRecord globalVersionRecord = buildVssRecord(userToken, storeId,
KeyValue.newBuilder()
.setKey(GLOBAL_VERSION_KEY)
.setVersion(request.getGlobalVersion())
Expand DownExpand Up@@ -130,15 +131,17 @@ private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private static DeleteConditionStep<VssDbRecord> buildNonConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey())));
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))));
}

private static DeleteConditionStep<VssDbRecord> buildConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
Expand All@@ -153,9 +156,9 @@ private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onConflict(VSS_DB.STORE_ID, VSS_DB.KEY)
.onConflict(VSS_DB.USER_TOKEN, VSS_DB.STORE_ID, VSS_DB.KEY)
.doUpdate()
.set(VSS_DB.VALUE, vssRecord.getValue())
.set(VSS_DB.VERSION, 1L)
Expand All@@ -165,7 +168,7 @@ private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord v
private Insert<VssDbRecord> buildConditionalInsertRecordQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onDuplicateKeyIgnore();
}
Expand All@@ -175,14 +178,16 @@ private Update<VssDbRecord> buildConditionalUpdateRecordQuery(DSLContext dsl, Vs
.set(Map.of(VSS_DB.VALUE, vssRecord.getValue(),
VSS_DB.VERSION, vssRecord.getVersion() + 1,
VSS_DB.LAST_UPDATED_AT, vssRecord.getLastUpdatedAt()))
.where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
private VssDbRecord buildVssRecord(String userToken, String storeId, KeyValue kv) {
OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS);
return new VssDbRecord()
.setUserToken(userToken)
.setStoreId(storeId)
.setKey(kv.getKey())
.setValue(kv.getValue().toByteArray())
Expand All@@ -192,9 +197,9 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
}

@Override
public DeleteObjectResponse delete(DeleteObjectRequest request) {
public DeleteObjectResponse delete(String userToken, DeleteObjectRequest request) {
String storeId = request.getStoreId();
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
VssDbRecord vssDbRecord = buildVssRecord(userToken, storeId, request.getKeyValue());

context.transaction((ctx) -> {
DSLContext dsl = ctx.dsl();
Expand All@@ -206,7 +211,7 @@ public DeleteObjectResponse delete(DeleteObjectRequest request) {
}

@Override
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
public ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request) {
String storeId = request.getStoreId();
String keyPrefix = request.getKeyPrefix();
String pageToken = request.getPageToken();
Expand All@@ -221,12 +226,13 @@ public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
.setStoreId(storeId)
.setKey(GLOBAL_VERSION_KEY)
.build();
globalVersion = get(getGlobalVersionRequest).getValue().getVersion();
globalVersion = get(userToken, getGlobalVersionRequest).getValue().getVersion();
}

List<VssDbRecord> vssDbRecords = context.select(VSS_DB.KEY, VSS_DB.VERSION).from(VSS_DB)
.where(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix)))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix))))
.orderBy(VSS_DB.KEY)
.seek(pageToken)
.limit(Math.min(pageSize, LIST_KEY_VERSIONS_MAX_PAGE_SIZE))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
CREATE TABLE vss_db (
user_token character varying(120) NOT NULL CHECK (user_token <> ''),
store_id character varying(120) NOT NULL CHECK (store_id <> ''),
key character varying(600) NOT NULL,
value bytea NULL,
version bigint NOT NULL,
created_at TIMESTAMP WITH TIME ZONE,
last_updated_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (store_id, key)
PRIMARY KEY (user_token, store_id, key)
);
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/org/vss/KVStore.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,11 +4,11 @@ public interface KVStore {

String GLOBAL_VERSION_KEY = "vss_global_version";

GetObjectResponse get(GetObjectRequest request);
GetObjectResponse get(String userToken, GetObjectRequest request);

PutObjectResponse put(PutObjectRequest request);
PutObjectResponse put(String userToken, PutObjectRequest request);

DeleteObjectResponse delete(DeleteObjectRequest request);
DeleteObjectResponse delete(String userToken, DeleteObjectRequest request);

ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request);
}
5 changes: 4 additions & 1 deletion app/src/main/java/org/vss/api/AbstractVssApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,16 +7,19 @@
import org.vss.ErrorCode;
import org.vss.ErrorResponse;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.exception.AuthException;
import org.vss.exception.ConflictException;
import org.vss.exception.NoSuchKeyException;

public abstract class AbstractVssApi {
final KVStore kvStore;
final Authorizer authorizer;

@Inject
public AbstractVssApi(KVStore kvStore) {
public AbstractVssApi(KVStore kvStore, Authorizer authorizer) {
this.kvStore = kvStore;
this.authorizer = authorizer;
}

Response toResponse(GeneratedMessageV3 protoResponse) {
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/DeleteObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,27 +4,32 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.DeleteObjectRequest;
import org.vss.DeleteObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.DELETE_OBJECT)
@Slf4j
public class DeleteObjectApi extends AbstractVssApi {
@Inject
public DeleteObjectApi(KVStore kvstore) {
super(kvstore);
public DeleteObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
DeleteObjectResponse response = kvStore.delete(request);
DeleteObjectResponse response = kvStore.delete(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in DeleteObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/GetObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.GetObjectRequest;
import org.vss.GetObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.GET_OBJECT)
@Slf4j
public class GetObjectApi extends AbstractVssApi {

@Inject
public GetObjectApi(KVStore kvstore) {
super(kvstore);
public GetObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
GetObjectRequest request = GetObjectRequest.parseFrom(payload);
GetObjectResponse response = kvStore.get(request);
GetObjectResponse response = kvStore.get(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in GetObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/ListKeyVersionsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.ListKeyVersionsRequest;
import org.vss.ListKeyVersionsResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.LIST_KEY_VERSIONS)
@Slf4j
public class ListKeyVersionsApi extends AbstractVssApi {

@Inject
public ListKeyVersionsApi(KVStore kvStore) {
super(kvStore);
public ListKeyVersionsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload);
ListKeyVersionsResponse response = kvStore.listKeyVersions(request);
ListKeyVersionsResponse response = kvStore.listKeyVersions(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in ListKeyVersionsApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/PutObjectsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.PutObjectRequest;
import org.vss.PutObjectResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.PUT_OBJECTS)
@Slf4j
public class PutObjectsApi extends AbstractVssApi {

@Inject
public PutObjectsApi(KVStore kvStore) {
super(kvStore);
public PutObjectsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload);
PutObjectResponse response = kvStore.put(putObjectRequest);
PutObjectResponse response = kvStore.put(authResponse.getUserToken(), putObjectRequest);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in PutObjectsApi: ", e);
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/vss/auth/AuthResponse.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
package org.vss.auth;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
private String userToken;
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/vss/auth/Authorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// Interface for authorizer that is run before every request.
public interface Authorizer {
AuthResponse verify(HttpHeaders headers) throws AuthException;
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/vss/auth/NoopAuthorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// A no-operation authorizer, that lets any user-request go through.
public class NoopAuthorizer implements Authorizer {
private static String UNAUTHENTICATED_USER = "unauth-user";

@Override
public AuthResponse verify(HttpHeaders headers) throws AuthException {
return new AuthResponse(UNAUTHENTICATED_USER);
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/vss/guice/BaseModule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,10 @@
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.tools.StringUtils;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.auth.NoopAuthorizer;
import org.vss.impl.postgres.PostgresBackendImpl;

public class BaseModule extends AbstractModule {
Expand All@@ -20,6 +23,9 @@ public class BaseModule extends AbstractModule {
protected void configure() {
// Provide PostgresBackend as default implementation for KVStore.
bind(KVStore.class).to(PostgresBackendImpl.class).in(Singleton.class);

// Default to Noop Authorizer.
bind(Authorizer.class).to(NoopAuthorizer.class).in(Singleton.class);
}

@Provides
Expand Down
52 changes: 29 additions & 23 deletions app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,11 +48,12 @@ public PostgresBackendImpl(DSLContext context) {
}

@Override
public GetObjectResponse get(GetObjectRequest request) {
public GetObjectResponse get(String userToken, GetObjectRequest request) {

VssDbRecord vssDbRecord = context.selectFrom(VSS_DB)
.where(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey())))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey()))))
.fetchOne();

final KeyValue keyValue;
Expand All@@ -77,18 +78,18 @@ public GetObjectResponse get(GetObjectRequest request) {
}

@Override
public PutObjectResponse put(PutObjectRequest request) {
public PutObjectResponse put(String userToken, PutObjectRequest request) {

String storeId = request.getStoreId();

List<VssDbRecord> vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

List<VssDbRecord> vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

if (request.hasGlobalVersion()) {
VssDbRecord globalVersionRecord = buildVssRecord(storeId,
VssDbRecord globalVersionRecord = buildVssRecord(userToken, storeId,
KeyValue.newBuilder()
.setKey(GLOBAL_VERSION_KEY)
.setVersion(request.getGlobalVersion())
Expand DownExpand Up@@ -130,15 +131,17 @@ private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private static DeleteConditionStep<VssDbRecord> buildNonConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey())));
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))));
}

private static DeleteConditionStep<VssDbRecord> buildConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
Expand All@@ -153,9 +156,9 @@ private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onConflict(VSS_DB.STORE_ID, VSS_DB.KEY)
.onConflict(VSS_DB.USER_TOKEN, VSS_DB.STORE_ID, VSS_DB.KEY)
.doUpdate()
.set(VSS_DB.VALUE, vssRecord.getValue())
.set(VSS_DB.VERSION, 1L)
Expand All@@ -165,7 +168,7 @@ private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord v
private Insert<VssDbRecord> buildConditionalInsertRecordQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onDuplicateKeyIgnore();
}
Expand All@@ -175,14 +178,16 @@ private Update<VssDbRecord> buildConditionalUpdateRecordQuery(DSLContext dsl, Vs
.set(Map.of(VSS_DB.VALUE, vssRecord.getValue(),
VSS_DB.VERSION, vssRecord.getVersion() + 1,
VSS_DB.LAST_UPDATED_AT, vssRecord.getLastUpdatedAt()))
.where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
private VssDbRecord buildVssRecord(String userToken, String storeId, KeyValue kv) {
OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS);
return new VssDbRecord()
.setUserToken(userToken)
.setStoreId(storeId)
.setKey(kv.getKey())
.setValue(kv.getValue().toByteArray())
Expand All@@ -192,9 +197,9 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
}

@Override
public DeleteObjectResponse delete(DeleteObjectRequest request) {
public DeleteObjectResponse delete(String userToken, DeleteObjectRequest request) {
String storeId = request.getStoreId();
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
VssDbRecord vssDbRecord = buildVssRecord(userToken, storeId, request.getKeyValue());

context.transaction((ctx) -> {
DSLContext dsl = ctx.dsl();
Expand All@@ -206,7 +211,7 @@ public DeleteObjectResponse delete(DeleteObjectRequest request) {
}

@Override
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
public ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request) {
String storeId = request.getStoreId();
String keyPrefix = request.getKeyPrefix();
String pageToken = request.getPageToken();
Expand All@@ -221,12 +226,13 @@ public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
.setStoreId(storeId)
.setKey(GLOBAL_VERSION_KEY)
.build();
globalVersion = get(getGlobalVersionRequest).getValue().getVersion();
globalVersion = get(userToken, getGlobalVersionRequest).getValue().getVersion();
}

List<VssDbRecord> vssDbRecords = context.select(VSS_DB.KEY, VSS_DB.VERSION).from(VSS_DB)
.where(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix)))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix))))
.orderBy(VSS_DB.KEY)
.seek(pageToken)
.limit(Math.min(pageSize, LIST_KEY_VERSIONS_MAX_PAGE_SIZE))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
CREATE TABLE vss_db (
user_token character varying(120) NOT NULL CHECK (user_token <> ''),
store_id character varying(120) NOT NULL CHECK (store_id <> ''),
key character varying(600) NOT NULL,
value bytea NULL,
version bigint NOT NULL,
created_at TIMESTAMP WITH TIME ZONE,
last_updated_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (store_id, key)
PRIMARY KEY (user_token, store_id, key)
);
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/org/vss/KVStore.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,11 +4,11 @@ public interface KVStore {

String GLOBAL_VERSION_KEY = "vss_global_version";

GetObjectResponse get(GetObjectRequest request);
GetObjectResponse get(String userToken, GetObjectRequest request);

PutObjectResponse put(PutObjectRequest request);
PutObjectResponse put(String userToken, PutObjectRequest request);

DeleteObjectResponse delete(DeleteObjectRequest request);
DeleteObjectResponse delete(String userToken, DeleteObjectRequest request);

ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request);
}
5 changes: 4 additions & 1 deletion app/src/main/java/org/vss/api/AbstractVssApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,16 +7,19 @@
import org.vss.ErrorCode;
import org.vss.ErrorResponse;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.exception.AuthException;
import org.vss.exception.ConflictException;
import org.vss.exception.NoSuchKeyException;

public abstract class AbstractVssApi {
final KVStore kvStore;
final Authorizer authorizer;

@Inject
public AbstractVssApi(KVStore kvStore) {
public AbstractVssApi(KVStore kvStore, Authorizer authorizer) {
this.kvStore = kvStore;
this.authorizer = authorizer;
}

Response toResponse(GeneratedMessageV3 protoResponse) {
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/DeleteObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,27 +4,32 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.DeleteObjectRequest;
import org.vss.DeleteObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.DELETE_OBJECT)
@Slf4j
public class DeleteObjectApi extends AbstractVssApi {
@Inject
public DeleteObjectApi(KVStore kvstore) {
super(kvstore);
public DeleteObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
DeleteObjectResponse response = kvStore.delete(request);
DeleteObjectResponse response = kvStore.delete(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in DeleteObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/GetObjectApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.GetObjectRequest;
import org.vss.GetObjectResponse;
import org.vss.KVStore;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.GET_OBJECT)
@Slf4j
public class GetObjectApi extends AbstractVssApi {

@Inject
public GetObjectApi(KVStore kvstore) {
super(kvstore);
public GetObjectApi(KVStore kvstore, Authorizer authorizer) {
super(kvstore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
GetObjectRequest request = GetObjectRequest.parseFrom(payload);
GetObjectResponse response = kvStore.get(request);
GetObjectResponse response = kvStore.get(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in GetObjectApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/ListKeyVersionsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.ListKeyVersionsRequest;
import org.vss.ListKeyVersionsResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.LIST_KEY_VERSIONS)
@Slf4j
public class ListKeyVersionsApi extends AbstractVssApi {

@Inject
public ListKeyVersionsApi(KVStore kvStore) {
super(kvStore);
public ListKeyVersionsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload);
ListKeyVersionsResponse response = kvStore.listKeyVersions(request);
ListKeyVersionsResponse response = kvStore.listKeyVersions(authResponse.getUserToken(), request);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in ListKeyVersionsApi: ", e);
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/org/vss/api/PutObjectsApi.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,28 +4,33 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.vss.KVStore;
import org.vss.PutObjectRequest;
import org.vss.PutObjectResponse;
import org.vss.auth.AuthResponse;
import org.vss.auth.Authorizer;

@Path(VssApiEndpoint.PUT_OBJECTS)
@Slf4j
public class PutObjectsApi extends AbstractVssApi {

@Inject
public PutObjectsApi(KVStore kvStore) {
super(kvStore);
public PutObjectsApi(KVStore kvStore, Authorizer authorizer) {
super(kvStore, authorizer);
}

@POST
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response execute(byte[] payload) {
public Response execute(byte[] payload, @Context HttpHeaders headers) {
try {
AuthResponse authResponse = authorizer.verify(headers);
PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload);
PutObjectResponse response = kvStore.put(putObjectRequest);
PutObjectResponse response = kvStore.put(authResponse.getUserToken(), putObjectRequest);
return toResponse(response);
} catch (Exception e) {
log.error("Exception in PutObjectsApi: ", e);
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/vss/auth/AuthResponse.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
package org.vss.auth;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
private String userToken;
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/vss/auth/Authorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// Interface for authorizer that is run before every request.
public interface Authorizer {
AuthResponse verify(HttpHeaders headers) throws AuthException;
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/vss/auth/NoopAuthorizer.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package org.vss.auth;

import jakarta.ws.rs.core.HttpHeaders;
import org.vss.exception.AuthException;

// A no-operation authorizer, that lets any user-request go through.
public class NoopAuthorizer implements Authorizer {
private static String UNAUTHENTICATED_USER = "unauth-user";

@Override
public AuthResponse verify(HttpHeaders headers) throws AuthException {
return new AuthResponse(UNAUTHENTICATED_USER);
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/vss/guice/BaseModule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,10 @@
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.tools.StringUtils;
import org.vss.KVStore;
import org.vss.auth.Authorizer;
import org.vss.auth.NoopAuthorizer;
import org.vss.impl.postgres.PostgresBackendImpl;

public class BaseModule extends AbstractModule {
Expand All@@ -20,6 +23,9 @@ public class BaseModule extends AbstractModule {
protected void configure() {
// Provide PostgresBackend as default implementation for KVStore.
bind(KVStore.class).to(PostgresBackendImpl.class).in(Singleton.class);

// Default to Noop Authorizer.
bind(Authorizer.class).to(NoopAuthorizer.class).in(Singleton.class);
}

@Provides
Expand Down
52 changes: 29 additions & 23 deletions app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,11 +48,12 @@ public PostgresBackendImpl(DSLContext context) {
}

@Override
public GetObjectResponse get(GetObjectRequest request) {
public GetObjectResponse get(String userToken, GetObjectRequest request) {

VssDbRecord vssDbRecord = context.selectFrom(VSS_DB)
.where(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey())))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(request.getStoreId())
.and(VSS_DB.KEY.eq(request.getKey()))))
.fetchOne();

final KeyValue keyValue;
Expand All@@ -77,18 +78,18 @@ public GetObjectResponse get(GetObjectRequest request) {
}

@Override
public PutObjectResponse put(PutObjectRequest request) {
public PutObjectResponse put(String userToken, PutObjectRequest request) {

String storeId = request.getStoreId();

List<VssDbRecord> vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

List<VssDbRecord> vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream()
.map(kv -> buildVssRecord(storeId, kv)).toList());
.map(kv -> buildVssRecord(userToken, storeId, kv)).toList());

if (request.hasGlobalVersion()) {
VssDbRecord globalVersionRecord = buildVssRecord(storeId,
VssDbRecord globalVersionRecord = buildVssRecord(userToken, storeId,
KeyValue.newBuilder()
.setKey(GLOBAL_VERSION_KEY)
.setVersion(request.getGlobalVersion())
Expand DownExpand Up@@ -130,15 +131,17 @@ private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private static DeleteConditionStep<VssDbRecord> buildNonConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey())));
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))));
}

private static DeleteConditionStep<VssDbRecord> buildConditionalDeleteQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
return dsl.deleteFrom(VSS_DB).where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
Expand All@@ -153,9 +156,9 @@ private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {

private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onConflict(VSS_DB.STORE_ID, VSS_DB.KEY)
.onConflict(VSS_DB.USER_TOKEN, VSS_DB.STORE_ID, VSS_DB.KEY)
.doUpdate()
.set(VSS_DB.VALUE, vssRecord.getValue())
.set(VSS_DB.VERSION, 1L)
Expand All@@ -165,7 +168,7 @@ private Query buildNonConditionalUpsertRecordQuery(DSLContext dsl, VssDbRecord v
private Insert<VssDbRecord> buildConditionalInsertRecordQuery(DSLContext dsl,
VssDbRecord vssRecord) {
return dsl.insertInto(VSS_DB)
.values(vssRecord.getStoreId(), vssRecord.getKey(),
.values(vssRecord.getUserToken(), vssRecord.getStoreId(), vssRecord.getKey(),
vssRecord.getValue(), 1, vssRecord.getCreatedAt(), vssRecord.getLastUpdatedAt())
.onDuplicateKeyIgnore();
}
Expand All@@ -175,14 +178,16 @@ private Update<VssDbRecord> buildConditionalUpdateRecordQuery(DSLContext dsl, Vs
.set(Map.of(VSS_DB.VALUE, vssRecord.getValue(),
VSS_DB.VERSION, vssRecord.getVersion() + 1,
VSS_DB.LAST_UPDATED_AT, vssRecord.getLastUpdatedAt()))
.where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.where(VSS_DB.USER_TOKEN.eq(vssRecord.getUserToken())
.and(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
.and(VSS_DB.VERSION.eq(vssRecord.getVersion()))));
}

private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
private VssDbRecord buildVssRecord(String userToken, String storeId, KeyValue kv) {
OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS);
return new VssDbRecord()
.setUserToken(userToken)
.setStoreId(storeId)
.setKey(kv.getKey())
.setValue(kv.getValue().toByteArray())
Expand All@@ -192,9 +197,9 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
}

@Override
public DeleteObjectResponse delete(DeleteObjectRequest request) {
public DeleteObjectResponse delete(String userToken, DeleteObjectRequest request) {
String storeId = request.getStoreId();
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
VssDbRecord vssDbRecord = buildVssRecord(userToken, storeId, request.getKeyValue());

context.transaction((ctx) -> {
DSLContext dsl = ctx.dsl();
Expand All@@ -206,7 +211,7 @@ public DeleteObjectResponse delete(DeleteObjectRequest request) {
}

@Override
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
public ListKeyVersionsResponse listKeyVersions(String userToken, ListKeyVersionsRequest request) {
String storeId = request.getStoreId();
String keyPrefix = request.getKeyPrefix();
String pageToken = request.getPageToken();
Expand All@@ -221,12 +226,13 @@ public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
.setStoreId(storeId)
.setKey(GLOBAL_VERSION_KEY)
.build();
globalVersion = get(getGlobalVersionRequest).getValue().getVersion();
globalVersion = get(userToken, getGlobalVersionRequest).getValue().getVersion();
}

List<VssDbRecord> vssDbRecords = context.select(VSS_DB.KEY, VSS_DB.VERSION).from(VSS_DB)
.where(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix)))
.where(VSS_DB.USER_TOKEN.eq(userToken)
.and(VSS_DB.STORE_ID.eq(storeId)
.and(VSS_DB.KEY.startsWith(keyPrefix))))
.orderBy(VSS_DB.KEY)
.seek(pageToken)
.limit(Math.min(pageSize, LIST_KEY_VERSIONS_MAX_PAGE_SIZE))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
CREATE TABLE vss_db (
user_token character varying(120) NOT NULL CHECK (user_token <> ''),
store_id character varying(120) NOT NULL CHECK (store_id <> ''),
key character varying(600) NOT NULL,
value bytea NULL,
version bigint NOT NULL,
created_at TIMESTAMP WITH TIME ZONE,
last_updated_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (store_id, key)
PRIMARY KEY (user_token, store_id, key)
);
Loading