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
|
*sqlx.DB
|
||||||
|
|
||||||
DBFile string
|
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
|
// 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 {
|
if v, found := os.LookupEnv(envkey(key)); found {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
if v, found := c.secrets[key]; found {
|
||||||
|
return v.Value
|
||||||
|
}
|
||||||
var configValue string
|
var configValue string
|
||||||
q := `select value from config where key=?`
|
q := `select value from config where key=?`
|
||||||
err := c.DB.Get(&configValue, q, key)
|
err := c.DB.Get(&configValue, q, key)
|
||||||
|
@ -162,6 +174,33 @@ func (c *Config) Set(key, value string) error {
|
||||||
return nil
|
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 {
|
func (c *Config) SetMap(key string, values map[string]string) error {
|
||||||
b, err := json.Marshal(values)
|
b, err := json.Marshal(values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -200,6 +239,7 @@ func ReadConfig(dbpath string) *Config {
|
||||||
}
|
}
|
||||||
c := Config{
|
c := Config{
|
||||||
DBFile: dbpath,
|
DBFile: dbpath,
|
||||||
|
secrets: map[string]Secret{},
|
||||||
}
|
}
|
||||||
c.DB = sqlDB
|
c.DB = sqlDB
|
||||||
|
|
||||||
|
@ -208,7 +248,19 @@ func ReadConfig(dbpath string) *Config {
|
||||||
value string,
|
value string,
|
||||||
primary key (key)
|
primary key (key)
|
||||||
);`); err != nil {
|
);`); 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.")
|
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/giphy"
|
||||||
"github.com/velour/catbase/plugins/last"
|
"github.com/velour/catbase/plugins/last"
|
||||||
"github.com/velour/catbase/plugins/rest"
|
"github.com/velour/catbase/plugins/rest"
|
||||||
|
"github.com/velour/catbase/plugins/secrets"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/achievements"
|
"github.com/velour/catbase/plugins/achievements"
|
||||||
"github.com/velour/catbase/plugins/aoc"
|
"github.com/velour/catbase/plugins/aoc"
|
||||||
|
@ -121,6 +122,7 @@ func main() {
|
||||||
b := bot.New(c, client)
|
b := bot.New(c, client)
|
||||||
|
|
||||||
b.AddPlugin(admin.New(b))
|
b.AddPlugin(admin.New(b))
|
||||||
|
b.AddPlugin(secrets.New(b))
|
||||||
b.AddPlugin(giphy.New(b))
|
b.AddPlugin(giphy.New(b))
|
||||||
b.AddPlugin(emojifyme.New(b))
|
b.AddPlugin(emojifyme.New(b))
|
||||||
b.AddPlugin(last.New(b))
|
b.AddPlugin(last.New(b))
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/itchyny/gojq"
|
"github.com/itchyny/gojq"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
|
@ -235,9 +236,14 @@ func (p *RestPlugin) mkHandler(w *wire) bot.ResponseHandler {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
values := bot.RegexValues{}
|
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 {
|
for k := range r.Values {
|
||||||
values[k] = url.QueryEscape(r.Values[k])
|
values[k] = url.QueryEscape(r.Values[k])
|
||||||
}
|
}
|
||||||
|
log.Debug().Interface("values", values).Msgf("r.Values")
|
||||||
urlStr := w.URL.String()
|
urlStr := w.URL.String()
|
||||||
parse, err := template.New(urlStr).Parse(urlStr)
|
parse, err := template.New(urlStr).Parse(urlStr)
|
||||||
if p.handleErr(err, r) {
|
if p.handleErr(err, r) {
|
||||||
|
@ -249,6 +255,10 @@ func (p *RestPlugin) mkHandler(w *wire) bot.ResponseHandler {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
newURL, err := url.Parse(buf.String())
|
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) {
|
if p.handleErr(err, r) {
|
||||||
return true
|
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