@@ -68,6 +69,7 @@ export default function ChallengeListPage() {
| Title |
Type |
Difficulty |
+ Author |
@@ -93,6 +95,9 @@ export default function ChallengeListPage() {
{c.difficulty}
+
+ {c.createdBy ?? '—'}
+ |
))}
diff --git a/frontend/src/pages/ChallengePage.jsx b/frontend/src/pages/ChallengePage.jsx
index 4e4d825..3b6706d 100644
--- a/frontend/src/pages/ChallengePage.jsx
+++ b/frontend/src/pages/ChallengePage.jsx
@@ -1,8 +1,9 @@
import { useEffect, useRef, useState } from 'react'
-import { useParams, Link } from 'react-router-dom'
+import { useParams, Link, useNavigate } from 'react-router-dom'
import Editor from '@monaco-editor/react'
import api from '../api/axios'
import NavBar from '../components/NavBar'
+import { useAuth } from '../context/AuthContext'
function sortKeys(val) {
if (Array.isArray(val)) return val.map(sortKeys)
@@ -59,6 +60,8 @@ function compareResult(challengeType, actual, expected) {
export default function ChallengePage() {
const { id } = useParams()
+ const navigate = useNavigate()
+ const { user } = useAuth()
const [challenge, setChallenge] = useState(null)
const [error, setError] = useState(null)
const [activeSchema, setActiveSchema] = useState(0)
@@ -166,11 +169,52 @@ export default function ChallengePage() {
-
- {challenge.title}
-
+
+
+ {challenge.title}
+
+ {(user?.username === challenge.createdBy || user?.role === 'ADMIN') && (
+
+
+ Edit
+
+
+
+ )}
+
+ {challenge.createdBy && (
+
by {challenge.createdBy}
+ )}
-
+
{challenge.description}
diff --git a/frontend/src/pages/EditChallengePage.jsx b/frontend/src/pages/EditChallengePage.jsx
new file mode 100644
index 0000000..1a50c81
--- /dev/null
+++ b/frontend/src/pages/EditChallengePage.jsx
@@ -0,0 +1,307 @@
+import { useEffect, useState } from 'react'
+import { useNavigate, useParams, Link } from 'react-router-dom'
+import api from '../api/axios'
+
+const inputStyle = {
+ width: '100%',
+ background: '#2d3748',
+ border: '1px solid #4a5568',
+ borderRadius: 6,
+ color: '#f7fafc',
+ padding: '8px 12px',
+ fontSize: 14,
+ boxSizing: 'border-box',
+}
+
+const monoStyle = { ...inputStyle, fontFamily: 'monospace', fontSize: 13, minHeight: 120, resize: 'vertical' }
+
+const labelStyle = { display: 'block', color: '#a0aec0', fontSize: 13, marginBottom: 6 }
+
+const fieldStyle = { marginBottom: 20 }
+
+const sectionStyle = {
+ border: '1px solid #2d3748',
+ borderRadius: 8,
+ padding: '16px 16px 8px',
+ marginBottom: 12,
+ background: '#1a202c',
+}
+
+const addBtnStyle = {
+ background: 'none',
+ border: '1px dashed #4a5568',
+ color: '#a0aec0',
+ borderRadius: 6,
+ padding: '6px 16px',
+ fontSize: 13,
+ cursor: 'pointer',
+ width: '100%',
+ marginBottom: 20,
+}
+
+const removeBtnStyle = {
+ background: 'none',
+ border: 'none',
+ color: '#fc8181',
+ fontSize: 12,
+ cursor: 'pointer',
+ padding: '0 4px',
+}
+
+function emptySchema() {
+ return { label: '', format: 'JSON_SCHEMA', content: '' }
+}
+
+function emptyTestCase() {
+ return { description: '', inputJson: '', expectedJson: '', hidden: false }
+}
+
+export default function EditChallengePage() {
+ const { id } = useParams()
+ const navigate = useNavigate()
+ const [loading, setLoading] = useState(true)
+ const [form, setForm] = useState({
+ title: '',
+ description: '',
+ difficulty: 'EASY',
+ type: 'SCHEMA_MATCHING',
+ starterCode: '',
+ harnessCode: '',
+ })
+ const [schemas, setSchemas] = useState([emptySchema()])
+ const [testCases, setTestCases] = useState([emptyTestCase()])
+ const [error, setError] = useState(null)
+ const [submitting, setSubmitting] = useState(false)
+
+ useEffect(() => {
+ api.get(`/challenges/${id}`).then(res => {
+ const c = res.data
+ setForm({
+ title: c.title ?? '',
+ description: c.description ?? '',
+ difficulty: c.difficulty ?? 'EASY',
+ type: c.type ?? 'SCHEMA_MATCHING',
+ starterCode: c.starterCode ?? '',
+ harnessCode: c.harnessCode ?? '',
+ })
+ setSchemas(c.schemas?.length ? c.schemas.map(s => ({
+ label: s.label ?? '',
+ format: s.format ?? 'JSON_SCHEMA',
+ content: s.content ?? '',
+ })) : [emptySchema()])
+ setTestCases(c.testCases?.length ? c.testCases.map(t => ({
+ description: t.description ?? '',
+ inputJson: t.inputJson ?? '',
+ expectedJson: t.expectedJson ?? '',
+ hidden: t.hidden ?? false,
+ })) : [emptyTestCase()])
+ setLoading(false)
+ }).catch(() => {
+ setError('Failed to load challenge.')
+ setLoading(false)
+ })
+ }, [id])
+
+ function setField(field) {
+ return e => setForm(f => ({ ...f, [field]: e.target.value }))
+ }
+
+ function updateSchema(i, field, value) {
+ setSchemas(prev => prev.map((s, idx) => idx === i ? { ...s, [field]: value } : s))
+ }
+
+ function updateTestCase(i, field, value) {
+ setTestCases(prev => prev.map((t, idx) => idx === i ? { ...t, [field]: value } : t))
+ }
+
+ function addSchema() { setSchemas(prev => [...prev, emptySchema()]) }
+ function removeSchema(i) { setSchemas(prev => prev.filter((_, idx) => idx !== i)) }
+
+ function addTestCase() { setTestCases(prev => [...prev, emptyTestCase()]) }
+ function removeTestCase(i) { setTestCases(prev => prev.filter((_, idx) => idx !== i)) }
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ if (!form.title.trim()) { setError('Title is required.'); return }
+ setError(null)
+ setSubmitting(true)
+ try {
+ const payload = {
+ ...form,
+ schemas: schemas.filter(s => s.label.trim() || s.content.trim()),
+ testCases: testCases.filter(t => t.inputJson.trim() || t.expectedJson.trim()),
+ }
+ await api.put(`/challenges/${id}`, payload)
+ navigate(`/challenges/${id}`)
+ } catch {
+ setError('Failed to save changes. Please try again.')
+ setSubmitting(false)
+ }
+ }
+
+ if (loading) return
Loading…
+
+ return (
+
+
+
Edit Challenge
+ Cancel
+
+
+ {error &&
{error}
}
+
+
+
+ )
+}
diff --git a/src/main/java/no/hvl/schemalab/DataSeeder.java b/src/main/java/no/hvl/schemalab/DataSeeder.java
index 13b5d55..d153dd6 100644
--- a/src/main/java/no/hvl/schemalab/DataSeeder.java
+++ b/src/main/java/no/hvl/schemalab/DataSeeder.java
@@ -95,7 +95,7 @@ private void seedDevUser() {
AppUser dev = new AppUser();
dev.setUsername("dev");
dev.setPasswordHash(passwordEncoder.encode("dev"));
- dev.setRole("USER");
+ dev.setRole("ADMIN");
appUserRepository.save(dev);
}
}
diff --git a/src/main/java/no/hvl/schemalab/config/SecurityConfig.java b/src/main/java/no/hvl/schemalab/config/SecurityConfig.java
index 36f6f08..17c555e 100644
--- a/src/main/java/no/hvl/schemalab/config/SecurityConfig.java
+++ b/src/main/java/no/hvl/schemalab/config/SecurityConfig.java
@@ -44,6 +44,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/challenges/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/challenges").authenticated()
+ .requestMatchers(HttpMethod.PUT, "/api/challenges/**").authenticated()
+ .requestMatchers(HttpMethod.DELETE, "/api/challenges/**").authenticated()
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/api/submissions/**").authenticated()
diff --git a/src/main/java/no/hvl/schemalab/controller/ChallengeController.java b/src/main/java/no/hvl/schemalab/controller/ChallengeController.java
index d0d141a..5762983 100644
--- a/src/main/java/no/hvl/schemalab/controller/ChallengeController.java
+++ b/src/main/java/no/hvl/schemalab/controller/ChallengeController.java
@@ -5,9 +5,11 @@
import no.hvl.schemalab.model.TestCase;
import no.hvl.schemalab.repository.ChallengeRepository;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -30,6 +32,7 @@ public List