mirror of https://github.com/velour/catbase.git
secrets: add plugin
Also modify rest to use secrets plugin
This commit is contained in:
parent
0cd1b5e7ed
commit
a1c5a4c580
|
@ -23,6 +23,15 @@ type Config struct {
|
|||
*sqlx.DB
|
||||
|
||||
DBFile string
|
||||
secrets map[string]Secret
|
||||
}
|
||||
|
||||
// Secret is a config value that is loaded permanently and not ever displayed
|
||||
type Secret struct {
|
||||
// Key is the key field of the table
|
||||
Key string `db:"key"`
|
||||
// Value represents the secret that must not be shared
|
||||
Value string `db:"value"`
|
||||
}
|
||||
|
||||
// GetFloat64 returns the config value for a string key
|
||||
|
@ -85,6 +94,9 @@ func (c *Config) GetString(key, fallback string) string {
|
|||
if v, found := os.LookupEnv(envkey(key)); found {
|
||||
return v
|
||||
}
|
||||
if v, found := c.secrets[key]; found {
|
||||
return v.Value
|
||||
}
|
||||
var configValue string
|
||||
q := `select value from config where key=?`
|
||||
err := c.DB.Get(&configValue, q, key)
|
||||
|
@ -162,6 +174,33 @@ func (c *Config) Set(key, value string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) RefreshSecrets() error {
|
||||
q := `select key, value from secrets`
|
||||
var secrets []Secret
|
||||
err := c.Select(&secrets, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secretMap := map[string]Secret{}
|
||||
for _, s := range secrets {
|
||||
secretMap[s.Key] = s
|
||||
}
|
||||
c.secrets = secretMap
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) GetAllSecrets() map[string]Secret {
|
||||
return c.secrets
|
||||
}
|
||||
|
||||
func (c *Config) SecretKeys() []string {
|
||||
keys := []string{}
|
||||
for k := range c.secrets {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (c *Config) SetMap(key string, values map[string]string) error {
|
||||
b, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
|
@ -200,6 +239,7 @@ func ReadConfig(dbpath string) *Config {
|
|||
}
|
||||
c := Config{
|
||||
DBFile: dbpath,
|
||||
secrets: map[string]Secret{},
|
||||
}
|
||||
c.DB = sqlDB
|
||||
|
||||
|
@ -208,7 +248,19 @@ func ReadConfig(dbpath string) *Config {
|
|||
value string,
|
||||
primary key (key)
|
||||
);`); err != nil {
|
||||
panic(err)
|
||||
log.Fatal().Err(err).Msgf("failed to initialize config")
|
||||
}
|
||||
|
||||
if _, err := c.Exec(`create table if not exists secrets (
|
||||
key string,
|
||||
value string,
|
||||
primary key (key)
|
||||
);`); err != nil {
|
||||
log.Fatal().Err(err).Msgf("failed to initialize config")
|
||||
}
|
||||
|
||||
if err := c.RefreshSecrets(); err != nil {
|
||||
log.Fatal().Err(err).Msgf("failed to initialize config")
|
||||
}
|
||||
|
||||
log.Info().Msgf("catbase is running.")
|
||||
|
|
2
main.go
2
main.go
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/velour/catbase/plugins/giphy"
|
||||
"github.com/velour/catbase/plugins/last"
|
||||
"github.com/velour/catbase/plugins/rest"
|
||||
"github.com/velour/catbase/plugins/secrets"
|
||||
|
||||
"github.com/velour/catbase/plugins/achievements"
|
||||
"github.com/velour/catbase/plugins/aoc"
|
||||
|
@ -121,6 +122,7 @@ func main() {
|
|||
b := bot.New(c, client)
|
||||
|
||||
b.AddPlugin(admin.New(b))
|
||||
b.AddPlugin(secrets.New(b))
|
||||
b.AddPlugin(giphy.New(b))
|
||||
b.AddPlugin(emojifyme.New(b))
|
||||
b.AddPlugin(last.New(b))
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"text/template"
|
||||
|
||||
"github.com/itchyny/gojq"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/velour/catbase/bot"
|
||||
|
@ -235,9 +236,14 @@ func (p *RestPlugin) mkHandler(w *wire) bot.ResponseHandler {
|
|||
return false
|
||||
}
|
||||
values := bot.RegexValues{}
|
||||
for _, s := range p.b.Config().GetAllSecrets() {
|
||||
values[s.Key] = s.Value
|
||||
}
|
||||
log.Debug().Interface("values", values).Msgf("secrets")
|
||||
for k := range r.Values {
|
||||
values[k] = url.QueryEscape(r.Values[k])
|
||||
}
|
||||
log.Debug().Interface("values", values).Msgf("r.Values")
|
||||
urlStr := w.URL.String()
|
||||
parse, err := template.New(urlStr).Parse(urlStr)
|
||||
if p.handleErr(err, r) {
|
||||
|
@ -249,6 +255,10 @@ func (p *RestPlugin) mkHandler(w *wire) bot.ResponseHandler {
|
|||
return true
|
||||
}
|
||||
newURL, err := url.Parse(buf.String())
|
||||
log.Debug().
|
||||
Interface("values", values).
|
||||
Str("URL", buf.String()).
|
||||
Msg("Querying URL with values")
|
||||
if p.handleErr(err, r) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
package secrets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/velour/catbase/bot"
|
||||
"github.com/velour/catbase/config"
|
||||
)
|
||||
|
||||
type SecretsPlugin struct {
|
||||
b bot.Bot
|
||||
c *config.Config
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func New(b bot.Bot) *SecretsPlugin {
|
||||
p := &SecretsPlugin{
|
||||
b: b,
|
||||
c: b.Config(),
|
||||
db: b.DB(),
|
||||
}
|
||||
p.registerWeb()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *SecretsPlugin) registerWeb() {
|
||||
http.HandleFunc("/secrets/add", p.handleRegister)
|
||||
http.HandleFunc("/secrets/remove", p.handleRemove)
|
||||
http.HandleFunc("/secrets/all", p.handleAll)
|
||||
http.HandleFunc("/secrets/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
value := r.URL.Query().Get("test")
|
||||
j, _ := json.Marshal(map[string]string{"value": value})
|
||||
w.Write(j)
|
||||
})
|
||||
http.HandleFunc("/secrets", p.handleIndex)
|
||||
p.b.RegisterWeb("/secrets", "Secrets")
|
||||
}
|
||||
|
||||
func (p *SecretsPlugin) registerSecret(key, value string) error {
|
||||
q := `insert into secrets (key, value) values (?, ?)`
|
||||
_, err := p.db.Exec(q, key, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.c.RefreshSecrets()
|
||||
}
|
||||
|
||||
func (p *SecretsPlugin) removeSecret(key string) error {
|
||||
q := `delete from secrets where key=?`
|
||||
_, err := p.db.Exec(q, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.c.RefreshSecrets()
|
||||
}
|
||||
|
||||
func (p *SecretsPlugin) updateSecret(key, value string) error {
|
||||
q := `update secrets set value=? where key=?)`
|
||||
_, err := p.db.Exec(q, value, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.c.RefreshSecrets()
|
||||
}
|
||||
|
||||
func mkCheckError(w http.ResponseWriter) func(error) bool {
|
||||
return func(err error) bool {
|
||||
if err != nil {
|
||||
log.Error().Stack().Err(err).Msgf("secret failed")
|
||||
w.WriteHeader(500)
|
||||
e, _ := json.Marshal(err)
|
||||
w.Write(e)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func checkMethod(method string, w http.ResponseWriter, r *http.Request) bool {
|
||||
if r.Method != method {
|
||||
w.WriteHeader(405)
|
||||
fmt.Fprintf(w, "Incorrect HTTP method")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *SecretsPlugin) sendKeys(w http.ResponseWriter, r *http.Request) {
|
||||
checkError := mkCheckError(w)
|
||||
log.Debug().Msgf("Keys before refresh: %v", p.c.SecretKeys())
|
||||
err := p.c.RefreshSecrets()
|
||||
log.Debug().Msgf("Keys after refresh: %v", p.c.SecretKeys())
|
||||
if checkError(err) {
|
||||
return
|
||||
}
|
||||
keys, err := json.Marshal(p.c.SecretKeys())
|
||||
if checkError(err) {
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.Write(keys)
|
||||
}
|
||||
|
||||
func (p *SecretsPlugin) handleAll(w http.ResponseWriter, r *http.Request) {
|
||||
p.sendKeys(w, r)
|
||||
}
|
||||
|
||||
func (p *SecretsPlugin) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug().Msgf("handleRegister")
|
||||
if checkMethod(http.MethodPost, w, r) {
|
||||
log.Debug().Msgf("failed post %s", r.Method)
|
||||
return
|
||||
}
|
||||
checkError := mkCheckError(w)
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
secret := config.Secret{}
|
||||
err := decoder.Decode(&secret)
|
||||
log.Debug().Msgf("decoding: %s", err)
|
||||
if checkError(err) {
|
||||
return
|
||||
}
|
||||
log.Debug().Msgf("Secret: %s", secret)
|
||||
err = p.registerSecret(secret.Key, secret.Value)
|
||||
if checkError(err) {
|
||||
return
|
||||
}
|
||||
p.sendKeys(w, r)
|
||||
}
|
||||
|
||||
func (p *SecretsPlugin) handleRemove(w http.ResponseWriter, r *http.Request) {
|
||||
if checkMethod(http.MethodDelete, w, r) {
|
||||
return
|
||||
}
|
||||
checkError := mkCheckError(w)
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
secret := config.Secret{}
|
||||
err := decoder.Decode(&secret)
|
||||
if checkError(err) {
|
||||
return
|
||||
}
|
||||
err = p.removeSecret(secret.Key)
|
||||
if checkError(err) {
|
||||
return
|
||||
}
|
||||
p.sendKeys(w, r)
|
||||
}
|
||||
|
||||
func (p *SecretsPlugin) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(indexTpl))
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package secrets
|
||||
|
||||
var indexTpl = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Load required Bootstrap and BootstrapVue CSS -->
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css">
|
||||
<link type="text/css" rel="stylesheet" href="//cdn.jsdelivr.net/npm/bootstrap-vue@2.21.2/dist/bootstrap-vue.min.css"/>
|
||||
|
||||
<!-- Load polyfills to support older browsers -->
|
||||
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
|
||||
|
||||
<!-- Load Vue followed by BootstrapVue -->
|
||||
<script src="//cdn.jsdelivr.net/npm/vue"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/bootstrap-vue@2.21.2/dist/bootstrap-vue.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/bootstrap-vue@2.21.2/dist/bootstrap-vue-icons.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/vue-router@3.5.1/dist/vue-router.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/axios@0.21.1/dist/axios.min.js"></script>
|
||||
<meta charset="UTF-8">
|
||||
<title>Memes</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<b-navbar>
|
||||
<b-navbar-brand>Memes</b-navbar-brand>
|
||||
<b-navbar-nav>
|
||||
<b-nav-item v-for="item in nav" :href="item.url" :active="item.name === 'Meme'" :key="item.key">{{ item.name }}</b-nav-item>
|
||||
</b-navbar-nav>
|
||||
</b-navbar>
|
||||
<b-alert
|
||||
dismissable
|
||||
variant="error"
|
||||
:show="err != ''"
|
||||
@dismissed="err = ''">
|
||||
{{ err }}
|
||||
</b-alert>
|
||||
<b-form @submit="add">
|
||||
<b-container>
|
||||
<b-row>
|
||||
<b-col cols="3">
|
||||
<b-input placeholder="Key..." v-model="secret.key"></b-input>
|
||||
</b-col>
|
||||
<b-col cols="3">
|
||||
<b-input placeholder="Value..." v-model="secret.value"></b-input>
|
||||
</b-col>
|
||||
<b-col cols="3">
|
||||
<b-button type="submit">Add Secret</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row style="padding-top: 2em;">
|
||||
<b-col>
|
||||
<ul>
|
||||
<li v-for="key in results" key="key"><a @click="rm(key)" href="#">X</a> {{key}}</li>
|
||||
</ul>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</b-form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes: []
|
||||
});
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
router,
|
||||
data: {
|
||||
err: '',
|
||||
nav: [],
|
||||
secret: {key: '', value: ''},
|
||||
results: [],
|
||||
fields: [
|
||||
{key: 'key', sortable: true},
|
||||
]
|
||||
},
|
||||
mounted() {
|
||||
axios.get('/nav')
|
||||
.then(resp => {
|
||||
this.nav = resp.data;
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
this.refresh();
|
||||
},
|
||||
methods: {
|
||||
refresh: function () {
|
||||
axios.get('/secrets/all')
|
||||
.then(resp => {
|
||||
this.results = resp.data
|
||||
this.err = ''
|
||||
})
|
||||
.catch(err => (this.err = err))
|
||||
},
|
||||
add: function (evt) {
|
||||
if (evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
axios.post('/secrets/add', this.secret)
|
||||
.then(resp => {
|
||||
this.results = resp.data;
|
||||
this.secret.key = '';
|
||||
this.secret.value = '';
|
||||
this.refresh();
|
||||
})
|
||||
.catch(err => this.err = err)
|
||||
},
|
||||
rm: function (key) {
|
||||
if (confirm("Are you sure you want to delete this meme?")) {
|
||||
axios.delete('/secrets/remove', {data: {key: key}})
|
||||
.then(resp => {
|
||||
this.refresh();
|
||||
})
|
||||
.catch(err => this.err = err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
Loading…
Reference in New Issue