Skip to content

Commit

Permalink
Use random token for claiming scratches
Browse files Browse the repository at this point in the history
  • Loading branch information
encounter committed Jan 17, 2024
1 parent 5369f81 commit 2d36188
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 4 deletions.
20 changes: 20 additions & 0 deletions backend/coreapp/migrations/0050_scratch_claim_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.0 on 2024-01-17 00:43

import coreapp.models.scratch
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("coreapp", "0048_remove_scratch_project_function_and_more"),
]

operations = [
migrations.AddField(
model_name="scratch",
name="claim_token",
field=models.CharField(
default=coreapp.models.scratch.gen_claim_token, max_length=64, null=True
),
),
]
5 changes: 5 additions & 0 deletions backend/coreapp/models/scratch.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ def gen_scratch_id() -> str:
return ret


def gen_claim_token() -> str:
return get_random_string(length=32)


class Asm(models.Model):
hash = models.CharField(max_length=64, primary_key=True)
data = models.TextField()
Expand Down Expand Up @@ -106,6 +110,7 @@ class Scratch(models.Model):
libraries = LibrariesField(default=list)
parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL)
owner = models.ForeignKey(Profile, null=True, blank=True, on_delete=models.SET_NULL)
claim_token = models.CharField(max_length=64, null=True, default=gen_claim_token)

class Meta:
ordering = ["-creation_time"]
Expand Down
5 changes: 5 additions & 0 deletions backend/coreapp/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ class Meta:
]


# On initial creation, include the "claim_token" field.
class ClaimableScratchSerializer(ScratchSerializer):
claim_token = serializers.CharField(read_only=True)


class ProjectSerializer(JSONFormSerializer, serializers.ModelSerializer[Project]):
slug = serializers.SlugField()

Expand Down
11 changes: 9 additions & 2 deletions backend/coreapp/views/scratch.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from ..models.scratch import Asm, Assembly, Scratch
from ..platforms import Platform
from ..serializers import (
ClaimableScratchSerializer,
ScratchCreateSerializer,
ScratchSerializer,
TerseScratchSerializer,
Expand Down Expand Up @@ -318,7 +319,7 @@ def create(self, request: Any, *args: Any, **kwargs: Any) -> Response:
scratch = create_scratch(request.data)

return Response(
ScratchSerializer(scratch, context={"request": request}).data,
ClaimableScratchSerializer(scratch, context={"request": request}).data,
status=status.HTTP_201_CREATED,
)

Expand Down Expand Up @@ -426,15 +427,20 @@ def decompile(self, request: Request, pk: str) -> Response:
@action(detail=True, methods=["POST"])
def claim(self, request: Request, pk: str) -> Response:
scratch: Scratch = self.get_object()
token = request.data.get("token")

if not scratch.is_claimable():
return Response({"success": False})

if scratch.claim_token and scratch.claim_token != token:
return Response({"success": False})

profile = request.profile

logger.debug(f"Granting ownership of scratch {scratch} to {profile}")

scratch.owner = profile
scratch.claim_token = None
scratch.save()

return Response({"success": True})
Expand All @@ -451,6 +457,7 @@ def fork(self, request: Request, pk: str) -> Response:

parent_data = ScratchSerializer(parent, context={"request": request}).data
fork_data = {**parent_data, **request_data}
fork_data.pop("claim_token") # ensure a new token is generated

ser = ScratchSerializer(data=fork_data, context={"request": request})
ser.is_valid(raise_exception=True)
Expand All @@ -466,7 +473,7 @@ def fork(self, request: Request, pk: str) -> Response:
compile_scratch_update_score(new_scratch)

return Response(
ScratchSerializer(new_scratch, context={"request": request}).data,
ClaimableScratchSerializer(new_scratch, context={"request": request}).data,
status=status.HTTP_201_CREATED,
)

Expand Down
32 changes: 32 additions & 0 deletions frontend/src/app/scratch/[slug]/claim/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client"

import { useEffect, useRef } from "react"

import { useRouter } from "next/navigation"

import LoadingSkeleton from "@/app/scratch/[slug]/loading"
import { post } from "@/lib/api/request"

export default function Page({ params, searchParams }: {
params: { slug: string }
searchParams: { token: string }
}) {
const router = useRouter()

// The POST request must happen on the client so
// that the Django session cookie is present.
const effectRan = useRef(false)
useEffect(() => {
if (!effectRan.current) {
post(`/scratch/${params.slug}/claim`, { token: searchParams.token })
.catch(err => console.error(err))
.finally(() => router.push(`/scratch/${params.slug}`))
}

return () => {
effectRan.current = true
}
}, [params.slug, router, searchParams.token])

return <LoadingSkeleton/>
}
2 changes: 1 addition & 1 deletion frontend/src/components/Scratch/ScratchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function Actions({ isCompiling, compile, scratch, setScratch, setDecompilationTa
const forkScratch = api.useForkScratchAndGo(scratch)
const [fuzzySaveAction, fuzzySaveScratch] = useFuzzySaveCallback(scratch, setScratch)
const [isSaving, setIsSaving] = useState(false)
const canSave = scratch.owner && userIsYou(scratch.owner)
const canSave = (scratch.owner && userIsYou(scratch.owner)) || fuzzySaveAction === FuzzySaveAction.CLAIM

const fuzzyShortcut = useShortcut([SpecialKey.CTRL_COMMAND, "S"], async () => {
setIsSaving(true)
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ export function useSaveScratch(localScratch: Scratch): () => Promise<Scratch> {
}

export async function claimScratch(scratch: Scratch): Promise<void> {
const { success } = await post(`${scratchUrl(scratch)}/claim`, {})
const { success } = await post(`${scratchUrl(scratch)}/claim`, {
token: scratch.claim_token,
})
const user = await get("/user")

if (!success)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface Scratch extends TerseScratch {
source_code: string
context: string
diff_label: string
claim_token: string | null
}

export interface Project {
Expand Down

0 comments on commit 2d36188

Please sign in to comment.