PGP import feature

This commit is contained in:
nemunaire 2022-09-03 16:16:41 +02:00
parent 9436220685
commit de4bb43e86
12 changed files with 547 additions and 0 deletions

1
api.go
View File

@ -21,6 +21,7 @@ func declareAPIRoutes(router *gin.Engine) {
declareAPIAuthAsksRoutes(apiAuthRoutes)
declareAPIAuthQuestionsRoutes(apiAuthRoutes)
declareAPIAuthHelpRoutes(apiAuthRoutes)
declareAPIAuthKeysRoutes(apiAuthRoutes)
declareAPIAuthSurveysRoutes(apiAuthRoutes)
declareAPIAuthUsersRoutes(apiAuthRoutes)
declareAPIAuthWorksRoutes(apiAuthRoutes)

12
db.go
View File

@ -74,6 +74,18 @@ CREATE TABLE IF NOT EXISTS user_sessions(
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(id_user) REFERENCES users(id_user)
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
`); err != nil {
return err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS user_keys(
id_key INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
id_user INTEGER NOT NULL,
type ENUM('pgp', 'ssh') NOT NULL,
content TEXT NOT NULL,
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(id_user) REFERENCES users(id_user)
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
`); err != nil {
return err
}

1
go.mod
View File

@ -3,6 +3,7 @@ module git.nemunai.re/atsebay.t
go 1.16
require (
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895
github.com/coreos/go-oidc/v3 v3.2.0
github.com/gin-gonic/gin v1.7.7
github.com/go-sql-driver/mysql v1.6.0

7
go.sum
View File

@ -57,7 +57,10 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg=
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -65,6 +68,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@ -280,6 +285,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -448,6 +454,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

299
keys.go Normal file
View File

@ -0,0 +1,299 @@
package main
import (
"bytes"
"fmt"
"log"
"net/http"
"net/mail"
"strconv"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/gin-gonic/gin"
)
func declareAPIAuthKeysRoutes(router *gin.RouterGroup) {
router.GET("/keys", func(c *gin.Context) {
var u *User
if user, ok := c.Get("user"); ok {
u = user.(*User)
} else {
u = c.MustGet("LoggedUser").(*User)
}
keys, err := u.GetKeys()
if err != nil {
log.Println("Unable to GetKeys:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve your keys. Please try again in a few moment."})
return
}
var ret []int64
for _, key := range keys {
ret = append(ret, key.Id)
}
c.JSON(http.StatusOK, ret)
})
router.POST("/keys", func(c *gin.Context) {
var u *User
if user, ok := c.Get("user"); ok {
u = user.(*User)
} else {
u = c.MustGet("LoggedUser").(*User)
}
var key Key
if err := c.ShouldBindJSON(&key); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
k, err := u.NewKey(key.Type, key.Content)
if err != nil {
log.Println("Unable to NewKey:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register your public key. Please try again in a few moment."})
return
}
c.JSON(http.StatusOK, k)
})
keysRoutes := router.Group("/keys/:kid")
keysRoutes.Use(keyHandler)
keysRoutes.GET("", func(c *gin.Context) {
var u *User
if user, ok := c.Get("user"); ok {
u = user.(*User)
} else {
u = c.MustGet("LoggedUser").(*User)
}
k := c.MustGet("key").(*Key)
if err := k.ReadInfos(u); err != nil {
log.Println("Unable to ReadInfos:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read your public key. Please try again in a few moment."})
return
}
c.JSON(http.StatusOK, k)
})
keysRoutes.PUT("", func(c *gin.Context) {
current := c.MustGet("key").(*Key)
var new Key
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
new.Id = current.Id
u := c.MustGet("LoggedUser").(*User)
if new.IdUser != current.IdUser && !u.IsAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Operation not allowed."})
return
}
if key, err := new.Update(); err != nil {
log.Println("Unable to Update key:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during key updation: %s", err.Error())})
return
} else {
c.JSON(http.StatusOK, key)
}
})
keysRoutes.DELETE("", func(c *gin.Context) {
key := c.MustGet("key").(*Key)
if _, err := key.Delete(); err != nil {
log.Println("Unable to Delete key:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during key deletion: %s", err.Error())})
return
} else {
c.JSON(http.StatusOK, nil)
}
})
}
func keyHandler(c *gin.Context) {
var u *User
if user, ok := c.Get("user"); ok {
u = user.(*User)
} else {
u = c.MustGet("LoggedUser").(*User)
}
if kid, err := strconv.Atoi(string(c.Param("kid"))); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad key identifier."})
return
} else if u.IsAdmin {
if key, err := getKey(kid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Key not found."})
return
} else {
c.Set("key", key)
c.Next()
}
} else if key, err := u.getKey(kid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Key not found."})
return
} else {
c.Set("key", key)
c.Next()
}
}
type Key struct {
Id int64 `json:"id"`
IdUser int64 `json:"id_user"`
Type string `json:"type"`
Content string `json:"key,omitempty"`
Time time.Time `json:"time"`
Infos map[string]interface{} `json:"infos,omitempty"`
}
func (u *User) GetKeys() (keys []*Key, err error) {
if rows, errr := DBQuery("SELECT id_key, id_user, type, content, time FROM user_keys WHERE id_user=?", u.Id); errr != nil {
return nil, errr
} else {
defer rows.Close()
for rows.Next() {
var k Key
if err = rows.Scan(&k.Id, &k.IdUser, &k.Type, &k.Content, &k.Time); err != nil {
return
}
keys = append(keys, &k)
}
if err = rows.Err(); err != nil {
return
}
return
}
}
func getKey(id int) (k *Key, err error) {
k = new(Key)
err = DBQueryRow("SELECT id_key, id_user, type, content, time FROM user_keys WHERE id_key=?", id).Scan(&k.Id, &k.IdUser, &k.Type, &k.Content, &k.Time)
return
}
func (u *User) getKey(id int) (k *Key, err error) {
k = new(Key)
err = DBQueryRow("SELECT id_key, id_user, type, content, time FROM user_keys WHERE id_key=? AND id_user=?", id, u.Id).Scan(&k.Id, &k.IdUser, &k.Type, &k.Content, &k.Time)
return
}
func (u *User) NewKey(kind, content string) (*Key, error) {
if res, err := DBExec("INSERT INTO user_keys (id_user, type, content) VALUES (?, ?, ?)", u.Id, kind, content); err != nil {
return nil, err
} else if kid, err := res.LastInsertId(); err != nil {
return nil, err
} else {
return &Key{kid, u.Id, kind, content, time.Now(), nil}, nil
}
}
func (k *Key) CheckKey() error {
if k.Type == "pgp" {
keys, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(k.Content))
if err != nil {
return err
}
if len(keys) != 1 {
return fmt.Errorf("This is not a single public key file.")
}
if keys[0].PrivateKey != nil {
return fmt.Errorf("You send your PRIVATE key along with your public key. YOUR PRIVATE KEY IS COMPROMISED. Please revoke and regenerate a new key pair.")
}
if keys[0].Revocations != nil {
return fmt.Errorf("Your key seems to be revoked.")
}
return nil
} else {
return fmt.Errorf("%q is not a valid key type.", k.Type)
}
}
func (k *Key) ReadInfos(u *User) error {
if k.Type == "pgp" {
keys, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(k.Content))
if err != nil {
return err
}
k.Content = ""
k.Infos = map[string]interface{}{}
var std_identity *openpgp.Identity
for name, idt := range keys[0].Identities {
if idt.Revoked(time.Now()) {
continue
}
address, err := mail.ParseAddress(name)
if err != nil {
continue
}
if address.Address == u.Email {
std_identity = idt
break
}
}
if std_identity.UserId != nil {
k.Infos["identity"] = std_identity.UserId.Name
k.Infos["email"] = std_identity.UserId.Email
k.Infos["comment"] = std_identity.UserId.Comment
}
if std_identity.SelfSignature != nil {
k.Infos["keyid"] = fmt.Sprintf("%X", std_identity.SelfSignature.IssuerFingerprint)
k.Infos["sigexpired"] = std_identity.SelfSignature.SigExpired(time.Now())
k.Infos["creation"] = std_identity.SelfSignature.CreationTime
}
return nil
} else {
return fmt.Errorf("%q is not a valid key type.", k.Type)
}
}
func (k *Key) Update() (*Key, error) {
if _, err := DBExec("UPDATE user_keys SET id_user = ?, type = ?, content = ? WHERE id_key = ?", k.IdUser, k.Type, k.Content, k.Content, k.Id); err != nil {
return nil, err
} else {
return k, err
}
}
func (k Key) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM user_keys WHERE id_key = ?", k.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func ClearKeys() (int64, error) {
if res, err := DBExec("DELETE FROM user_keys"); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}

View File

@ -55,6 +55,7 @@ func declareStaticRoutes(router *gin.Engine) {
router.GET("/bug-bounty", serveOrReverse("/"))
router.GET("/grades", serveOrReverse("/"))
router.GET("/help", serveOrReverse("/"))
router.GET("/keys", serveOrReverse("/"))
router.GET("/surveys", serveOrReverse("/"))
router.GET("/surveys/*_", serveOrReverse("/"))
router.GET("/users", serveOrReverse("/"))

View File

@ -0,0 +1,52 @@
<script lang="ts">
import { getKeys, getKey, Key } from '../lib/key';
export let student = null;
</script>
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Informations</th>
</tr>
</thead>
<tbody>
{#await getKeys(student.id)}
<tr>
<td colspan="4">
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des clefs&hellip;
</div>
</td>
</tr>
{:then keys}
{#if keys && keys.length > 0}
{#each keys as keyid}
{#await getKey(keyid)}
Veuillez patienter
{:then key}
<tr>
<td>{key.id}</td>
<td>{key.type.toUpperCase()}</td>
<td>
<dl>
{#each Object.keys(key.infos) as k}
<dt class="float-start me-3 my-0 py-0">{k}</dt>
<dd>{#if key.infos[k]}{key.infos[k]}{:else}<span class="fst-italic">-</span>{/if}</dd>
{/each}
</dl>
</td>
</tr>
{/await}
{/each}
{:else}
<tr>
<td colspan="4" class="text-center fst-italic">Cet utilisateur n'a pas défini de clef</td>
</tr>
{/if}
{/await}
</tbody>
</table>

61
ui/src/lib/key.js Normal file
View File

@ -0,0 +1,61 @@
export class Key {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, id_user, type, key, time, infos }) {
this.id = id;
this.id_user = id_user;
this.type = type;
this.key = key;
this.time = time;
this.infos = infos;
}
async delete() {
const res = await fetch(`api/keys/${this.id}`, {
method: 'DELETE',
headers: {'Accept': 'application/json'}
});
if (res.status == 200) {
return true;
} else {
throw new Error((await res.json()).errmsg);
}
}
async save() {
const res = await fetch(this.id?`api/keys/${this.id}`:'api/keys', {
method: this.id?'PUT':'POST',
headers: {'Accept': 'application/json'},
body: JSON.stringify(this),
});
if (res.status == 200) {
const data = await res.json();
this.update(data);
return data;
} else {
throw new Error((await res.json()).errmsg);
}
}
}
export async function getKeys(userid) {
const res = await fetch(userid?`api/users/${userid}/keys`:`api/keys`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getKey(kid) {
const res = await fetch(`api/keys/${kid}`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return new Key(await res.json());
} else {
throw new Error((await res.json()).errmsg);
}
}

View File

@ -125,6 +125,7 @@
<img class="rounded-circle" src="//photos.cri.epita.fr/square/{$user.login}" alt="Menu" style="margin: -0.75em 0; max-height: 2.5em; border: 2px solid white;">
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" class:active={rroute === 'keys'} href="keys">Clef PGP</a></li>
<li><a class="dropdown-item" class:active={rroute === 'help'} href="help">Besoin d'aide&nbsp;?</a></li>
<li><a class="dropdown-item" class:active={rroute === 'bug-bounty'} href="bug-bounty">Bug Bounty</a></li>
<li><hr class="dropdown-divider"></li>

102
ui/src/routes/keys.svelte Normal file
View File

@ -0,0 +1,102 @@
<script>
import { getKeys, getKey, Key } from '../lib/key';
import { user } from '../stores/user';
import { ToastsStore } from '../stores/toasts';
let keysP = getKeys();
let mykey = "";
let holdSubmit = false;
async function submitPGPKey() {
holdSubmit = true;
let key = new Key({ type: 'pgp', key: mykey });
key.save().then(() => {
keysP = getKeys();
holdSubmit = false;
mykey = "";
ToastsStore.addToast({
msg: "Votre nouvelle clef a bien été enregistrée.",
color: "success",
title: "Clef PGP",
});
}, (error) => {
submitInProgress = false;
ToastsStore.addErrorToast({
msg: "Une erreur s'est produite durant l'envoi de votre clef : " + error + "\nVeuillez réessayer dans quelques instants.",
});
});
}
</script>
<h2>Ma clef PGP</h2>
<p class="lead">
Vos rendus doivent être signés avec votre clef PGP.
</p>
{#await keysP}
Veuillez patienter
{:then keys}
{#if keys && keys.length > 0}
<p>
Vous avez actuellement enregistré {#if keys.length > 1}les clefs publiques suivantes{:else}la clef publique suivante{/if}&nbsp;:
</p>
{#each keys as keyid}
{#await getKey(keyid)}
Veuillez patienter
{:then key}
<div class="alert alert-dark d-flex justify-content-between">
<div class="d-flex">
<div class="d-flex flex-column justify-content-center me-3">
<i class="bi bi-key-fill display-4"></i>
<div class="text-center badge bg-light" style="font-variant: small-caps;">
{key.type}
</div>
</div>
<div>
Adresse électronique&nbsp;: <strong class="badge bg-secondary">{key.infos.email}</strong><br>
Nom&nbsp;: <strong>{key.infos.identity}</strong> {#if key.infos.comment}<span class="fst-italic">({key.infos.identity})</span>{/if}<br>
Key ID&nbsp;: <strong>{key.infos.keyid}</strong><br>
Date de la signature&nbsp;: <strong>{key.infos.creation}</strong><br>
Clef expirée&nbsp;: <span class="badge" class:bg-danger={key.infos.sigexpired} class:bg-info={!key.infos.sigexpired}>{key.infos.sigexpired?"oui":"non"}</span>
</div>
</div>
<div class="d-flex flex-column justify-content-center">
<button
type="button"
class="btn btn-outline-danger float-end"
on:click={() => key.delete().then(() => { keysP = getKeys(); })}
>
Supprimer la clef
</button>
</div>
</div>
{/await}
{/each}
{:else}
<p>
Afin de pouvoir les vérifier, veuillez envoyer votre clef publique dans le formulaire ci-dessous.
Utilisez la commande <code>gpg --export --armor {#if $user}{$user.email}{:else}login_x@epita.fr{/if}</code>&nbsp;:
</p>
<form class="container" on:submit|preventDefault={submitPGPKey}>
<textarea
class="form-control"
rows="10"
bind:value={mykey}
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----
QmllbiBzw7tyIHF1ZSBjJ2VzdCB1bmUgY2hhw65uZSBxdWkgdmV1dCBkaXJlIHF1
ZWxxdWUgY2hvc2UK ...
-----END PGP PUBLIC KEY BLOCK-----"
></textarea>
<button
type="submit"
class="mt-2 btn btn-primary"
>
Enregistrer cette clef PGP
</button>
</form>
{/if}
{/await}

View File

@ -9,6 +9,7 @@
</script>
<script lang="ts">
import UserKeys from '../../../components/UserKeys.svelte';
import UserSurveys from '../../../components/UserSurveys.svelte';
import { user } from '../../../stores/user';
import { getSurveys } from '../../../lib/surveys';
@ -102,6 +103,14 @@
</div>
</div>
</div>
<div class="card-header">
<h3 class="card-title">
Clefs
</h3>
</div>
<UserKeys
{student}
/>
<div class="card-header">
<button
class="btn btn-secondary float-end"

View File

@ -23,6 +23,7 @@ func declareAPIAuthUsersRoutes(router *gin.RouterGroup) {
})
declareAPIAuthSurveysRoutes(usersRoutes)
declareAPIAuthKeysRoutes(usersRoutes)
}
func declareAPIAdminUsersRoutes(router *gin.RouterGroup) {