Added deployment script.
This commit is contained in:
parent
3a1092d468
commit
e947d7567a
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# A build/deploy script to deploy litelist to litelist.andrewlalis.com
|
||||||
|
# Builds the front-end app, builds the API, and deploys them to the server.
|
||||||
|
|
||||||
|
echo "Building app"
|
||||||
|
cd litelist-app
|
||||||
|
rm -rf dist
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo "Building api"
|
||||||
|
cd litelist-api
|
||||||
|
dub clean
|
||||||
|
dub build --build=release --compiler=/opt/ldc2/ldc2-1.33.0-linux-x86_64/bin/ldc2
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Now deploy
|
||||||
|
ssh -f root@andrewlalis.com 'systemctl stop litelist-api.service'
|
||||||
|
echo "Copying litelist-api binary to server"
|
||||||
|
scp litelist-api/litelist-api root@andrewlalis.com:/opt/litelist/
|
||||||
|
echo "Copying app distribution to server"
|
||||||
|
rsync -rav -e ssh --delete litelist-app/dist/* root@andrewlalis.com:/opt/litelist/app-content
|
||||||
|
ssh -f root@andrewlalis.com 'systemctl start litelist-api.service'
|
|
@ -0,0 +1,13 @@
|
||||||
|
[Unit]
|
||||||
|
Description=litelist-api
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/litelist
|
||||||
|
ExecStart=/opt/litelist/litelist-api
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -5,6 +5,7 @@
|
||||||
"copyright": "Copyright © 2023, Andrew Lalis",
|
"copyright": "Copyright © 2023, Andrew Lalis",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"botan": "~>1.13.5",
|
"botan": "~>1.13.5",
|
||||||
|
"d-properties": "~>1.0.4",
|
||||||
"d2sqlite3": "~>1.0.0",
|
"d2sqlite3": "~>1.0.0",
|
||||||
"handy-httpd": "~>7.9.3",
|
"handy-httpd": "~>7.9.3",
|
||||||
"jwt": "~>0.4.0",
|
"jwt": "~>0.4.0",
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"versions": {
|
"versions": {
|
||||||
"botan": "1.13.5",
|
"botan": "1.13.5",
|
||||||
"botan-math": "1.0.4",
|
"botan-math": "1.0.4",
|
||||||
|
"d-properties": "1.0.4",
|
||||||
"d2sqlite3": "1.0.0",
|
"d2sqlite3": "1.0.0",
|
||||||
"handy-httpd": "7.9.3",
|
"handy-httpd": "7.9.3",
|
||||||
"httparsed": "1.2.1",
|
"httparsed": "1.2.1",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import slf4d.default_provider;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
auto provider = new shared DefaultProvider(true, Levels.INFO);
|
auto provider = new shared DefaultProvider(true, Levels.INFO);
|
||||||
provider.getLoggerFactory().setModuleLevelPrefix("handy_httpd", Levels.WARN);
|
// provider.getLoggerFactory().setModuleLevelPrefix("handy_httpd", Levels.WARN);
|
||||||
configureLoggingProvider(provider);
|
configureLoggingProvider(provider);
|
||||||
|
|
||||||
HttpServer server = initServer();
|
HttpServer server = initServer();
|
||||||
|
@ -14,23 +14,41 @@ void main() {
|
||||||
private HttpServer initServer() {
|
private HttpServer initServer() {
|
||||||
import handy_httpd.handlers.path_delegating_handler;
|
import handy_httpd.handlers.path_delegating_handler;
|
||||||
import handy_httpd.handlers.filtered_handler;
|
import handy_httpd.handlers.filtered_handler;
|
||||||
|
import handy_httpd.handlers.file_resolving_handler;
|
||||||
|
import d_properties;
|
||||||
import auth;
|
import auth;
|
||||||
import lists;
|
import lists;
|
||||||
|
import std.file;
|
||||||
|
import std.conv;
|
||||||
|
|
||||||
ServerConfig config = ServerConfig.defaultValues();
|
ServerConfig config = ServerConfig.defaultValues();
|
||||||
config.enableWebSockets = false;
|
config.enableWebSockets = false;
|
||||||
config.workerPoolSize = 3;
|
config.workerPoolSize = 3;
|
||||||
config.port = 8080;
|
|
||||||
config.connectionQueueSize = 10;
|
config.connectionQueueSize = 10;
|
||||||
|
if (exists("application.properties")) {
|
||||||
|
Properties props = Properties("application.properties");
|
||||||
|
if (props.has("port")) {
|
||||||
|
config.port = props.get("port").to!ushort;
|
||||||
|
}
|
||||||
|
if (props.has("workers")) {
|
||||||
|
config.workerPoolSize = props.get("workers").to!size_t;
|
||||||
|
}
|
||||||
|
if (props.has("hostname")) {
|
||||||
|
config.hostname = props.get("hostname");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set some CORS headers to prevent headache.
|
||||||
config.defaultHeaders["Access-Control-Allow-Origin"] = "*";
|
config.defaultHeaders["Access-Control-Allow-Origin"] = "*";
|
||||||
config.defaultHeaders["Access-Control-Allow-Credentials"] = "true";
|
config.defaultHeaders["Access-Control-Allow-Credentials"] = "true";
|
||||||
config.defaultHeaders["Access-Control-Allow-Methods"] = "*";
|
config.defaultHeaders["Access-Control-Allow-Methods"] = "*";
|
||||||
config.defaultHeaders["Vary"] = "origin";
|
config.defaultHeaders["Vary"] = "origin";
|
||||||
config.defaultHeaders["Access-Control-Allow-Headers"] = "Authorization";
|
config.defaultHeaders["Access-Control-Allow-Headers"] = "Authorization";
|
||||||
|
|
||||||
|
immutable string API_PATH = "/api";
|
||||||
|
|
||||||
auto mainHandler = new PathDelegatingHandler();
|
auto mainHandler = new PathDelegatingHandler();
|
||||||
mainHandler.addMapping(Method.GET, "/status", (ref HttpRequestContext ctx) {
|
mainHandler.addMapping(Method.GET, API_PATH ~ "/status", (ref HttpRequestContext ctx) {
|
||||||
ctx.response.writeBodyString("online");
|
ctx.response.writeBodyString("online");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,18 +56,22 @@ private HttpServer initServer() {
|
||||||
ctx.response.setStatus(HttpStatus.OK);
|
ctx.response.setStatus(HttpStatus.OK);
|
||||||
});
|
});
|
||||||
|
|
||||||
mainHandler.addMapping(Method.POST, "/register", &createNewUser);
|
mainHandler.addMapping(Method.POST, API_PATH ~ "/register", &createNewUser);
|
||||||
mainHandler.addMapping(Method.POST, "/login", &handleLogin);
|
mainHandler.addMapping(Method.POST, API_PATH ~ "/login", &handleLogin);
|
||||||
mainHandler.addMapping(Method.GET, "/me", &getMyUser);
|
mainHandler.addMapping(Method.GET, API_PATH ~ "/me", &getMyUser);
|
||||||
mainHandler.addMapping(Method.OPTIONS, "/**", optionsHandler);
|
mainHandler.addMapping(Method.DELETE, API_PATH ~ "/me", &deleteMyUser);
|
||||||
mainHandler.addMapping(Method.DELETE, "/me", &deleteMyUser);
|
mainHandler.addMapping(Method.GET, API_PATH ~ "/renew-token", &renewToken);
|
||||||
|
|
||||||
mainHandler.addMapping(Method.GET, "/lists", &getNoteLists);
|
mainHandler.addMapping(Method.GET, API_PATH ~ "/lists", &getNoteLists);
|
||||||
mainHandler.addMapping(Method.POST, "/lists", &createNoteList);
|
mainHandler.addMapping(Method.POST, API_PATH ~ "/lists", &createNoteList);
|
||||||
mainHandler.addMapping(Method.GET, "/lists/{id}", &getNoteList);
|
mainHandler.addMapping(Method.GET, API_PATH ~ "/lists/{id}", &getNoteList);
|
||||||
mainHandler.addMapping(Method.DELETE, "/lists/{id}", &deleteNoteList);
|
mainHandler.addMapping(Method.DELETE, API_PATH ~ "/lists/{id}", &deleteNoteList);
|
||||||
mainHandler.addMapping(Method.POST, "/lists/{listId}/notes", &createNote);
|
mainHandler.addMapping(Method.POST, API_PATH ~ "/lists/{listId}/notes", &createNote);
|
||||||
mainHandler.addMapping(Method.DELETE, "/lists/{listId}/notes/{noteId}", &deleteNote);
|
mainHandler.addMapping(Method.DELETE, API_PATH ~ "/lists/{listId}/notes/{noteId}", &deleteNote);
|
||||||
|
|
||||||
|
mainHandler.addMapping(Method.OPTIONS, API_PATH ~ "/**", optionsHandler);
|
||||||
|
|
||||||
|
mainHandler.addMapping("/**", new FileResolvingHandler("app-content", DirectoryResolutionStrategies.none));
|
||||||
|
|
||||||
return new HttpServer(mainHandler, config);
|
return new HttpServer(mainHandler, config);
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,15 @@ void handleLogin(ref HttpRequestContext ctx) {
|
||||||
ctx.response.writeBodyString(resp.toString(), "application/json");
|
ctx.response.writeBodyString(resp.toString(), "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void renewToken(ref HttpRequestContext ctx) {
|
||||||
|
if (!validateAuthenticatedRequest(ctx)) return;
|
||||||
|
AuthContext auth = AuthContextHolder.getOrThrow();
|
||||||
|
|
||||||
|
JSONValue resp = JSONValue(string[string].init);
|
||||||
|
resp.object["token"] = generateToken(auth.user);
|
||||||
|
ctx.response.writeBodyString(resp.toString(), "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
void createNewUser(ref HttpRequestContext ctx) {
|
void createNewUser(ref HttpRequestContext ctx) {
|
||||||
JSONValue userData = ctx.request.readBodyAsJson();
|
JSONValue userData = ctx.request.readBodyAsJson();
|
||||||
string username = userData.object["username"].str;
|
string username = userData.object["username"].str;
|
||||||
|
|
|
@ -14,9 +14,6 @@ static UserDataSource userDataSource;
|
||||||
|
|
||||||
static this() {
|
static this() {
|
||||||
userDataSource = new FsSqliteDataSource();
|
userDataSource = new FsSqliteDataSource();
|
||||||
import slf4d;
|
|
||||||
import d2sqlite3.library;
|
|
||||||
infoF!"Sqlite version %s"(versionString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct User {
|
struct User {
|
||||||
|
@ -62,7 +59,7 @@ class FsSqliteDataSource : UserDataSource {
|
||||||
User createUser(string username, string email, string passwordHash) {
|
User createUser(string username, string email, string passwordHash) {
|
||||||
string dirPath = buildPath(USERS_DIR, username);
|
string dirPath = buildPath(USERS_DIR, username);
|
||||||
if (exists(dirPath)) throw new Exception("User already has a directory.");
|
if (exists(dirPath)) throw new Exception("User already has a directory.");
|
||||||
mkdir(dirPath);
|
mkdirRecurse(dirPath);
|
||||||
string dataPath = buildPath(dirPath, DATA_FILE);
|
string dataPath = buildPath(dirPath, DATA_FILE);
|
||||||
JSONValue userObj = JSONValue(string[string].init);
|
JSONValue userObj = JSONValue(string[string].init);
|
||||||
userObj.object["username"] = username;
|
userObj.object["username"] = username;
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
VITE_API_URL=http://localhost:8080/api
|
|
@ -0,0 +1 @@
|
||||||
|
VITE_API_URL=https://litelist.andrewlalis.com/api
|
|
@ -38,12 +38,7 @@ export async function login(username: string, password: string): Promise<LoginIn
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const content: LoginTokenResponse = await response.json()
|
const content: LoginTokenResponse = await response.json()
|
||||||
const token = content.token
|
const token = content.token
|
||||||
const userResponse = await fetch(API_URL + "/me", {
|
const user = await getMyUser(token)
|
||||||
headers: {
|
|
||||||
"Authorization": "Bearer " + token
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const user: User = await userResponse.json()
|
|
||||||
return {token: token, user: user}
|
return {token: token, user: user}
|
||||||
} else if (response.status < 500) {
|
} else if (response.status < 500) {
|
||||||
throw {message: "Invalid credentials."}
|
throw {message: "Invalid credentials."}
|
||||||
|
@ -51,3 +46,24 @@ export async function login(username: string, password: string): Promise<LoginIn
|
||||||
throw {message: "Server error. Try again later."}
|
throw {message: "Server error. Try again later."}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMyUser(token: string): Promise<User> {
|
||||||
|
const userResponse = await fetch(API_URL + "/me", {
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer " + token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (userResponse.ok) return await userResponse.json()
|
||||||
|
throw userResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renewToken(token: string): Promise<string> {
|
||||||
|
const response = await fetch(API_URL + "/renew-token", {
|
||||||
|
headers: {"Authorization": "Bearer " + token}
|
||||||
|
})
|
||||||
|
if (response.ok) {
|
||||||
|
const content: LoginTokenResponse = await response.json()
|
||||||
|
return content.token
|
||||||
|
}
|
||||||
|
throw response
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export const API_URL = "http://localhost:8080"
|
export const API_URL = import.meta.env.VITE_API_URL
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {useAuthStore} from "@/stores/auth";
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
async function doLogOut() {
|
||||||
|
await authStore.logOut()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="button-container">
|
||||||
|
<button @click="doLogOut()">
|
||||||
|
Log Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.button-container {
|
||||||
|
max-width: 50ch;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,39 +1,51 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
import LoginView from "@/views/LoginView.vue";
|
import LoginView from "@/views/LoginView.vue";
|
||||||
import ListsView from "@/views/ListsView.vue";
|
import ListsView from "@/views/ListsView.vue";
|
||||||
import {useAuthStore} from "@/stores/auth";
|
import {useAuthStore} from "@/stores/auth";
|
||||||
import SingleListView from "@/views/SingleListView.vue";
|
import SingleListView from "@/views/SingleListView.vue";
|
||||||
|
|
||||||
function checkAuth() {
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
if (!authStore.authenticated) return "login"
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "home-redirect",
|
|
||||||
redirect: to => {
|
redirect: to => {
|
||||||
return "/login"
|
return "/login"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
component: LoginView
|
component: LoginView,
|
||||||
|
beforeEnter: () => {
|
||||||
|
// Check if the user is already logged in, and if so, go straight to /lists.
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
if (authStore.authenticated) {
|
||||||
|
return "/lists"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/lists",
|
path: "/lists",
|
||||||
component: ListsView,
|
component: ListsView
|
||||||
beforeEnter: checkAuth
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/lists/:id",
|
path: "/lists/:id",
|
||||||
component: SingleListView,
|
component: SingleListView
|
||||||
beforeEnter: checkAuth
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const publicRoutes = [
|
||||||
|
"/",
|
||||||
|
"/login"
|
||||||
|
]
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
await authStore.tryLogInFromStoredToken()
|
||||||
|
if (!publicRoutes.includes(to.path) && !authStore.authenticated) {
|
||||||
|
return "/login" // Redirect to login page if user is trying to go to an authenticated page.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -1,26 +1,76 @@
|
||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {type Ref, ref} from "vue";
|
import {type Ref, ref} from "vue";
|
||||||
import type {User} from "@/api/auth";
|
import type {User} from "@/api/auth";
|
||||||
import {emptyUser} from "@/api/auth";
|
import {emptyUser, getMyUser, renewToken} from "@/api/auth";
|
||||||
|
import {getSecondsTilExpire} from "@/util";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
|
||||||
|
const LOCAL_STORAGE_KEY = "access_token"
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", () => {
|
export const useAuthStore = defineStore("auth", () => {
|
||||||
const authenticated: Ref<boolean> = ref(false)
|
const authenticated: Ref<boolean> = ref(false)
|
||||||
const user: Ref<User> = ref(emptyUser())
|
const user: Ref<User> = ref(emptyUser())
|
||||||
const token: Ref<string> = ref("")
|
const token: Ref<string> = ref("")
|
||||||
|
const tokenRefreshInterval: Ref<number> = ref(0)
|
||||||
|
|
||||||
function logIn(newToken: string, newUser: User) {
|
const router = useRouter()
|
||||||
|
|
||||||
|
async function logIn(newToken: string, newUser: User) {
|
||||||
authenticated.value = true
|
authenticated.value = true
|
||||||
user.value = newUser
|
user.value = newUser
|
||||||
token.value = newToken
|
token.value = newToken
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY, token.value)
|
||||||
|
tokenRefreshInterval.value = setInterval(tryRefreshToken, 60000)
|
||||||
|
await router.push("/lists")
|
||||||
}
|
}
|
||||||
|
|
||||||
function logOut() {
|
async function logOut() {
|
||||||
authenticated.value = false
|
authenticated.value = false
|
||||||
user.value = emptyUser()
|
user.value = emptyUser()
|
||||||
token.value = ""
|
token.value = ""
|
||||||
|
if (tokenRefreshInterval.value) {
|
||||||
|
clearInterval(tokenRefreshInterval.value)
|
||||||
|
}
|
||||||
|
localStorage.removeItem(LOCAL_STORAGE_KEY)
|
||||||
|
await router.push("/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
return {authenticated, user, token, logIn, logOut}
|
/**
|
||||||
})
|
* Periodically called to renew the access token, if it's close to expiring.
|
||||||
|
*/
|
||||||
|
async function tryRefreshToken() {
|
||||||
|
if (authenticated.value && getSecondsTilExpire(token.value) < 100) {
|
||||||
|
try {
|
||||||
|
const newToken = await renewToken(token.value)
|
||||||
|
token.value = newToken
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY, newToken)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn("Failed to renew the access token.", e)
|
||||||
|
await logOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type AuthStore = typeof useAuthStore
|
/**
|
||||||
|
* Tries to log in using an access token stored in the local storage.
|
||||||
|
*/
|
||||||
|
async function tryLogInFromStoredToken(): Promise<void> {
|
||||||
|
if (authenticated.value) return
|
||||||
|
const storedToken: string | null = localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||||
|
if (storedToken && getSecondsTilExpire(storedToken) > 60) {
|
||||||
|
try {
|
||||||
|
const storedUser = await getMyUser(storedToken)
|
||||||
|
console.log("Logging in using stored token for user: " + storedUser.username)
|
||||||
|
logIn(storedToken, storedUser)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn("Failed to log in using stored token.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated, user, token,
|
||||||
|
logIn, logOut,
|
||||||
|
tryLogInFromStoredToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
@ -5,4 +5,23 @@ export function stringToColor(str: string, saturation: number = 100, lightness:
|
||||||
hash = hash & hash;
|
hash = hash & hash;
|
||||||
}
|
}
|
||||||
return `hsl(${(hash % 360)}, ${saturation}%, ${lightness}%)`;
|
return `hsl(${(hash % 360)}, ${saturation}%, ${lightness}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseJwt (token: string): any {
|
||||||
|
const base64Url = token.split('.')[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
|
||||||
|
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||||
|
}).join(''));
|
||||||
|
return JSON.parse(jsonPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUnixTime(): number {
|
||||||
|
return Math.floor(new Date().getTime() / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSecondsTilExpire(token: string): number {
|
||||||
|
const now = getUnixTime()
|
||||||
|
const decoded = parseJwt(token)
|
||||||
|
return decoded.exp - now
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useAuthStore} from "@/stores/auth";
|
import {useAuthStore} from "@/stores/auth";
|
||||||
import {onMounted, ref, type Ref} from "vue";
|
import {nextTick, onMounted, ref, type Ref} from "vue";
|
||||||
import type {NoteList} from "@/api/lists";
|
import type {NoteList} from "@/api/lists";
|
||||||
import {createNoteList, getNoteLists} from "@/api/lists";
|
import {createNoteList, getNoteLists} from "@/api/lists";
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
import {stringToColor} from "@/util";
|
import {stringToColor} from "@/util";
|
||||||
|
import LogOutButton from "@/components/LogOutButton.vue";
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -26,6 +27,12 @@ function toggleCreatingNewList() {
|
||||||
newListModel.value.description = ""
|
newListModel.value.description = ""
|
||||||
}
|
}
|
||||||
creatingNewList.value = !creatingNewList.value
|
creatingNewList.value = !creatingNewList.value
|
||||||
|
if (creatingNewList.value) {
|
||||||
|
nextTick(() => {
|
||||||
|
const nameField: HTMLElement | null = document.getElementById("list-name")
|
||||||
|
if (nameField) nameField.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goToList(id: number) {
|
async function goToList(id: number) {
|
||||||
|
@ -33,12 +40,12 @@ async function goToList(id: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createList() {
|
async function createList() {
|
||||||
const noteList = await createNoteList(
|
await createNoteList(
|
||||||
authStore.token,
|
authStore.token,
|
||||||
newListModel.value.name,
|
newListModel.value.name,
|
||||||
newListModel.value.description
|
newListModel.value.description
|
||||||
)
|
)
|
||||||
await router.push("/lists/" + noteList.id)
|
noteLists.value = await getNoteLists(authStore.token)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -76,6 +83,10 @@ async function createList() {
|
||||||
<h3 v-text="list.name"></h3>
|
<h3 v-text="list.name"></h3>
|
||||||
<p v-text="list.description"></p>
|
<p v-text="list.description"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<LogOutButton/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -20,8 +20,7 @@ function resetLogin() {
|
||||||
async function doLogin() {
|
async function doLogin() {
|
||||||
try {
|
try {
|
||||||
const info = await login(loginModel.value.username, loginModel.value.password)
|
const info = await login(loginModel.value.username, loginModel.value.password)
|
||||||
authStore.logIn(info.token, info.user)
|
await authStore.logIn(info.token, info.user)
|
||||||
await router.push("lists")
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error.message)
|
console.error(error.message)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {NoteList} from "@/api/lists";
|
import type {NoteList} from "@/api/lists";
|
||||||
import {onMounted, ref, type Ref} from "vue";
|
import {nextTick, onMounted, ref, type Ref} from "vue";
|
||||||
import {useAuthStore} from "@/stores/auth";
|
import {useAuthStore} from "@/stores/auth";
|
||||||
import {createNote, deleteNote, deleteNoteList, getNoteList} from "@/api/lists";
|
import {createNote, deleteNote, deleteNoteList, getNoteList} from "@/api/lists";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
@ -14,7 +14,8 @@ const creatingNote: Ref<boolean> = ref(false)
|
||||||
const newNoteText: Ref<string> = ref("")
|
const newNoteText: Ref<string> = ref("")
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const listId = parseInt(route.params.id)
|
let listId: number | null = null;
|
||||||
|
if (!Array.isArray(route.params.id)) listId = parseInt(route.params.id)
|
||||||
// If no valid list id could be found, go back.
|
// If no valid list id could be found, go back.
|
||||||
if (!listId) {
|
if (!listId) {
|
||||||
await router.push("/lists")
|
await router.push("/lists")
|
||||||
|
@ -34,15 +35,15 @@ async function deleteNoteAndRefresh(id: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteList(id: number) {
|
async function deleteList(id: number) {
|
||||||
const dialog: HTMLDialogElement = document.getElementById("list-delete-dialog")
|
const dialog = document.getElementById("list-delete-dialog") as HTMLDialogElement
|
||||||
dialog.showModal()
|
dialog.showModal()
|
||||||
const confirmButton: HTMLButtonElement = document.getElementById("delete-confirm-button")
|
const confirmButton = document.getElementById("delete-confirm-button") as HTMLButtonElement
|
||||||
confirmButton.onclick = async () => {
|
confirmButton.onclick = async () => {
|
||||||
dialog.close()
|
dialog.close()
|
||||||
await deleteNoteList(authStore.token, id)
|
await deleteNoteList(authStore.token, id)
|
||||||
await router.push("/lists")
|
await router.push("/lists")
|
||||||
}
|
}
|
||||||
const cancelButton: HTMLButtonElement = document.getElementById("delete-cancel-button")
|
const cancelButton = document.getElementById("delete-cancel-button") as HTMLButtonElement
|
||||||
cancelButton.onclick = async () => {
|
cancelButton.onclick = async () => {
|
||||||
dialog.close()
|
dialog.close()
|
||||||
}
|
}
|
||||||
|
@ -53,6 +54,12 @@ function toggleCreatingNewNote() {
|
||||||
newNoteText.value = ""
|
newNoteText.value = ""
|
||||||
}
|
}
|
||||||
creatingNote.value = !creatingNote.value
|
creatingNote.value = !creatingNote.value
|
||||||
|
if (creatingNote.value) {
|
||||||
|
nextTick(() => {
|
||||||
|
const noteInput = document.getElementById("note-content")
|
||||||
|
if (noteInput) noteInput.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNoteAndRefresh() {
|
async function createNoteAndRefresh() {
|
||||||
|
|
Loading…
Reference in New Issue