mirror of
https://github.com/Kugelschieber/migo.git
synced 2026-01-17 22:30:29 +00:00
Basic login.
This commit is contained in:
131
api/auth.go
Normal file
131
api/auth.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
jwtAuth *jwtauth.JWTAuth
|
||||
)
|
||||
|
||||
// GetJWTAuth returns the JWT authorizer.
|
||||
func GetJWTAuth() *jwtauth.JWTAuth {
|
||||
return jwtAuth
|
||||
}
|
||||
|
||||
// InitJWT initializes JWT authentication.
|
||||
func InitJWT() {
|
||||
generateRSAKeys()
|
||||
jwtAuth = jwtauth.New("RS256", loadRSAPrivateKey(), loadRSAPublicKey())
|
||||
}
|
||||
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
req := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}{}
|
||||
|
||||
if err := decodeJSON(w, r, &req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
t, tokenString, _ := jwtAuth.Encode(map[string]interface{}{"username": req.Username})
|
||||
encodeJSON(w, struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}{
|
||||
tokenString,
|
||||
t.Expiration(),
|
||||
})
|
||||
}
|
||||
|
||||
func generateRSAKeys() {
|
||||
err := os.Mkdir("secrets", 0755)
|
||||
|
||||
if os.IsExist(err) {
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Fatalf("Error creating secrets directory: %v", err)
|
||||
}
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error generating RSA key: %v", err)
|
||||
}
|
||||
|
||||
pub := key.Public()
|
||||
keyPEM := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
},
|
||||
)
|
||||
pubPEM := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: x509.MarshalPKCS1PublicKey(pub.(*rsa.PublicKey)),
|
||||
},
|
||||
)
|
||||
|
||||
if err := os.WriteFile("secrets/jwt.rsa", keyPEM, 0700); err != nil {
|
||||
log.Fatalf("Error writing private RSA key: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile("secrets/jwt.rsa.pub", pubPEM, 0755); err != nil {
|
||||
log.Fatalf("Error writing public RSA key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadRSAPublicKey() *rsa.PublicKey {
|
||||
data, err := os.ReadFile("secrets/jwt.rsa.pub")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading RSA key: %v", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
|
||||
if block == nil {
|
||||
log.Fatalf("Error decoding RSA key: %v", err)
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKCS1PublicKey(block.Bytes)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing RSA key: %v", err)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func loadRSAPrivateKey() *rsa.PrivateKey {
|
||||
data, err := os.ReadFile("secrets/jwt.rsa")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading RSA key: %v", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
|
||||
if block == nil {
|
||||
log.Fatalf("Error decoding RSA key: %v", err)
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing RSA key: %v", err)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func DebugHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func Debug(w http.ResponseWriter, r *http.Request) {
|
||||
_, claims, err := jwtauth.FromContext(r.Context())
|
||||
|
||||
if err != nil {
|
||||
|
||||
42
api/util.go
Normal file
42
api/util.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type apiError struct {
|
||||
Validation map[string]string `json:"validation"`
|
||||
Err []error `json:"error"`
|
||||
}
|
||||
|
||||
func decodeJSON(w http.ResponseWriter, r *http.Request, req any) error {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeJSON(w http.ResponseWriter, resp any) {
|
||||
respBody, err := json.Marshal(resp)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("Error marshalling response: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(respBody); err != nil && err != syscall.EPIPE {
|
||||
log.Printf("Error sending response: %v", err)
|
||||
}
|
||||
}
|
||||
125
cmd/admin/package-lock.json
generated
125
cmd/admin/package-lock.json
generated
@@ -8,7 +8,10 @@
|
||||
"name": "migo",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"pinia": "^2.1.3",
|
||||
"qs": "^6.11.2",
|
||||
"sass": "^1.63.6",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.2"
|
||||
@@ -1103,6 +1106,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
|
||||
@@ -1115,6 +1123,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -1160,7 +1178,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
@@ -1249,6 +1266,17 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -1331,6 +1359,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -1817,6 +1853,25 @@
|
||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
@@ -1826,6 +1881,19 @@
|
||||
"is-callable": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -1848,8 +1916,7 @@
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/function.prototype.name": {
|
||||
"version": "1.1.5",
|
||||
@@ -1882,7 +1949,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
@@ -2019,7 +2085,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
@@ -2061,7 +2126,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -2073,7 +2137,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -2442,6 +2505,14 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
@@ -2581,6 +2652,25 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -2848,7 +2938,6 @@
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -3139,6 +3228,11 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
@@ -3148,6 +3242,20 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
|
||||
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -3409,7 +3517,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"pinia": "^2.1.3",
|
||||
"qs": "^6.11.2",
|
||||
"sass": "^1.63.6",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.2"
|
||||
|
||||
@@ -1,4 +1,51 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border-width: 0;
|
||||
padding: 10px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #767676;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
border: 1px solid #ed6673;
|
||||
border-radius: 3px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
input[type~="submit"], button {
|
||||
width: auto;
|
||||
border-width: 0;
|
||||
background: #ed6673;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.login {
|
||||
max-width: 350px;
|
||||
margin: 200px auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #c5c5c5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
8
cmd/admin/src/repositories/APIError.ts
Normal file
8
cmd/admin/src/repositories/APIError.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface Validation {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export interface APIError {
|
||||
validation: Validation
|
||||
error: string[]
|
||||
}
|
||||
85
cmd/admin/src/repositories/Repository.ts
Normal file
85
cmd/admin/src/repositories/Repository.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import {AxiosError, AxiosInstance} from "axios";
|
||||
import type {APIError} from "@/repositories/APIError";
|
||||
import axios from "@/repositories/axios";
|
||||
|
||||
export class Repository {
|
||||
protected axios: AxiosInstance = axios;
|
||||
private controller: Map<string, AbortController> = new Map;
|
||||
|
||||
protected async performGet<T>(endpoint: string, params: unknown, defaultValue: T, disableCancellation?: boolean): Promise<T | APIError> {
|
||||
try {
|
||||
let controller = null;
|
||||
|
||||
if (!disableCancellation) {
|
||||
controller = this.cancel(endpoint);
|
||||
}
|
||||
|
||||
const resp = await this.axios.get<T>(endpoint, {
|
||||
params,
|
||||
signal: controller?.signal
|
||||
});
|
||||
return Promise.resolve<T>(resp.data || defaultValue);
|
||||
} catch (e) {
|
||||
return this.handleException<T>(e, defaultValue);
|
||||
} finally {
|
||||
if (!disableCancellation) {
|
||||
this.clearCancel(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async performPost<T>(endpoint: string, data: unknown): Promise<T | APIError> {
|
||||
try {
|
||||
const resp = await this.axios.post<T>(endpoint, data);
|
||||
return Promise.resolve<T>(resp.data);
|
||||
} catch (e) {
|
||||
const err = e as AxiosError;
|
||||
return Promise.reject<APIError>(err);
|
||||
}
|
||||
}
|
||||
|
||||
protected async performPut<T>(endpoint: string, data: unknown): Promise<T | APIError> {
|
||||
try {
|
||||
const resp = await this.axios.put<T>(endpoint, data);
|
||||
return Promise.resolve<T>(resp.data);
|
||||
} catch (e) {
|
||||
const err = e as AxiosError;
|
||||
return Promise.reject<APIError>(err);
|
||||
}
|
||||
}
|
||||
|
||||
protected async performDelete(endpoint: string, params: unknown): Promise<APIError | null> {
|
||||
try {
|
||||
await this.axios.delete<null>(endpoint, {params});
|
||||
return Promise.resolve<null>(null);
|
||||
} catch (e) {
|
||||
return Promise.reject<APIError>(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected handleException<T>(e: unknown, defaultValue: T): Promise<T | APIError> {
|
||||
const err = e as AxiosError;
|
||||
|
||||
if (err.code && err.code === "ERR_CANCELED") {
|
||||
return Promise.resolve<T>(defaultValue);
|
||||
}
|
||||
|
||||
return Promise.reject<APIError>(err);
|
||||
}
|
||||
|
||||
private cancel(name: string): AbortController {
|
||||
const controller = this.controller.get(name);
|
||||
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
}
|
||||
|
||||
const newController = new AbortController();
|
||||
this.controller.set(name, newController);
|
||||
return newController;
|
||||
}
|
||||
|
||||
private clearCancel(name: string) {
|
||||
this.controller.delete(name);
|
||||
}
|
||||
}
|
||||
13
cmd/admin/src/repositories/UserRepository.ts
Normal file
13
cmd/admin/src/repositories/UserRepository.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {Repository} from "@/repositories/Repository";
|
||||
import type {APIError} from "@/repositories/APIError";
|
||||
|
||||
export interface TokenResponse {
|
||||
access_token: string
|
||||
expires_at: Date
|
||||
}
|
||||
|
||||
export const UserRepository = new class extends Repository {
|
||||
async login(username: string, password: string): Promise<TokenResponse | APIError> {
|
||||
return this.performPost<TokenResponse>("/login", {username, password});
|
||||
}
|
||||
}
|
||||
74
cmd/admin/src/repositories/axios.ts
Normal file
74
cmd/admin/src/repositories/axios.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import axios, {AxiosResponse} from "axios";
|
||||
import Cookies from "js-cookie";
|
||||
import qs from "qs";
|
||||
import {APIError} from "@/repositories/APIError";
|
||||
|
||||
let refresh: Promise<void | AxiosResponse> | undefined;
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: "/api/v1",
|
||||
timeout: 10000,
|
||||
paramsSerializer: {
|
||||
serialize: params => qs.stringify(params, {arrayFormat: "repeat"})
|
||||
}
|
||||
});
|
||||
|
||||
instance.interceptors.request.use(config => {
|
||||
const accessToken = Cookies.get("access_token");
|
||||
|
||||
if (accessToken && config && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
return Promise.resolve(config);
|
||||
}, err => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(resp => {
|
||||
return resp;
|
||||
}, err => {
|
||||
if (err.code && err.code === "ERR_CANCELED") {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
if (err.response.status === 401 && err.request.path !== "/refresh") {
|
||||
return refreshToken().then(() => {
|
||||
return instance.request(err.config);
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
//removeCookies(); TODO
|
||||
localStorage.clear();
|
||||
|
||||
if (e.request.path !== "/login") {
|
||||
location.href = "/login";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(err.response.data as APIError);
|
||||
});
|
||||
|
||||
async function refreshToken() {
|
||||
if (refresh) {
|
||||
return refresh;
|
||||
}
|
||||
|
||||
const refresh_token = Cookies.get("refresh_token");
|
||||
refresh = instance.get("/refresh", {params: {refresh_token}}).then(({data: {access_token}}) => {
|
||||
const now = new Date();
|
||||
const expires = Date.UTC(now.getUTCFullYear()+1, now.getUTCMonth(), now.getUTCDate());
|
||||
Cookies.set("access_token", access_token, {
|
||||
expires: new Date(expires),
|
||||
secure: true,
|
||||
domain: "localhost", //getCookieDomain(), TODO
|
||||
path: "/",
|
||||
sameSite: "strict"
|
||||
});
|
||||
refresh = undefined;
|
||||
return Promise.resolve();
|
||||
});
|
||||
return refresh;
|
||||
}
|
||||
|
||||
export default instance;
|
||||
@@ -1,9 +1,44 @@
|
||||
<template>
|
||||
<h1>Sign In</h1>
|
||||
<div class="login">
|
||||
<h1>Sign In</h1>
|
||||
<form v-on:submit.prevent="login">
|
||||
<fieldset>
|
||||
<label for="username">Username</label>
|
||||
<input type="text" name="username" id="username" v-model="username" />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password" v-model="password" />
|
||||
</fieldset>
|
||||
<input type="submit" value="Sign In" />
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import {defineComponent, ref} from "vue";
|
||||
import {UserRepository} from "@/repositories/UserRepository";
|
||||
|
||||
export default defineComponent({});
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
|
||||
async function login() {
|
||||
try {
|
||||
const token = await UserRepository.login(username.value, password.value);
|
||||
console.log(token);
|
||||
} catch (e) {
|
||||
// TODO
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
login
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
106
cmd/main.go
106
cmd/main.go
@@ -2,11 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"embed"
|
||||
"encoding/pem"
|
||||
"github.com/Kugelschieber/migo/api"
|
||||
"github.com/Kugelschieber/migo/db"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -30,106 +26,21 @@ var (
|
||||
|
||||
//go:embed admin/dist/assets
|
||||
assets embed.FS
|
||||
|
||||
jwtAuth *jwtauth.JWTAuth
|
||||
)
|
||||
|
||||
func init() {
|
||||
generateRSAKeys()
|
||||
|
||||
/*jwtAuth = jwtauth.New("RS256", loadRSAPrivateKey(), loadRSAPublicKey())
|
||||
_, tokenString, err := jwtAuth.Encode(map[string]interface{}{"test": 42})*/
|
||||
}
|
||||
|
||||
func generateRSAKeys() {
|
||||
err := os.Mkdir("secrets", 0755)
|
||||
|
||||
if os.IsExist(err) {
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Fatalf("Error creating secrets directory: %v", err)
|
||||
}
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error generating RSA key: %v", err)
|
||||
}
|
||||
|
||||
pub := key.Public()
|
||||
keyPEM := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
},
|
||||
)
|
||||
pubPEM := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: x509.MarshalPKCS1PublicKey(pub.(*rsa.PublicKey)),
|
||||
},
|
||||
)
|
||||
|
||||
if err := os.WriteFile("secrets/jwt.rsa", keyPEM, 0700); err != nil {
|
||||
log.Fatalf("Error writing private RSA key: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile("secrets/jwt.rsa.pub", pubPEM, 0755); err != nil {
|
||||
log.Fatalf("Error writing public RSA key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadRSAPublicKey() *rsa.PublicKey {
|
||||
data, err := os.ReadFile("secrets/jwt.rsa.pub")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading RSA key: %v", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
|
||||
if block == nil {
|
||||
log.Fatalf("Error decoding RSA key: %v", err)
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKCS1PublicKey(block.Bytes)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing RSA key: %v", err)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func loadRSAPrivateKey() *rsa.PrivateKey {
|
||||
data, err := os.ReadFile("secrets/jwt.rsa")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading RSA key: %v", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
|
||||
if block == nil {
|
||||
log.Fatalf("Error decoding RSA key: %v", err)
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing RSA key: %v", err)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := db.Init(); err != nil {
|
||||
log.Fatalf("Error initializing database: %v", err)
|
||||
}
|
||||
|
||||
defer db.Close()
|
||||
api.InitJWT()
|
||||
dev := os.Getenv("MIGO_DEV") != ""
|
||||
|
||||
if dev {
|
||||
log.Println("Running in development mode")
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.Recoverer)
|
||||
router.Use(middleware.Compress(5))
|
||||
@@ -140,11 +51,12 @@ func main() {
|
||||
AllowCredentials: true,
|
||||
MaxAge: 86400,
|
||||
}))
|
||||
router.Post("/api/v1/login", api.Login)
|
||||
router.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(jwtAuth))
|
||||
r.Use(jwtauth.Verifier(api.GetJWTAuth()))
|
||||
r.Use(jwtauth.Authenticator)
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
r.Get("/debug", api.DebugHandler)
|
||||
r.Get("/debug", api.Debug)
|
||||
})
|
||||
})
|
||||
router.Handle("/admin", http.RedirectHandler("/admin/", http.StatusFound))
|
||||
|
||||
Reference in New Issue
Block a user