diff --git a/client/package.json b/client/package.json index 551dd0e..d1881f4 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "@open-rpc/client-js": "^1.8.1", "@vueuse/core": "^9.13.0", "@vueuse/head": "^1.1.15", + "axios": "^1.3.4", "events": "^3.3.0", "focus-trap": "^7.3.1", "focus-trap-vue": "^4.0.2", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 64a3ad2..c79955f 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -12,6 +12,7 @@ specifiers: '@vue-macros/volar': ^0.8.4 '@vueuse/core': ^9.13.0 '@vueuse/head': ^1.1.15 + axios: ^1.3.4 eslint: ^8.35.0 eslint-plugin-vue: ^9.9.0 events: ^3.3.0 @@ -37,6 +38,7 @@ dependencies: '@open-rpc/client-js': 1.8.1 '@vueuse/core': 9.13.0_vue@3.2.47 '@vueuse/head': 1.1.15_vue@3.2.47 + axios: 1.3.4 events: 3.3.0 focus-trap: 7.3.1 focus-trap-vue: 4.0.2_oggptlzwchqpaguemspe4ract4 @@ -1507,11 +1509,25 @@ packages: '@babel/types': 7.21.2 dev: true + /asynckit/0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + /available-typed-arrays/1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} dev: true + /axios/1.3.4: + resolution: {integrity: sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -1646,6 +1662,13 @@ packages: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: true + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -1741,6 +1764,11 @@ packages: resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} dev: true + /delayed-stream/1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + /destr/1.2.2: resolution: {integrity: sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA==} dev: true @@ -2140,12 +2168,31 @@ packages: tabbable: 6.1.1 dev: false + /follow-redirects/1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 dev: true + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /fs-minipass/2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -2800,6 +2847,18 @@ packages: braces: 3.0.2 picomatch: 2.3.1 + /mime-db/1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types/2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + /mimic-fn/2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -3086,6 +3145,10 @@ packages: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true + /proxy-from-env/1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /prr/1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} dev: true diff --git a/client/src/App.vue b/client/src/App.vue index 594b84c..f862a2b 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -3,7 +3,7 @@ import IDashboard from '~icons/ri/dashboard-2-line'; import IRule from '~icons/material-symbols/rule-folder-outline-sharp'; import IAddress from '~icons/eos-icons/ip'; -import { authenticate, checkAuthentication, setup } from "./api"; +import { authenticate, logout, checkAuthentication, setup } from "./api"; enum NavState { Open, Reduced, Collapsed }; const NavStateCount = 3; @@ -31,17 +31,18 @@ async function tryLogin() { console.info("authentication error"); } else { // TODO Check for MFA here - authState = 1; + authState = AuthState.Authenticated; } } async function tryLogout() { - authState = 0; + logout(); + authState = AuthState.Unauthenticated; } function deAuthenticatedCallback() { console.info("Unauthenticated"); - authState = 0; + authState = AuthState.Unauthenticated; } onMounted(async() => { @@ -49,7 +50,7 @@ onMounted(async() => { let res = await checkAuthentication(); authState = res.auth; loginDisabled = false; - if (authState > 0) { + if (authState === AuthState.Authenticated) { console.info("Already Authenticated ", authState); } else console.info("Check Authentication error",res.error); diff --git a/client/src/api.ts b/client/src/api.ts index 5431aa7..db26556 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -1,7 +1,8 @@ import { RequestManager, HTTPTransport, WebSocketTransport, Client } from "@open-rpc/client-js"; +import axios from "axios"; const httpTransport = new HTTPTransport("http://"+ window.location.host +"/api"); -const socktransport = new WebSocketTransport("ws://"+ window.location.host + "/ws/api"); -const manager = new RequestManager([socktransport, httpTransport], () => crypto.randomUUID()); +// const socktransport = new WebSocketTransport("ws://"+ window.location.host + "/ws/api"); +const manager = new RequestManager([httpTransport], () => crypto.randomUUID()); const client = new Client(manager); let deAuthenticatedCallback; @@ -30,23 +31,31 @@ export async function authenticate(username: string, password: string): Promise< } } +export async function logout(): Promise { + const pResponse = axios.post("/logout", null, {timeout: 10100}); + try { + const response = await pResponse; + return { data: response.data, error: null}; + } catch (error) { + return { data: null, error: error}; + } +} + export async function checkAuthentication() { - const res = await apiCall("session-check", {}); - if (res.error == "HTTP: Your Session cookie is invalid") return {auth: 0, error: null}; - if (res.error == "HTTP: Your Session Requires TFA") return {auth: 1, error: null}; - else if (res.error) return {auth: 0, error: res.error}; - else { - /* TODO add commit_hash storing + const pResponse = axios.post("/session", null, {timeout: 10100}); + try { + const response = await pResponse; const last_hash = window.localStorage.getItem("commit_hash"); if (last_hash) { - if (last_hash !== res.data.commit_hash) { + if (last_hash !== response.data.commit_hash) { console.log("Detected New Backend Version, Reloading..."); window.localStorage.removeItem("commit_hash"); - window.location.reload(true); + window.location.reload(); } - } else window.localStorage.setItem("commit_hash", res.data.commit_hash); - */ + } else window.localStorage.setItem("commit_hash", response.data.commit_hash); return {auth: 2, error: null}; + } catch (error) { + return {auth: 0, error: error}; } } \ No newline at end of file diff --git a/client/vite.config.ts b/client/vite.config.ts index fc00a89..846c521 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -18,6 +18,9 @@ export default defineConfig({ server: { "proxy": { "/api": "http://localhost:8080", + "/login": "http://localhost:8080", + "/logout": "http://localhost:8080", + "/session": "http://localhost:8080", "/ws": { target: "ws://localhost:8080", ws: true, diff --git a/pkg/server/session.go b/pkg/server/session.go index 162a2e7..908174f 100644 --- a/pkg/server/session.go +++ b/pkg/server/session.go @@ -1,11 +1,15 @@ package server import ( + "encoding/json" + "io" "net/http" + "runtime/debug" "sync" "time" "github.com/google/uuid" + "golang.org/x/exp/slog" ) type SessionKeyType string @@ -19,9 +23,29 @@ type Session struct { // TODO Add []websocket.Conn pointer to close all active websockets, alternativly do this via context cancelation } +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type SessionResponse struct { + CommitHash string `json:"commit_hash"` +} + var sessionsSync sync.Mutex var sessions map[string]*Session = map[string]*Session{} +var CommitHash = func() string { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + return setting.Value + } + } + } + return "asd" +}() + func GetSession(r *http.Request) (string, *Session) { c, err := r.Cookie("session") if err != nil { @@ -69,18 +93,28 @@ func CleanupSessions(stop chan struct{}) { } func HandleLogin(w http.ResponseWriter, r *http.Request) { - username := r.PostFormValue("username") - password := r.PostFormValue("password") - if username == "admin" && password == "12345" { - GenerateSession(w, username) + buf, err := io.ReadAll(r.Body) + if err != nil { + slog.Error("Reading Body", err) + return + } + var req LoginRequest + err = json.Unmarshal(buf, &req) + if err != nil { + slog.Error("Unmarshal", err) + return + } + if req.Username == "admin" && req.Password == "12345" { + slog.Info("User Login Successfull") + GenerateSession(w, req.Username) w.WriteHeader(http.StatusOK) - http.Redirect(w, r, "/", http.StatusFound) + return } w.WriteHeader(http.StatusUnauthorized) } func HandleLogout(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{Name: SessionCookieName, Value: "", Expires: time.Now()}) + http.SetCookie(w, &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: "", Expires: time.Now()}) w.WriteHeader(http.StatusOK) } @@ -97,4 +131,14 @@ func HandleSession(w http.ResponseWriter, r *http.Request) { } http.SetCookie(w, &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: s.Expires}) w.WriteHeader(http.StatusOK) + resp := SessionResponse{ + CommitHash: CommitHash, + } + res, err := json.Marshal(resp) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(res) }