add session table
This commit is contained in:
parent
25ab7a4f58
commit
ca62140e6f
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -1,13 +1,38 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
//
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { auth, clearSession } from '@/auth'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const signedIn = computed(() => Boolean(auth.token))
|
||||||
|
|
||||||
|
async function signOut () {
|
||||||
|
clearSession()
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div aria-hidden="true" class="page-glow" />
|
<div aria-hidden="true" class="page-glow" />
|
||||||
|
|
||||||
<main class="min-h-screen p-5 flex items-center">
|
<div v-if="signedIn" class="app-shell">
|
||||||
<router-view />
|
<aside class="drawer">
|
||||||
</main>
|
<div class="brand"><span class="brand-mark">B</span><span>Bolts</span></div>
|
||||||
|
|
||||||
|
<nav aria-label="Main navigation">
|
||||||
|
<router-link v-if="auth.role === 'admin'" to="/users">User Management</router-link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="account">
|
||||||
|
<div><strong>{{ auth.username }}</strong><span>{{ auth.role }}</span></div>
|
||||||
|
<button type="button" @click="signOut">Sign out</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="content"><router-view /></main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main v-else class="login-main"><router-view /></main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -25,6 +50,24 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell { display: flex; min-height: 100vh; }
|
||||||
|
.drawer { position: fixed; inset: 0 auto 0 0; display: flex; width: 16rem; padding: 1.5rem; flex-direction: column; border-right: 1px solid var(--v0-divider); background: var(--v0-surface); }
|
||||||
|
.brand { display: flex; align-items: center; gap: 0.7rem; margin-bottom: 2.5rem; font-size: 1.15rem; font-weight: 700; }
|
||||||
|
.brand-mark { display: grid; width: 2.25rem; height: 2.25rem; place-items: center; border-radius: 0.7rem; color: var(--v0-on-primary); background: var(--v0-primary); }
|
||||||
|
nav a { display: block; padding: 0.8rem 0.9rem; border-radius: 0.65rem; color: var(--v0-on-surface-variant); text-decoration: none; }
|
||||||
|
nav a.router-link-active { color: var(--v0-primary); background: color-mix(in srgb, var(--v0-primary) 12%, transparent); font-weight: 700; }
|
||||||
|
.account { display: flex; margin-top: auto; padding-top: 1rem; align-items: center; justify-content: space-between; border-top: 1px solid var(--v0-divider); }
|
||||||
|
.account div { display: flex; flex-direction: column; }
|
||||||
|
.account span { color: var(--v0-on-surface-variant); font-size: 0.75rem; text-transform: capitalize; }
|
||||||
|
.account button { border: 0; color: var(--v0-primary); background: transparent; cursor: pointer; }
|
||||||
|
.content { width: 100%; min-height: 100vh; margin-left: 16rem; padding: 3rem; }
|
||||||
|
.login-main { display: flex; min-height: 100vh; padding: 1.25rem; align-items: center; }
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.drawer { width: 12rem; }
|
||||||
|
.content { margin-left: 12rem; padding: 2rem 1.25rem; }
|
||||||
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input {
|
input {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
|||||||
25
frontend/src/auth.ts
Normal file
25
frontend/src/auth.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
interface StoredSession {
|
||||||
|
username: string
|
||||||
|
role: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedSession = sessionStorage.getItem('bolts-session')
|
||||||
|
|
||||||
|
export const auth = reactive<StoredSession>(
|
||||||
|
savedSession
|
||||||
|
? JSON.parse(savedSession) as StoredSession
|
||||||
|
: { username: '', role: '', token: '' },
|
||||||
|
)
|
||||||
|
|
||||||
|
export function saveSession (session: StoredSession) {
|
||||||
|
Object.assign(auth, session)
|
||||||
|
sessionStorage.setItem('bolts-session', JSON.stringify(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSession () {
|
||||||
|
Object.assign(auth, { username: '', role: '', token: '' })
|
||||||
|
sessionStorage.removeItem('bolts-session')
|
||||||
|
}
|
||||||
@ -1,16 +1,20 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { saveSession } from '@/auth'
|
||||||
|
|
||||||
interface LoginResponse {
|
interface LoginResponse {
|
||||||
username: string
|
username: string
|
||||||
|
role: string
|
||||||
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const authenticatedUser = ref('')
|
|
||||||
|
|
||||||
async function login () {
|
async function login () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@ -33,8 +37,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json() as LoginResponse
|
const result = await response.json() as LoginResponse
|
||||||
authenticatedUser.value = result.username
|
saveSession(result)
|
||||||
password.value = ''
|
password.value = ''
|
||||||
|
await router.push(result.role === 'admin' ? '/users' : '/')
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage.value = 'Unable to reach the server. Please try again.'
|
errorMessage.value = 'Unable to reach the server. Please try again.'
|
||||||
} finally {
|
} finally {
|
||||||
@ -42,12 +47,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset () {
|
|
||||||
authenticatedUser.value = ''
|
|
||||||
username.value = ''
|
|
||||||
password.value = ''
|
|
||||||
errorMessage.value = ''
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -60,27 +59,7 @@
|
|||||||
<span>Bolts</span>
|
<span>Bolts</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="authenticatedUser" class="success-panel" role="status">
|
<form class="login-card" @submit.prevent="login">
|
||||||
<div aria-hidden="true" class="success-icon">
|
|
||||||
✓
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="eyebrow">
|
|
||||||
Signed in
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1>Welcome, {{ authenticatedUser }}</h1>
|
|
||||||
|
|
||||||
<p class="supporting">
|
|
||||||
You’re securely connected to your Bolts workspace.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button class="secondary-button" type="button" @click="reset">
|
|
||||||
Sign out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form v-else class="login-card" @submit.prevent="login">
|
|
||||||
<div class="card-heading">
|
<div class="card-heading">
|
||||||
<p class="eyebrow">
|
<p class="eyebrow">
|
||||||
Welcome back
|
Welcome back
|
||||||
|
|||||||
78
frontend/src/pages/users.vue
Normal file
78
frontend/src/pages/users.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const users = ref<User[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.token || auth.role !== 'admin') {
|
||||||
|
await router.replace('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/users', {
|
||||||
|
headers: { Authorization: `Bearer ${auth.token}` },
|
||||||
|
})
|
||||||
|
if (!response.ok) throw new Error('User request failed')
|
||||||
|
users.value = await response.json() as User[]
|
||||||
|
} catch {
|
||||||
|
errorMessage.value = 'Unable to load users.'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="users-page">
|
||||||
|
<header>
|
||||||
|
<p class="eyebrow">Administration</p>
|
||||||
|
<h1>User Management</h1>
|
||||||
|
<p>View the people who can access this workspace.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="user-card">
|
||||||
|
<p v-if="loading">Loading users…</p>
|
||||||
|
<p v-else-if="errorMessage" role="alert">{{ errorMessage }}</p>
|
||||||
|
|
||||||
|
<table v-else>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Username</th><th>Role</th></tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="user in users" :key="user.id">
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td><span class="role-badge">{{ user.role }}</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.users-page { width: 100%; max-width: 70rem; }
|
||||||
|
header { margin-bottom: 2rem; }
|
||||||
|
.eyebrow { margin: 0 0 0.5rem; color: var(--v0-primary); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
|
||||||
|
h1 { margin: 0; font-size: 2rem; letter-spacing: -0.04em; }
|
||||||
|
header > p:last-child { color: var(--v0-on-surface-variant); }
|
||||||
|
.user-card { overflow: hidden; border: 1px solid var(--v0-divider); border-radius: 1rem; background: var(--v0-surface); }
|
||||||
|
.user-card > p { padding: 1.5rem; }
|
||||||
|
table { width: 100%; border-collapse: collapse; text-align: left; }
|
||||||
|
th, td { padding: 1rem 1.25rem; border-bottom: 1px solid var(--v0-divider); }
|
||||||
|
th { color: var(--v0-on-surface-variant); font-size: 0.75rem; text-transform: uppercase; }
|
||||||
|
tbody tr:last-child td { border-bottom: 0; }
|
||||||
|
.role-badge { padding: 0.25rem 0.6rem; border-radius: 999px; color: var(--v0-primary); background: color-mix(in srgb, var(--v0-primary) 14%, transparent); font-size: 0.8rem; font-weight: 700; }
|
||||||
|
</style>
|
||||||
@ -7,6 +7,7 @@
|
|||||||
// Composables
|
// Composables
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Index from '@/pages/index.vue'
|
import Index from '@/pages/index.vue'
|
||||||
|
import Users from '@/pages/users.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@ -15,6 +16,10 @@ const router = createRouter({
|
|||||||
path: '/',
|
path: '/',
|
||||||
component: Index,
|
component: Index,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
component: Users,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,9 @@ test('logs in with the initialized admin user', async ({ page }) => {
|
|||||||
await page.getByRole('textbox', { name: 'Password', exact: true }).fill('admin')
|
await page.getByRole('textbox', { name: 'Password', exact: true }).fill('admin')
|
||||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Welcome, admin' })).toBeVisible()
|
await expect(page).toHaveURL('/users')
|
||||||
|
await expect(page.getByRole('heading', { name: 'User Management' })).toBeVisible()
|
||||||
|
const adminRow = page.getByRole('row').filter({ hasText: 'admin' })
|
||||||
|
await expect(adminRow).toContainText('admin')
|
||||||
await expect(page.getByRole('button', { name: 'Sign out' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Sign out' })).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': 'http://localhost:7070',
|
'/api': process.env.VITE_API_TARGET ?? 'http://localhost:7070',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import dev.mduchene.bolts.persistence.Database
|
import dev.mduchene.bolts.persistence.Database
|
||||||
import dev.mduchene.bolts.persistence.DatabaseConfig
|
import dev.mduchene.bolts.persistence.DatabaseConfig
|
||||||
import dev.mduchene.bolts.user.LoginService
|
import dev.mduchene.bolts.user.LoginService
|
||||||
|
import dev.mduchene.bolts.user.SessionRepository
|
||||||
import dev.mduchene.bolts.user.UserRepository
|
import dev.mduchene.bolts.user.UserRepository
|
||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
import io.javalin.http.HttpStatus
|
import io.javalin.http.HttpStatus
|
||||||
@ -8,7 +9,8 @@ import io.javalin.http.HttpStatus
|
|||||||
fun main() {
|
fun main() {
|
||||||
val database = Database(DatabaseConfig.fromEnvironment())
|
val database = Database(DatabaseConfig.fromEnvironment())
|
||||||
database.initialize()
|
database.initialize()
|
||||||
val loginService = LoginService(UserRepository(database))
|
val users = UserRepository(database)
|
||||||
|
val loginService = LoginService(users, SessionRepository(database))
|
||||||
loginService.initializeAdmin()
|
loginService.initializeAdmin()
|
||||||
|
|
||||||
Javalin.create { config ->
|
Javalin.create { config ->
|
||||||
@ -18,17 +20,35 @@ fun main() {
|
|||||||
val username = ctx.formParam("username").orEmpty()
|
val username = ctx.formParam("username").orEmpty()
|
||||||
val password = ctx.formParam("password").orEmpty()
|
val password = ctx.formParam("password").orEmpty()
|
||||||
|
|
||||||
if (loginService.authenticate(username, password)) {
|
val session = loginService.authenticate(username, password)
|
||||||
|
if (session != null) {
|
||||||
ctx.contentType("application/json")
|
ctx.contentType("application/json")
|
||||||
.result("""{"username":"${username.toJsonString()}"}""")
|
.result(
|
||||||
|
"""{"username":"${session.user.username.toJsonString()}","role":"${session.user.role.toJsonString()}","token":"${session.token}"}""",
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
ctx.status(HttpStatus.UNAUTHORIZED)
|
ctx.status(HttpStatus.UNAUTHORIZED)
|
||||||
.contentType("application/json")
|
.contentType("application/json")
|
||||||
.result("""{"message":"Invalid username or password"}""")
|
.result("""{"message":"Invalid username or password"}""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
get("/api/users") { ctx ->
|
||||||
|
val token = ctx.header("Authorization")?.removePrefix("Bearer ")
|
||||||
|
val currentUser = loginService.userForToken(token)
|
||||||
|
if (currentUser?.role != "admin") {
|
||||||
|
ctx.status(HttpStatus.FORBIDDEN)
|
||||||
|
.contentType("application/json")
|
||||||
|
.result("""{"message":"Admin access required"}""")
|
||||||
|
return@get
|
||||||
}
|
}
|
||||||
}.start(7070)
|
|
||||||
|
val result = users.findAll().joinToString(prefix = "[", postfix = "]") { user ->
|
||||||
|
"""{"id":${user.id},"username":"${user.username.toJsonString()}","role":"${user.role.toJsonString()}"}"""
|
||||||
|
}
|
||||||
|
ctx.contentType("application/json").result(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start(System.getenv("SERVER_PORT")?.toIntOrNull() ?: 7070)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toJsonString() = buildString {
|
private fun String.toJsonString() = buildString {
|
||||||
|
|||||||
@ -11,6 +11,8 @@ class Database(private val config: DatabaseConfig) {
|
|||||||
getConnection().use { connection ->
|
getConnection().use { connection ->
|
||||||
connection.createStatement().use { statement ->
|
connection.createStatement().use { statement ->
|
||||||
statement.execute(CREATE_USERS_TABLE)
|
statement.execute(CREATE_USERS_TABLE)
|
||||||
|
statement.execute(ADD_USER_ROLE)
|
||||||
|
statement.execute(CREATE_SESSIONS_TABLE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -23,5 +25,18 @@ class Database(private val config: DatabaseConfig) {
|
|||||||
password_hash VARCHAR(255) NOT NULL
|
password_hash VARCHAR(255) NOT NULL
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
const val ADD_USER_ROLE = """
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS role VARCHAR(50) NOT NULL DEFAULT 'user'
|
||||||
|
"""
|
||||||
|
|
||||||
|
const val CREATE_SESSIONS_TABLE = """
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
token VARCHAR(36) PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,37 @@
|
|||||||
package dev.mduchene.bolts.user
|
package dev.mduchene.bolts.user
|
||||||
|
|
||||||
import org.mindrot.jbcrypt.BCrypt
|
import org.mindrot.jbcrypt.BCrypt
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class LoginService(
|
class LoginService(
|
||||||
private val users: UserRepository,
|
private val users: UserRepository,
|
||||||
|
private val sessions: SessionRepository,
|
||||||
private val environment: Map<String, String> = System.getenv(),
|
private val environment: Map<String, String> = System.getenv(),
|
||||||
) {
|
) {
|
||||||
|
data class Session(val token: String, val user: User)
|
||||||
|
|
||||||
fun initializeAdmin() {
|
fun initializeAdmin() {
|
||||||
val username = environment.valueOrDefault("ADMIN_USERNAME", "admin")
|
val username = environment.valueOrDefault("ADMIN_USERNAME", "admin")
|
||||||
if (users.findByUsername(username) == null) {
|
val existingUser = users.findByUsername(username)
|
||||||
|
if (existingUser == null) {
|
||||||
val password = environment.valueOrDefault("ADMIN_PASSWORD", "admin")
|
val password = environment.valueOrDefault("ADMIN_PASSWORD", "admin")
|
||||||
users.create(username, BCrypt.hashpw(password, BCrypt.gensalt()))
|
users.create(username, BCrypt.hashpw(password, BCrypt.gensalt()), "admin")
|
||||||
|
} else if (existingUser.role != "admin") {
|
||||||
|
users.updateRole(existingUser.id, "admin")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authenticate(username: String, password: String): Boolean {
|
fun authenticate(username: String, password: String): Session? {
|
||||||
val user = users.findByUsername(username) ?: return false
|
val user = users.findByUsername(username) ?: return null
|
||||||
return BCrypt.checkpw(password, user.passwordHash)
|
if (!BCrypt.checkpw(password, user.passwordHash)) return null
|
||||||
|
|
||||||
|
val token = UUID.randomUUID().toString()
|
||||||
|
sessions.create(token, user.id)
|
||||||
|
return Session(token, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun userForToken(token: String?): User? = token?.let(sessions::findUserByToken)
|
||||||
|
|
||||||
private fun Map<String, String>.valueOrDefault(name: String, default: String) =
|
private fun Map<String, String>.valueOrDefault(name: String, default: String) =
|
||||||
this[name]?.takeIf { it.isNotBlank() } ?: default
|
this[name]?.takeIf { it.isNotBlank() } ?: default
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/main/kotlin/dev/mduchene/bolts/user/SessionRepository.kt
Normal file
43
src/main/kotlin/dev/mduchene/bolts/user/SessionRepository.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package dev.mduchene.bolts.user
|
||||||
|
|
||||||
|
import dev.mduchene.bolts.persistence.Database
|
||||||
|
import java.sql.ResultSet
|
||||||
|
|
||||||
|
class SessionRepository(private val database: Database) {
|
||||||
|
fun create(token: String, userId: Long) {
|
||||||
|
val sql = "INSERT INTO sessions (token, user_id) VALUES (?, ?)"
|
||||||
|
|
||||||
|
database.getConnection().use { connection ->
|
||||||
|
connection.prepareStatement(sql).use { statement ->
|
||||||
|
statement.setString(1, token)
|
||||||
|
statement.setLong(2, userId)
|
||||||
|
statement.executeUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findUserByToken(token: String): User? {
|
||||||
|
val sql = """
|
||||||
|
SELECT users.id, users.username, users.password_hash, users.role
|
||||||
|
FROM sessions
|
||||||
|
JOIN users ON users.id = sessions.user_id
|
||||||
|
WHERE sessions.token = ?
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
database.getConnection().use { connection ->
|
||||||
|
connection.prepareStatement(sql).use { statement ->
|
||||||
|
statement.setString(1, token)
|
||||||
|
statement.executeQuery().use { result ->
|
||||||
|
return if (result.next()) result.toUser() else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ResultSet.toUser() = User(
|
||||||
|
id = getLong("id"),
|
||||||
|
username = getString("username"),
|
||||||
|
passwordHash = getString("password_hash"),
|
||||||
|
role = getString("role"),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -4,4 +4,5 @@ data class User(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val username: String,
|
val username: String,
|
||||||
val passwordHash: String,
|
val passwordHash: String,
|
||||||
|
val role: String,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,17 +4,18 @@ import dev.mduchene.bolts.persistence.Database
|
|||||||
import java.sql.ResultSet
|
import java.sql.ResultSet
|
||||||
|
|
||||||
class UserRepository(private val database: Database) {
|
class UserRepository(private val database: Database) {
|
||||||
fun create(username: String, passwordHash: String): User {
|
fun create(username: String, passwordHash: String, role: String): User {
|
||||||
val sql = """
|
val sql = """
|
||||||
INSERT INTO users (username, password_hash)
|
INSERT INTO users (username, password_hash, role)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?, ?)
|
||||||
RETURNING id, username, password_hash
|
RETURNING id, username, password_hash, role
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
database.getConnection().use { connection ->
|
database.getConnection().use { connection ->
|
||||||
connection.prepareStatement(sql).use { statement ->
|
connection.prepareStatement(sql).use { statement ->
|
||||||
statement.setString(1, username)
|
statement.setString(1, username)
|
||||||
statement.setString(2, passwordHash)
|
statement.setString(2, passwordHash)
|
||||||
|
statement.setString(3, role)
|
||||||
statement.executeQuery().use { result ->
|
statement.executeQuery().use { result ->
|
||||||
check(result.next()) { "User insert returned no row" }
|
check(result.next()) { "User insert returned no row" }
|
||||||
return result.toUser()
|
return result.toUser()
|
||||||
@ -25,7 +26,7 @@ class UserRepository(private val database: Database) {
|
|||||||
|
|
||||||
fun findByUsername(username: String): User? {
|
fun findByUsername(username: String): User? {
|
||||||
val sql = """
|
val sql = """
|
||||||
SELECT id, username, password_hash
|
SELECT id, username, password_hash, role
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = ?
|
WHERE username = ?
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
@ -40,9 +41,35 @@ class UserRepository(private val database: Database) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findAll(): List<User> {
|
||||||
|
val sql = "SELECT id, username, password_hash, role FROM users ORDER BY username"
|
||||||
|
|
||||||
|
database.getConnection().use { connection ->
|
||||||
|
connection.prepareStatement(sql).use { statement ->
|
||||||
|
statement.executeQuery().use { result ->
|
||||||
|
return buildList {
|
||||||
|
while (result.next()) add(result.toUser())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateRole(id: Long, role: String) {
|
||||||
|
val sql = "UPDATE users SET role = ? WHERE id = ?"
|
||||||
|
database.getConnection().use { connection ->
|
||||||
|
connection.prepareStatement(sql).use { statement ->
|
||||||
|
statement.setString(1, role)
|
||||||
|
statement.setLong(2, id)
|
||||||
|
statement.executeUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun ResultSet.toUser() = User(
|
private fun ResultSet.toUser() = User(
|
||||||
id = getLong("id"),
|
id = getLong("id"),
|
||||||
username = getString("username"),
|
username = getString("username"),
|
||||||
passwordHash = getString("password_hash"),
|
passwordHash = getString("password_hash"),
|
||||||
|
role = getString("role"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user