forked from TWS/kalkutago
Compare commits
12 commits
frontend/f
...
main
Author | SHA1 | Date | |
---|---|---|---|
scott | 0051d87d47 | ||
D. Scott Boggs | 033f2a561b | ||
D. Scott Boggs | 019ccda845 | ||
D. Scott Boggs | bc6f06e210 | ||
D. Scott Boggs | 424bc15512 | ||
D. Scott Boggs | 89b0180989 | ||
D. Scott Boggs | cd16208dd7 | ||
D. Scott Boggs | dafdd491f9 | ||
D. Scott Boggs | 003383e455 | ||
D. Scott Boggs | 5aa16762f7 | ||
D. Scott Boggs | db3ef7640a | ||
scott | f32a188750 |
1
Makefile
1
Makefile
|
@ -7,6 +7,7 @@ client/dist/index.html:
|
|||
build-client: client/dist/index.html
|
||||
|
||||
start-server: build-client
|
||||
-mkdir db.mount
|
||||
docker compose up --build -d
|
||||
|
||||
clean:
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { error } from '../error'
|
||||
import router from "../router";
|
||||
import { state } from '../state'
|
||||
|
||||
async function logOut() {
|
||||
const result = await fetch('/api/v1/auth', {method: 'DELETE'})
|
||||
if(!result.ok) return error('failed to log out')
|
||||
console.debug('logged out')
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
|
@ -31,7 +24,7 @@ async function logOut() {
|
|||
</RouterLink>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<button class="button is-info" @click="logOut">
|
||||
<button class="button is-info" @click="state.logOut()">
|
||||
Log Out
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -15,8 +15,8 @@ const className = computed(() => isSet.value ? "button is-rounded is-info" : "bu
|
|||
|
||||
async function toggle() {
|
||||
if (isSet.value) {
|
||||
await state.taskMarkedIncomplete(props.track, props.date)
|
||||
await props.track.markIncomplete(props.date)
|
||||
} else
|
||||
await state.taskCompleted(props.track, props.date)
|
||||
await props.track.markComplete(props.date)
|
||||
}
|
||||
</script>
|
|
@ -1,12 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { state } from '../state';
|
||||
import { Track } from '../track';
|
||||
|
||||
const props = defineProps<{icon: String, id: number|undefined}>()
|
||||
|
||||
const del = () => {
|
||||
if(props.id)
|
||||
if(confirm("are you sure you want to delete this track?"))
|
||||
state.removeTrack(props.id)
|
||||
Track.deleteById(props.id)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Track } from "./track"
|
|||
import { Tick } from './ticks'
|
||||
import { error } from "./error"
|
||||
import { getCookie } from "./util";
|
||||
import router from './router'
|
||||
|
||||
enum State {
|
||||
Unfetched,
|
||||
|
@ -10,15 +11,6 @@ enum State {
|
|||
Fetched,
|
||||
}
|
||||
|
||||
function dateQuery(date: Date): URLSearchParams {
|
||||
let query = new URLSearchParams()
|
||||
query.set("year", date.getUTCFullYear().toString())
|
||||
query.set("month", (date.getUTCMonth() + 1).toString())
|
||||
// good thing I still had this ^^^^^^^^^^^^^^ in mind when I wrote this 😬
|
||||
query.set("day", date.getUTCDate().toString())
|
||||
return query
|
||||
}
|
||||
|
||||
interface LoggedInUser {
|
||||
name: string
|
||||
}
|
||||
|
@ -100,36 +92,13 @@ class AppState {
|
|||
if (this.state != State.Unfetched) return
|
||||
await this.repopulate()
|
||||
}
|
||||
async taskCompleted(track: Track, date: Date): Promise<Tick> {
|
||||
const query = dateQuery(date)
|
||||
const response: Response = await fetch(`/api/v1/tracks/${track.id}/ticked?${query.toString()}`, { method: "PATCH" })
|
||||
const body = await response.text()
|
||||
if (!response.ok) {
|
||||
error(body)
|
||||
throw new Error(`error setting tick for track ${track.id} ("${track.name}"): ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return JSON.parse(body)
|
||||
}
|
||||
async taskMarkedIncomplete(track: Track, date: Date) {
|
||||
const query = dateQuery(date)
|
||||
const { ok, status, statusText } = await fetch(`/api/v1/tracks/${track.id}/all-ticks?${query.toString()}`, { method: 'DELETE' })
|
||||
if (!ok)
|
||||
error(`error deleting ticks for ${track.id}: ${statusText} (${status})`)
|
||||
}
|
||||
async addTrack(track: Track): Promise<boolean> {
|
||||
const response = await fetch('/api/v1/tracks', {
|
||||
method: "POST",
|
||||
body: JSON.stringify(track),
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
if (!response.ok)
|
||||
error(`error submitting track: ${track}: ${response.statusText} (${response.status})`)
|
||||
return response.ok
|
||||
}
|
||||
async removeTrack(trackID: number) {
|
||||
const response = await fetch(`/api/v1/tracks/${trackID}`, { method: "DELETE" })
|
||||
if (!response.ok) error(`error deleting track with ID ${trackID}: ${response.statusText} (${response.status})`)
|
||||
async logOut() {
|
||||
const result = await fetch('/api/v1/auth', {method: 'DELETE'})
|
||||
if(!result.ok) return error('failed to log out')
|
||||
this.user = undefined
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const state = reactive(new AppState)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { error } from "./error"
|
||||
import { Tick, ITick } from './ticks'
|
||||
import { dateQuery } from "./util"
|
||||
|
||||
export interface ITrack {
|
||||
id?: number
|
||||
|
@ -48,6 +49,34 @@ export class Track implements ITrack {
|
|||
this.fetchTicks = this.fetchTicks.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this track to the database. A `TrackAdded` event should have been
|
||||
* received from the server on the event stream by the time this returns.
|
||||
*
|
||||
* @returns whether or not the query succeeded
|
||||
*/
|
||||
async create(): Promise<boolean> {
|
||||
// note that this.id is expected to be `undefined` here.
|
||||
const response = await fetch('/api/v1/tracks', {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this),
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
if (!response.ok)
|
||||
error(`error submitting track ${this.name}: ${response.statusText} (${response.status})`)
|
||||
return response.ok
|
||||
}
|
||||
|
||||
async delete() {
|
||||
const id = this.id
|
||||
if (id) await Track.deleteById(id)
|
||||
}
|
||||
|
||||
static async deleteById(id: number) {
|
||||
const response = await fetch(`/api/v1/tracks/${id}`, { method: "DELETE" })
|
||||
if (!response.ok) error(`error deleting track with ID ${id}: ${response.statusText} (${response.status})`)
|
||||
}
|
||||
|
||||
static fromJSON(track: ITrack): Track {
|
||||
return new Track(track.id, track.name, track.description, track.icon, track.enabled, track.multiple_entries_per_day, track.color, track.order)
|
||||
}
|
||||
|
@ -98,4 +127,36 @@ export class Track implements ITrack {
|
|||
}
|
||||
return []
|
||||
}
|
||||
/**
|
||||
* Mark this track as being completed on the given date. A `TickAdded` event
|
||||
* should have been received from the server on the event stream by the time
|
||||
* this returns.
|
||||
*
|
||||
* @param date the date the task was completed
|
||||
* @returns the decoded server API response
|
||||
*/
|
||||
async markComplete(date: Date) {
|
||||
const query = dateQuery(date)
|
||||
const response: Response = await fetch(`/api/v1/tracks/${this.id}/ticked?${query.toString()}`, { method: "PATCH" })
|
||||
const body = await response.text()
|
||||
if (!response.ok) {
|
||||
error(body)
|
||||
throw new Error(`error setting tick for track ${this.id} ("${this.name}"): ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return JSON.parse(body)
|
||||
}
|
||||
/**
|
||||
* Mark this track as being incomplete on the given date. A `TickAdded` event
|
||||
* should have been received from the server on the event stream by the time
|
||||
* this returns.
|
||||
*
|
||||
* @param date the date the task was completed
|
||||
* @returns the decoded server API response
|
||||
*/
|
||||
async markIncomplete(date: Date) {
|
||||
const query = dateQuery(date)
|
||||
const { ok, status, statusText } = await fetch(`/api/v1/tracks/${this.id}/all-ticks?${query.toString()}`, { method: 'DELETE' })
|
||||
if (!ok)
|
||||
error(`error deleting ticks for ${this.id}: ${statusText} (${status})`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,4 +5,13 @@ export function getCookie(key: string): string | null {
|
|||
if(end === -1)
|
||||
end = undefined
|
||||
return document.cookie.substring(start + key.length + 1, end)
|
||||
}
|
||||
}
|
||||
|
||||
export function dateQuery(date: Date): URLSearchParams {
|
||||
let query = new URLSearchParams()
|
||||
query.set("year", date.getUTCFullYear().toString())
|
||||
query.set("month", (date.getUTCMonth() + 1).toString())
|
||||
// good thing I still had this ^^^^^^^^^^^^^^ in mind when I wrote this 😬
|
||||
query.set("day", date.getUTCDate().toString())
|
||||
return query
|
||||
}
|
||||
|
|
|
@ -3,38 +3,38 @@ import { ref, computed } from 'vue'
|
|||
import { state } from '../state';
|
||||
import router from '../router'
|
||||
|
||||
const name = ref("")
|
||||
const password = ref("")
|
||||
const $name = ref("")
|
||||
const $password = ref("")
|
||||
const signUpWait = ref(false)
|
||||
const loginWait = ref(false)
|
||||
const signUpClass = computed(() => `submit button is-success ${signUpWait.value ? 'is-loading' : ''}`)
|
||||
const loginClass = computed(() => `submit button is-info ${loginWait.value ? 'is-loading' : ''}`)
|
||||
|
||||
async function signUp() {
|
||||
const $name = name.value
|
||||
const name = $name.value, password = $password.value
|
||||
signUpWait.value = true
|
||||
const result = await fetch("/api/v1/auth", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: $name, password: password.value }),
|
||||
body: JSON.stringify({ name, password }),
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
if (result.ok) {
|
||||
state.user = { name: $name }
|
||||
state.user = { name }
|
||||
await state.repopulate()
|
||||
router.push("/")
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const $name = name.value
|
||||
const name = $name.value, password = $password.value
|
||||
loginWait.value = true
|
||||
const result = await fetch("/api/v1/auth", {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: $name, password: password.value }),
|
||||
body: JSON.stringify({ name, password }),
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
if (result.ok) {
|
||||
state.user = { name: $name }
|
||||
state.user = { name }
|
||||
await state.repopulate()
|
||||
router.push("/")
|
||||
}
|
||||
|
@ -54,14 +54,14 @@ if(state.user?.name) router.push("/")
|
|||
<div class="field">
|
||||
<label for="username" class=label>Name</label>
|
||||
<div class="control">
|
||||
<input type="text" name="username" class="input" v-model="name" />
|
||||
<input type="text" name="username" class="input" v-model="$name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password" class="label">Password</label>
|
||||
<div class="control">
|
||||
<input type="password" name="password" class="input" v-model="password" />
|
||||
<input type="password" name="password" class="input" v-model="$password" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { RouterLink, useRouter } from 'vue-router';
|
||||
import { Track } from '../track';
|
||||
import { computed, ref } from 'vue';
|
||||
import { state } from '../state';
|
||||
|
||||
const props = defineProps<{ initialState?: Track }>()
|
||||
const router = useRouter()
|
||||
|
@ -26,7 +25,7 @@ const submit = async () => {
|
|||
const track = new Track(undefined, name.value, description.value,
|
||||
icon.value, Number(enabled.value), Number(multipleEntriesPerDay.value),
|
||||
color.value, order.value)
|
||||
if (await state.addTrack(track))
|
||||
if (await track.create())
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -14,7 +14,7 @@ services:
|
|||
POSTGRES_USER: kalkutago
|
||||
POSTGRES_DB: kalkutago
|
||||
POSTGRES_HOST: database
|
||||
secrets: [ postgres-password ]
|
||||
secrets: [ postgres-password, cookie-secret ]
|
||||
depends_on: [ database ]
|
||||
volumes:
|
||||
- ./client/dist:/src/public:ro
|
||||
|
|
|
@ -32,7 +32,7 @@ async fn get_track_check_user(
|
|||
track_id: i32,
|
||||
user: &users::Model,
|
||||
) -> Result<Json<tracks::Model>, Either<Status, api::ErrorResponder>> {
|
||||
if let Some(Some(user)) = user
|
||||
if let Some(Some(track)) = user
|
||||
.find_related(Tracks)
|
||||
.filter(tracks::Column::Id.eq(track_id))
|
||||
.one(db)
|
||||
|
@ -40,7 +40,7 @@ async fn get_track_check_user(
|
|||
.transpose()
|
||||
.map(|it| it.ok())
|
||||
{
|
||||
Ok(Json(user))
|
||||
Ok(Json(track))
|
||||
} else {
|
||||
Err(Left(Status::NotFound))
|
||||
}
|
||||
|
|
|
@ -95,6 +95,7 @@ impl Update {
|
|||
if receiver_count > 0 {
|
||||
trace!(receiver_count = receiver_count, update = as_serde!(self); "sending update");
|
||||
let count = tx.send(self.clone())?;
|
||||
trace!(count = count; "update sent");
|
||||
} else {
|
||||
trace!("no update receivers, skipping message");
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ impl ActiveModel {
|
|||
pub fn new(name: impl AsRef<str>, password: impl AsRef<str>) -> error::Result<Self> {
|
||||
use sea_orm::ActiveValue::Set;
|
||||
let name = Set(name.as_ref().to_string());
|
||||
let password_hash = Set(hash(password.as_ref(), DEFAULT_COST + 2)?);
|
||||
let password_hash = Set(hash(password.as_ref(), DEFAULT_COST)?);
|
||||
Ok(Self {
|
||||
name,
|
||||
password_hash,
|
||||
|
|
Loading…
Reference in a new issue