diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7b016a8..d53ecaf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,4 @@
{
- "java.compile.nullAnalysis.mode": "automatic"
+ "java.compile.nullAnalysis.mode": "automatic",
+ "java.configuration.updateBuildConfiguration": "automatic"
}
\ No newline at end of file
diff --git a/bin/main/application.properties b/bin/main/application.properties
new file mode 100644
index 0000000..c0cbefd
--- /dev/null
+++ b/bin/main/application.properties
@@ -0,0 +1,7 @@
+spring.application.name=schemalab
+spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/schemalab}
+spring.datasource.username=${SPRING_DATASOURCE_USERNAME:schemalab}
+spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:schemalab}
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.jpa.hibernate.ddl-auto=update
+server.port=8080
diff --git a/build.gradle.kts b/build.gradle.kts
index e46893f..6d29a5e 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -18,19 +18,18 @@ repositories {
}
dependencies {
- implementation("org.springframework.boot:spring-boot-h2console")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
+ runtimeOnly("org.postgresql:postgresql")
compileOnly("org.projectlombok:lombok:1.18.46")
annotationProcessor("org.projectlombok:lombok:1.18.46")
-
+
testCompileOnly("org.projectlombok:lombok:1.18.46")
testAnnotationProcessor("org.projectlombok:lombok:1.18.46")
-
- runtimeOnly("com.h2database:h2")
+ testRuntimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test")
testImplementation("org.springframework.boot:spring-boot-starter-security-test")
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
diff --git a/docker-compose.yml b/docker-compose.yml
index 0db4bc3..13a4ac0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,8 +1,31 @@
services:
+ postgres:
+ image: postgres:17
+ environment:
+ POSTGRES_DB: schemalab
+ POSTGRES_USER: schemalab
+ POSTGRES_PASSWORD: schemalab
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U schemalab"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
backend:
build: .
ports:
- "8080:8080"
+ environment:
+ SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/schemalab
+ SPRING_DATASOURCE_USERNAME: schemalab
+ SPRING_DATASOURCE_PASSWORD: schemalab
+ depends_on:
+ postgres:
+ condition: service_healthy
runner:
build: ./runner
@@ -14,3 +37,6 @@ services:
depends_on:
- backend
- runner
+
+volumes:
+ postgres_data:
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index a20f439..eaf7c18 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,17 +1,35 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
+import { AuthProvider, useAuth } from './context/AuthContext'
import ChallengeListPage from './pages/ChallengeListPage'
import ChallengePage from './pages/ChallengePage'
import AboutPage from './pages/AboutPage'
+import LoginPage from './pages/LoginPage'
+import RegisterPage from './pages/RegisterPage'
+
+function ProtectedRoute({ children }) {
+ const { user } = useAuth()
+ return user ? children :
+}
+
+function AppRoutes() {
+ return (
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ )
+}
export default function App() {
return (
-
- } />
- } />
- } />
- } />
-
+
+
+
)
}
diff --git a/frontend/src/api/axios.js b/frontend/src/api/axios.js
index 2bd83d1..9ff5e8a 100644
--- a/frontend/src/api/axios.js
+++ b/frontend/src/api/axios.js
@@ -1,10 +1,13 @@
import axios from 'axios'
-const api = axios.create({
- baseURL: '/api',
- headers: {
- Authorization: 'Basic ' + btoa('dev:dev'),
- },
+const api = axios.create({ baseURL: '/api' })
+
+api.interceptors.request.use(config => {
+ const credentials = localStorage.getItem('auth_credentials')
+ if (credentials) {
+ config.headers.Authorization = `Basic ${credentials}`
+ }
+ return config
})
export default api
diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx
new file mode 100644
index 0000000..0e5ff30
--- /dev/null
+++ b/frontend/src/components/NavBar.jsx
@@ -0,0 +1,49 @@
+import { Link, useNavigate } from 'react-router-dom'
+import { useAuth } from '../context/AuthContext'
+
+export default function NavBar() {
+ const { user, logout } = useAuth()
+ const navigate = useNavigate()
+
+ function handleLogout() {
+ logout()
+ navigate('/login')
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx
new file mode 100644
index 0000000..1df766c
--- /dev/null
+++ b/frontend/src/context/AuthContext.jsx
@@ -0,0 +1,43 @@
+import { createContext, useContext, useState } from 'react'
+import api from '../api/axios'
+
+const AuthContext = createContext(null)
+
+export function AuthProvider({ children }) {
+ const [user, setUser] = useState(() => {
+ const stored = localStorage.getItem('auth_user')
+ return stored ? JSON.parse(stored) : null
+ })
+
+ async function login(username, password) {
+ const credentials = btoa(`${username}:${password}`)
+ const res = await api.post('/auth/login', null, {
+ headers: { Authorization: `Basic ${credentials}` },
+ })
+ localStorage.setItem('auth_credentials', credentials)
+ localStorage.setItem('auth_user', JSON.stringify(res.data))
+ setUser(res.data)
+ return res.data
+ }
+
+ async function register(username, password) {
+ await api.post('/auth/register', { username, password })
+ return login(username, password)
+ }
+
+ function logout() {
+ localStorage.removeItem('auth_credentials')
+ localStorage.removeItem('auth_user')
+ setUser(null)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useAuth() {
+ return useContext(AuthContext)
+}
diff --git a/frontend/src/pages/AboutPage.jsx b/frontend/src/pages/AboutPage.jsx
index d177870..1dfa447 100644
--- a/frontend/src/pages/AboutPage.jsx
+++ b/frontend/src/pages/AboutPage.jsx
@@ -1,6 +1,10 @@
+import NavBar from '../components/NavBar'
+
export default function AboutPage() {
return (
-
+ <>
+
+
About SchemaLab
@@ -85,6 +89,7 @@ export default function AboutPage() {
))}
-
+
+ >
)
}
diff --git a/frontend/src/pages/ChallengeListPage.jsx b/frontend/src/pages/ChallengeListPage.jsx
index e0e41f5..ece5f5b 100644
--- a/frontend/src/pages/ChallengeListPage.jsx
+++ b/frontend/src/pages/ChallengeListPage.jsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import api from '../api/axios'
+import NavBar from '../components/NavBar'
const DIFFICULTY_COLOR = {
EASY: '#68d391',
@@ -24,52 +25,49 @@ export default function ChallengeListPage() {
}, [])
return (
-
-
-
- SchemaLab
-
- About
-
-
- Practice schema matching and schema versioning challenges.
-
+ <>
+
+
+
+ Practice schema matching and schema versioning challenges.
+
- {error &&
{error}
}
+ {error &&
{error}
}
-
-
-
- | # |
- Title |
- Type |
- Difficulty |
-
-
-
- {challenges.map((c, i) => (
-
- | {i + 1} |
-
-
- {c.title}
-
- |
-
- {TYPE_LABEL[c.type] ?? c.type}
- |
-
-
- {c.difficulty}
-
- |
+
+
+
+ | # |
+ Title |
+ Type |
+ Difficulty |
- ))}
-
-
-
+
+
+ {challenges.map((c, i) => (
+
+ | {i + 1} |
+
+
+ {c.title}
+
+ |
+
+ {TYPE_LABEL[c.type] ?? c.type}
+ |
+
+
+ {c.difficulty}
+
+ |
+
+ ))}
+
+
+
+ >
)
}
diff --git a/frontend/src/pages/ChallengePage.jsx b/frontend/src/pages/ChallengePage.jsx
index 846edbc..4e4d825 100644
--- a/frontend/src/pages/ChallengePage.jsx
+++ b/frontend/src/pages/ChallengePage.jsx
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { useParams, Link } from 'react-router-dom'
import Editor from '@monaco-editor/react'
import api from '../api/axios'
+import NavBar from '../components/NavBar'
function sortKeys(val) {
if (Array.isArray(val)) return val.map(sortKeys)
@@ -144,7 +145,9 @@ export default function ChallengePage() {
if (!challenge) return
Loading…
return (
-
+
)
}
diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx
new file mode 100644
index 0000000..8b4ea8f
--- /dev/null
+++ b/frontend/src/pages/LoginPage.jsx
@@ -0,0 +1,108 @@
+import { useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import { useAuth } from '../context/AuthContext'
+
+export default function LoginPage() {
+ const { login } = useAuth()
+ const navigate = useNavigate()
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const [error, setError] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ setError(null)
+ setLoading(true)
+ try {
+ await login(username, password)
+ navigate('/challenges')
+ } catch {
+ setError('Invalid username or password.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Sign in
+
+
+ Don't have an account?{' '}
+ Register
+
+
+
+
+
+ )
+}
+
+const inputStyle = {
+ width: '100%',
+ padding: '8px 12px',
+ background: '#0f1117',
+ border: '1px solid #2d3748',
+ borderRadius: 6,
+ color: '#f7fafc',
+ fontSize: 14,
+ outline: 'none',
+}
diff --git a/frontend/src/pages/RegisterPage.jsx b/frontend/src/pages/RegisterPage.jsx
new file mode 100644
index 0000000..2c671d6
--- /dev/null
+++ b/frontend/src/pages/RegisterPage.jsx
@@ -0,0 +1,113 @@
+import { useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import { useAuth } from '../context/AuthContext'
+
+export default function RegisterPage() {
+ const { register } = useAuth()
+ const navigate = useNavigate()
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const [error, setError] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ setError(null)
+ setLoading(true)
+ try {
+ await register(username, password)
+ navigate('/challenges')
+ } catch (err) {
+ const status = err?.response?.status
+ if (status === 500 || status === 409) {
+ setError('Username already taken.')
+ } else {
+ setError('Registration failed. Please try again.')
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Create account
+
+
+ Already have an account?{' '}
+ Sign in
+
+
+
+
+
+ )
+}
+
+const inputStyle = {
+ width: '100%',
+ padding: '8px 12px',
+ background: '#0f1117',
+ border: '1px solid #2d3748',
+ borderRadius: 6,
+ color: '#f7fafc',
+ fontSize: 14,
+ outline: 'none',
+}
diff --git a/src/main/java/no/hvl/schemalab/model/AppUser.java b/src/main/java/no/hvl/schemalab/model/AppUser.java
index 3e177af..b95f5b7 100644
--- a/src/main/java/no/hvl/schemalab/model/AppUser.java
+++ b/src/main/java/no/hvl/schemalab/model/AppUser.java
@@ -1,5 +1,6 @@
package no.hvl.schemalab.model;
+import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
@Entity
@@ -14,6 +15,7 @@ public class AppUser {
private String username;
@Column(nullable = false)
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String passwordHash;
private String role;
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 5a0c508..c0cbefd 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,7 +1,7 @@
spring.application.name=schemalab
-# ci test
-spring.datasource.url=jdbc:h2:mem:schemalab
-spring.datasource.driver-class-name=org.h2.Driver
-spring.jpa.hibernate.ddl-auto=create-drop
-spring.h2.console.enabled=true
+spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/schemalab}
+spring.datasource.username=${SPRING_DATASOURCE_USERNAME:schemalab}
+spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:schemalab}
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.jpa.hibernate.ddl-auto=update
server.port=8080
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 0000000..ef91bba
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,3 @@
+spring.datasource.url=jdbc:h2:mem:testdb
+spring.datasource.driver-class-name=org.h2.Driver
+spring.jpa.hibernate.ddl-auto=create-drop